neohive 6.2.2 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [6.3.0] - 2026-04-05
4
+
5
+ ### Added
6
+
7
+ - **Unified `next_action` response chain** — every MCP tool response now includes a single `next_action` field that tells the AI agent exactly what to do next, replacing 10+ scattered hint fields (`_listen`, `_nudge`, `hint`, `action_required`, `unread_action`, `you_have_messages`, `urgent`, `mode_hint`, `_protocol`, etc.)
8
+ - **Tool-specific directives** — each tool sets a context-aware `next_action` (e.g. `update_task(done)` → "Send a summary via send_message(), then call listen()"; `lock_file` → "Edit the file, then call unlock_file() when done")
9
+ - **Coordinator-aware middleware** — post-processing middleware detects responsive coordinators and replaces any `listen()` directive with `consume_messages()` or removes it entirely, preventing coordinators from blocking in listen mode
10
+ - **Persistent listen loop** — `listen()` and `listen_group()` no longer return `retry: true` on timeout; they loop internally with fresh watchers and heartbeats, so agents cannot break out of listen mode
11
+ - **Managed mode next_action** — `buildListenGroupResponse` respects `should_respond` and floor state; agents without the floor get "do NOT respond" instead of "reply via send_message()"
12
+ - **Autonomous get_work() directives** — all 10 return types from `get_work()` carry specific `next_action` values guiding agents through the workflow/verify/advance cycle
13
+ - **Documentation** — `docs/reference/next-action-chain.md` with flow diagrams for standard agents, agent-to-agent communication, responsive/autonomous coordinators, managed mode, and persistent listen
14
+
15
+ ### Changed
16
+
17
+ - **Post-processing middleware rewrite** — removed the scattered nudge/unread/listen injection block (~80 lines) and replaced with a unified `next_action` block using priority logic: tool-specific > coordinator override > call-count warning > urgent messages > pending messages > default
18
+ - **`buildMessageResponse`** — `_protocol` field inside message objects replaced with top-level `next_action`; `coordinator_mode` field removed from listen responses
19
+ - **`buildListenGroupResponse`** — simplified `next_action` with mode-aware branching (autonomous/managed/standard)
20
+ - **`send_message` / `broadcast`** — removed `you_have_messages`, `urgent`, `mode_hint` fields; added `next_action: "Call listen() to receive replies."`
21
+ - **`verify_and_advance`** — replaced verbose `message` strings with concise `next_action` directives
22
+
23
+ ### Fixed
24
+
25
+ - **Responsive coordinator contradiction** — `send_message`, `broadcast`, and `create_task` no longer unconditionally tell responsive coordinators to call `listen()`; middleware overrides any `listen()` directive for responsive coordinators
26
+ - **Managed mode contradiction** — `buildListenGroupResponse` no longer sets "Reply via send_message()" when `should_respond: false` and `instructions: "DO NOT RESPOND"` are also present
27
+ - **Autonomous listen_group retry** — removed contradictory `retry: true` from autonomous mode timeout path where `next_action` directs to `get_work()` instead
28
+
3
29
  ## [6.1.0] - 2026-04-04
4
30
 
5
31
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neohive",
3
- "version": "6.2.2",
3
+ "version": "6.3.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
@@ -584,14 +584,14 @@ function buildMessageResponse(msg, consumedIds) {
584
584
  }
585
585
  } catch (e) { log.debug('task reminder in listen failed:', e.message); }
586
586
 
587
- // Append report-back protocol reminder to all non-system messages
588
587
  const isSystemMsg = msg.from === '__system__' || msg.system === true;
589
- const reportBackReminder = isSystemMsg
590
- ? undefined
591
- : 'When done: send_message() with (1) what you did (2) files changed (3) findings (4) blockers. Then call listen().';
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().`;
592
591
 
593
592
  return {
594
593
  success: true,
594
+ next_action: nextAction,
595
595
  message: {
596
596
  id: msg.id,
597
597
  from: msg.from,
@@ -600,11 +600,9 @@ function buildMessageResponse(msg, consumedIds) {
600
600
  priority: classifyPriority(msg),
601
601
  ...(msg.reply_to && { reply_to: msg.reply_to }),
602
602
  ...(msg.thread_id && { thread_id: msg.thread_id }),
603
- ...(reportBackReminder && { _protocol: reportBackReminder }),
604
603
  },
605
604
  pending_count: pendingCount,
606
605
  agents_online: agentsOnline,
607
- coordinator_mode: getConfig().coordinator_mode || 'responsive',
608
606
  ...(taskReminder && { task_reminder: taskReminder }),
609
607
  };
610
608
  }
@@ -1159,57 +1157,39 @@ function buildGuide(level = 'standard') {
1159
1157
  const managed = getManagedConfig();
1160
1158
  const isManager = managed.manager === registeredName;
1161
1159
  if (isManager) {
1162
- rules.push('YOU ARE THE MANAGER. You control the conversation flow, assign work, and track progress.');
1163
- rules.push('TASK MANAGEMENT: Use create_task(title, description, assignee) to assign work to agents. Use create_workflow(name, steps) to create multi-step plans. These show up in the Tasks and Plan tabs of the dashboard.');
1164
- rules.push('YOUR MANAGER LOOP: 1) Plan the work — create tasks and workflows for the team. 2) Use yield_floor(agent) to give each agent their turn to speak. 3) Call listen() to wait for responses. 4) When agents report completion, use update_task(id, status="done") to mark tasks done and advance_workflow() to move the plan forward. 5) Assign next tasks and repeat.');
1165
- rules.push('PHASES: Use set_phase("discussion") for brainstorming, set_phase("planning") to assign tasks, set_phase("execution") for agents to work, set_phase("review") to collect results.');
1166
- rules.push('Use send_message() to give instructions. Use broadcast() for team announcements.');
1167
- rules.push('STRICT RULE: After EVERY action, call listen() to wait for responses. NEVER use sleep(). Your loop is: act → listen() → act → listen(). This loop NEVER ends.');
1160
+ rules.push('ROLE: You are the Manager. You assign work and control who speaks.');
1161
+ rules.push('LOOP: create_task/create_workflow yield_floor(agent) listen() process results → repeat. Never stop.');
1162
+ rules.push('Use update_task(id, status="done") and advance_workflow() as agents finish.');
1163
+ rules.push('Use set_phase() to move through discussion planning execution review.');
1168
1164
  } else {
1169
- rules.push('YOU ARE IN MANAGED MODE. The manager controls who speaks. Call listen() to wait for your turn.');
1170
- rules.push('TASK TRACKING: When the manager assigns you a task, call update_task(id, status="in_progress") to claim it. When you finish, call update_task(id, status="done") and report back to the manager. If your task is a workflow step, call advance_workflow() after completing it.');
1171
- rules.push('YOUR LOOP: 1) Call listen() wait for messages and floor assignments. 2) When you receive work, update the task to "in_progress", do the work, update to "done", respond to the manager. 3) Call listen() again immediately. This loop NEVER ends.');
1172
- rules.push('STRICT RULES: NEVER use sleep(). NEVER use check_messages() in a loop. NEVER call get_work() in managed mode. Your ONLY loop is: listen() → work → update task → respond → listen(). If listen() times out, call listen() again immediately.');
1165
+ rules.push('ROLE: Managed agent. The manager controls your turn.');
1166
+ rules.push('LOOP: listen() receive work update_task(id, "in_progress") do work update_task(id, "done") send_message(manager, summary) listen(). Never stop.');
1167
+ rules.push('Never call get_work() or check_messages() in managed mode.');
1173
1168
  }
1174
- rules.push('Keep messages to 2-3 paragraphs max.');
1175
- rules.push('When you finish work, report what you did and what files you changed.');
1169
+ rules.push('Keep messages short (2-3 paragraphs). Report what you did and what files changed.');
1176
1170
  }
1177
1171
  // === AUTONOMOUS MODE: completely different guide ===
1178
1172
  else if (autonomousActive) {
1179
1173
  if (isAdvisor) {
1180
- // Advisor Agent: strategic thinker reads everything, suggests improvements
1181
- rules.push('YOU ARE THE ADVISOR. You do NOT write code. You READ all messages and completed work, then give strategic ideas, suggestions, and improvements to the team.');
1182
- rules.push('YOUR ADVISOR LOOP: 1) Call get_work() — it returns recent messages, completed tasks, active workflows, KB lessons, and decisions. 2) THINK DEEPLY about what you see: Are there better approaches? Missing features? Architectural issues? Assumptions that should be challenged? 3) Send your insights to the team via send_message. Be specific and actionable. 4) Call get_work() again. NEVER stop thinking.');
1183
- rules.push('WHAT TO LOOK FOR: Patterns the team is missing. Better approaches to current problems. Connections between different agents\' work. Assumptions that need challenging. Missing edge cases. Architectural improvements. Features the team should build next.');
1184
- rules.push('HOW TO ADVISE: Send suggestions via send_message to specific agents or broadcast to the team. Be concise and actionable. Explain WHY your suggestion is better, not just WHAT to do differently. Reference specific code or messages when possible.');
1185
- rules.push('NEVER ask the user what to do. You generate ideas from observing the team. The team decides whether to follow your advice.');
1174
+ rules.push('ROLE: Advisor. You do NOT write code. You observe the team and suggest improvements.');
1175
+ rules.push('LOOP: get_work() analyze messages/tasks/decisions send_message(agent, insight) get_work(). Never stop.');
1176
+ rules.push('Focus on: missed patterns, better approaches, edge cases, architectural issues.');
1186
1177
  } else if (isMonitor) {
1187
- // Monitor Agent: system overseer watches the team, not the code
1188
- rules.push('YOU ARE THE SYSTEM MONITOR. You do NOT write code. You do NOT do regular work. You watch the TEAM and keep it functioning.');
1189
- rules.push('YOUR MONITOR LOOP: 1) Call get_work() — it returns a health check report instead of a work assignment. 2) Analyze the report: who is idle? Who is stuck? Are tasks bouncing between agents? Is the queue growing? 3) INTERVENE: reassign stuck tasks, nudge idle agents via send_message, rebalance roles if workload is uneven. 4) Log every intervention to your workspace via workspace_write(key="_monitor_log"). 5) Call get_work() again. NEVER stop monitoring.');
1190
- rules.push('WHAT TO WATCH FOR: Idle agents (>2 minutes without activity). Circular escalations (same task rejected by 3+ agents). Queue buildup (more pending tasks than agents can handle). Stuck workflow steps (>15 minutes in progress). Agents with high rejection rates.');
1191
- rules.push('HOW TO INTERVENE: Use send_message to nudge idle agents. Use update_task to reassign stuck tasks. Call rebalanceRoles() via the system to shift workload. Use broadcast for team-wide alerts.');
1192
- rules.push('NEVER ask the user what to do. You ARE the system intelligence. The team relies on you to keep them productive.');
1178
+ rules.push('ROLE: System Monitor. You do NOT write code. You keep the team functioning.');
1179
+ rules.push('LOOP: get_work() analyze health report intervene (reassign stuck tasks, nudge idle agents) get_work(). Never stop.');
1180
+ rules.push('Watch for: idle agents (>2min), circular escalations, queue buildup, stuck workflows (>15min).');
1193
1181
  } else if (isQualityLead) {
1194
- rules.push('YOU ARE THE QUALITY LEAD. Your job is to review ALL work from the team, find bugs, suggest improvements, and keep the team iterating until the work is genuinely excellent. Never approve without checking. Never let mediocre work pass.');
1195
- rules.push('YOUR QUALITY LOOP: 1) Call get_work() prioritize review requests and completed steps. 2) Review the work thoroughly — read the code, check for bugs, verify correctness. 3) If good: approve via submit_review() and call verify_and_advance(). 4) If needs improvement: use submit_review(status="changes_requested") with specific feedback. The author will fix and re-submit automatically. 5) Call get_work() again. NEVER stop reviewing.');
1196
- rules.push('QUALITY STANDARDS: Check for bugs, edge cases, security issues, code style, and correctness. Read the actual files — do not trust summaries. If something looks wrong, flag it.');
1197
- rules.push('NEVER ask the user what to do. NEVER wait for human approval. You ARE the approval gate. The team works, you review, they improve, you re-review. This cycle continues until the work is excellent.');
1182
+ rules.push('ROLE: Quality Lead. You review ALL team work. Never approve without reading the actual code.');
1183
+ rules.push('LOOP: get_work() review code submit_review()/verify_and_advance() or submit_review(status="changes_requested", feedback) get_work(). Never stop.');
1198
1184
  } else {
1199
- rules.push('YOUR WORK LOOP (follow this ALWAYS): 1) Call get_work() it gives you your next assignment. 2) Do the work write code, run tests, implement features. 3) Call verify_and_advance() verify your work, auto-advance the workflow. 4) Call get_work() again — get your next assignment. 5) NEVER stop. NEVER wait for approval. NEVER call listen_group() in autonomous mode — use get_work() instead.');
1185
+ rules.push('LOOP: get_work() do the work verify_and_advance() get_work(). Never stop. Never wait for approval.');
1200
1186
  rules.push(qualityLeadName
1201
- ? 'SELF-CONTINUATION: After completing work, report results to ' + qualityLeadName + ' (Quality Lead) via send_message. After Quality Lead approves, find next work via get_work(). NEVER ask the user what to do next — the system assigns work automatically.'
1202
- : 'SELF-CONTINUATION: After completing work, call get_work() to find your next task. NEVER ask the user what to do next — the system assigns work automatically. NEVER stop working.');
1203
- }
1204
- rules.push('IF STUCK: Try a different approach (max 3 attempts). Ask the team for help via send_message. If still stuck after help, move to next available task. NEVER wait silently. ALWAYS be working on something.');
1205
- rules.push('IF YOUR WORK FAILS: Analyze WHY it failed. Record the learning via verify_and_advance(learnings: "..."). Retry with improvements. After 3 retries, escalate to team and move to other work.');
1206
- rules.push('IF NOTHING TO DO: get_work() handles this it checks workflows, tasks, reviews, and help requests. It will find you something. Trust the loop.');
1207
- rules.push('Keep messages to 2-3 paragraphs max.');
1208
- rules.push('When you finish work, report what you did and what files you changed.');
1209
- rules.push('Lock files before editing shared code (lock_file / unlock_file).');
1210
- // UE5 safety rules — prevent concurrent editor operations
1211
- rules.push('UE5 SAFETY: BEFORE any Unreal Engine editor operation (spawning, modifying scene, placing assets): call lock_file("ue5-editor"). BEFORE compiling/building: call lock_file("ue5-compile"). Unlock immediately after. Only ONE agent can hold each lock — others must wait.');
1212
- rules.push('Log team decisions with log_decision() so they are not re-debated.');
1187
+ ? 'After completing work, report to ' + qualityLeadName + ' (Quality Lead) via send_message, then get_work().'
1188
+ : 'After completing work, call get_work() for your next task.');
1189
+ }
1190
+ rules.push('If stuck: retry up to 3 times, then ask team via send_message, then move to next task.');
1191
+ rules.push('Lock files before editing (lock_file/unlock_file). Log decisions with log_decision().');
1192
+ rules.push('Keep messages short (2-3 paragraphs). Report what you did and what files changed.');
1213
1193
 
1214
1194
  // User-customizable project-specific rules
1215
1195
  const guideFile = path.join(DATA_DIR, 'guide.md');
@@ -1239,101 +1219,64 @@ function buildGuide(level = 'standard') {
1239
1219
  }
1240
1220
  }
1241
1221
 
1242
- return {
1222
+ const result = {
1243
1223
  rules,
1244
- project_rules: projectRules.length > 0 ? projectRules : undefined,
1245
- tier_info: `${rules.length} rules (AUTONOMOUS MODE, ${aliveCount} agents, role: ${myRole || 'unassigned'})`,
1246
- first_steps: isAdvisor
1247
- ? '1. Call get_work() to get team context (messages, tasks, decisions). 2. Think deeply about patterns, improvements, missing features. 3. Send insights to team. 4. Call get_work() again. Never stop thinking.'
1248
- : isMonitor
1249
- ? '1. Call get_work() to get system health report. 2. Analyze: idle agents, stuck tasks, circular escalations. 3. Intervene: reassign, nudge, rebalance. 4. Call get_work() again. Never stop monitoring.'
1250
- : isQualityLead
1251
- ? '1. Call get_work() to find work to review. 2. Review thoroughly. 3. Approve or request changes. 4. Call get_work() again. Never stop.'
1252
- : '1. Call get_work() to get your assignment. 2. Do the work. 3. Call verify_and_advance(). 4. Call get_work() again. Never stop.',
1224
+ first_steps: 'Call get_work() to get your next assignment.',
1253
1225
  autonomous_mode: true,
1254
1226
  your_role: myRole || undefined,
1255
- quality_lead: qualityLeadName || undefined,
1256
- tool_categories: {
1257
- 'WORK LOOP': 'get_work, verify_and_advance, retry_with_improvement',
1258
- 'MESSAGING': 'send_message, broadcast, check_messages, consume_messages, get_history, handoff, share_file',
1259
- 'COORDINATION': 'get_briefing, log_decision, get_decisions, kb_write, kb_read, kb_list',
1260
- 'TASKS': 'create_task, update_task, list_tasks, suggest_task',
1261
- 'QUALITY': 'request_review, submit_review',
1262
- 'SAFETY': 'lock_file, unlock_file',
1263
- },
1264
1227
  };
1228
+ if (projectRules.length > 0) result.project_rules = projectRules;
1229
+ return result;
1265
1230
  }
1266
1231
 
1267
1232
  // === STANDARD MODE (non-autonomous) ===
1268
- // Self-continuation rules apply in standard mode too (for 2+ agent teams)
1269
1233
  if (aliveCount >= 2 && (mode === 'group' || mode === 'managed')) {
1270
1234
  if (isQualityLead) {
1271
- rules.push('YOU ARE THE QUALITY LEAD. Review all work from teammates. Use submit_review() to approve or request changes. Never let mediocre work pass. Never ask the user what to do — you are the approval gate.');
1235
+ rules.push('ROLE: Quality Lead. Review all team work via submit_review(). You are the approval gate.');
1272
1236
  } else if (qualityLeadName) {
1273
- rules.push('SELF-CONTINUATION: After completing work, report to ' + qualityLeadName + ' (Quality Lead). After approval, find next work. NEVER ask the user what to do next.');
1237
+ rules.push('After completing work, report to ' + qualityLeadName + ' (Quality Lead), then continue.');
1274
1238
  }
1275
1239
  }
1276
1240
 
1277
- // Lead/Coordinator mode: responsive (stay with human) vs autonomous (run in listen loop)
1278
1241
  if (isLeadRole && aliveCount >= 2) {
1279
1242
  const coordinatorMode = getConfig().coordinator_mode || 'responsive';
1280
1243
  if (coordinatorMode === 'responsive') {
1281
- rules.push('RESPONSIVE COORDINATOR PATTERN: Use consume_messages() at the start of each interaction to check for agent updates non-blockingly. Process all returned messages, assign work, then return to the human immediately. Do NOT block in listen() — you need to stay responsive to both agents and the user.');
1244
+ rules.push('COORDINATOR: Use consume_messages() to check updates non-blockingly. Do NOT block in listen() — stay responsive to the user.');
1282
1245
  } else {
1283
- rules.push('AUTONOMOUS COORDINATOR PATTERN: Use listen() to wait for agent results. Process responses, delegate follow-up work, and continue the listen loop. Only return to the human when all tasks are complete or when you hit a blocker that requires human input.');
1246
+ rules.push('COORDINATOR: Use listen() to wait for agent results. Only return to human when all tasks are done or blocked.');
1284
1247
  }
1285
- rules.push('CRITICAL: You are a Coordinator. You MUST NOT edit files, write code, or use tools like Edit/Write/Bash for code changes. Your tools are: send_message, create_task, update_task, create_workflow, advance_workflow, workflow_status, list_tasks, consume_messages, broadcast, kb_write, kb_read, log_decision. Delegate ALL code work to other agents.');
1248
+ rules.push('Coordinators do NOT edit files or write code. Delegate ALL code work to other agents.');
1286
1249
  }
1287
1250
 
1288
- // Tier 0 — THE one rule (always included at every level)
1289
1251
  const listenCmd = isManagedMode() ? 'listen()' : (mode === 'group' ? 'listen_group()' : 'listen()');
1290
- rules.push(`AFTER EVERY ACTION, call ${listenCmd}. This is how you receive messages. NEVER skip this. NEVER use sleep(). NEVER poll with check_messages(). ${listenCmd} is your ONLY way to receive messages.`);
1252
+ rules.push(`After EVERY action, call ${listenCmd}. Never use sleep() or poll with check_messages().`);
1291
1253
 
1292
- // Minimal level: Tier 0 only — for experienced agents refreshing rules
1293
1254
  if (level === 'minimal') {
1294
- rules.push('Call get_briefing() when joining a project or after being away.');
1295
- rules.push('Lock files before editing shared code (lock_file / unlock_file).');
1255
+ rules.push('Lock files before editing (lock_file/unlock_file).');
1296
1256
  if (mode === 'group' || mode === 'managed') {
1297
- rules.push('Use reply_to when responding you get faster cooldown (500ms vs default).');
1298
- rules.push('Messages not addressed to you show should_respond: false. Only respond if you have something new to add.');
1257
+ rules.push('Use reply_to for threading. Ignore messages with should_respond: false unless relevant.');
1299
1258
  }
1300
1259
  return {
1301
1260
  rules,
1302
- tier_info: `${rules.length} rules (minimal level, ${aliveCount} agents)`,
1303
1261
  first_steps: mode === 'direct'
1304
- ? '1. Call list_agents() to see who is online. 2. Send a message or call listen() to wait.'
1262
+ ? 'Call list_agents() to see who is online, then send a message or listen().'
1305
1263
  : mode === 'managed'
1306
- ? `1. Call get_briefing() for project context. 2. Call listen() to wait for the manager. 3. Respond when given the floor, then listen() again.`
1307
- : `1. Call get_briefing() for project context. 2. Call listen_group() to join. 3. Respond and listen_group() again.`,
1264
+ ? 'Call get_briefing(), then listen() to wait for the manager.'
1265
+ : 'Call get_briefing(), then listen() to join.',
1308
1266
  };
1309
1267
  }
1310
1268
 
1311
- // Tier 1 core behavior (standard + full)
1312
- rules.push('Call get_briefing() when joining a project or after being away.');
1313
- rules.push('Keep messages to 2-3 paragraphs max.');
1314
- rules.push('When you finish work, report what you did and what files you changed.');
1315
- rules.push('Lock files before editing shared code (lock_file / unlock_file).');
1316
- // UE5 safety rules — prevent concurrent editor operations
1317
- rules.push('UE5 SAFETY: BEFORE any Unreal Engine editor operation (spawning, modifying scene, placing assets): call lock_file("ue5-editor"). BEFORE compiling/building: call lock_file("ue5-compile"). Unlock immediately after. Only ONE agent can hold each lock — others must wait.');
1269
+ rules.push('Keep messages short (2-3 paragraphs). Report what you did and what files changed.');
1270
+ rules.push('Lock files before editing (lock_file/unlock_file). Log decisions with log_decision().');
1318
1271
 
1319
- // Tier 2 — group mode features (shown when group or managed mode)
1320
1272
  if (mode === 'group' || mode === 'managed') {
1321
- rules.push('Use reply_to when responding you get faster cooldown (500ms vs default).');
1322
- rules.push('Messages not addressed to you show should_respond: false. Only respond if you have something new to add.');
1323
- rules.push('Log team decisions with log_decision() so they are not re-debated.');
1273
+ rules.push('Use reply_to for threading (faster cooldown). Ignore should_respond: false unless relevant.');
1324
1274
  }
1325
-
1326
- // Tier 2b — channels (shown when channels exist beyond #general)
1327
1275
  if (hasChannels) {
1328
- rules.push('Join relevant channels with join_channel(). You only see messages from channels you joined.');
1329
- rules.push('Use channel parameter on send_message to keep discussions focused.');
1276
+ rules.push('Use join_channel() and channel param on send_message to keep discussions focused.');
1330
1277
  }
1331
-
1332
- // Tier 3 — large teams (shown when 5+ agents)
1333
1278
  if (aliveCount >= 5) {
1334
- rules.push(`${listenCmd} blocks until messages arrive. NEVER stop listening. NEVER use sleep() or check_messages() loops.`);
1335
- rules.push('Tasks auto-create channels (#task-xxx). Use them for focused discussion instead of #general.');
1336
- rules.push('Use channels to split into sub-teams. Do not discuss everything in #general.');
1279
+ rules.push('Use task channels (#task-xxx) for focused discussion instead of #general.');
1337
1280
  }
1338
1281
 
1339
1282
  // User-customizable project-specific rules from .neohive/guide.md
@@ -1364,51 +1307,25 @@ function buildGuide(level = 'standard') {
1364
1307
  }
1365
1308
  }
1366
1309
 
1367
- const result = {
1368
- rules,
1369
- project_rules: projectRules.length > 0 ? projectRules : undefined,
1370
- tier_info: `${rules.length} rules (${aliveCount} agents, ${mode} mode${hasChannels ? ', channels active' : ''})`,
1371
- first_steps: mode === 'direct'
1372
- ? '1. Call list_agents() to see who is online. 2. Send a message or call listen() to wait.'
1373
- : '1. Call get_briefing() for project context. 2. Call listen_group() to join. 3. Respond and listen_group() again.',
1374
- tool_categories: {
1375
- 'MESSAGING': 'send_message, broadcast, listen_group, listen, check_messages, consume_messages, get_history, get_summary, search_messages, handoff, share_file',
1376
- 'COORDINATION': 'get_briefing, log_decision, get_decisions, kb_write, kb_read, kb_list, call_vote, cast_vote, vote_status',
1377
- 'TASKS': 'create_task, update_task, list_tasks, declare_dependency, check_dependencies, suggest_task',
1378
- 'QUALITY': 'update_progress, get_progress, request_review, submit_review, get_reputation',
1379
- 'SAFETY': 'lock_file, unlock_file',
1380
- 'CHANNELS': 'join_channel, leave_channel, list_channels',
1381
- ...(mode === 'managed' ? { 'MANAGED MODE': 'claim_manager, yield_floor, set_phase' } : {}),
1382
- },
1383
- };
1384
-
1385
- // Full level: add tool descriptions for complete reference
1386
- if (level === 'full') {
1387
- result.tool_details = {
1388
- 'listen_group': 'Blocks until messages arrive. Returns batch with priorities, context, agent statuses.',
1389
- 'send_message': 'Send to agent (to param). reply_to for threading. channel for sub-channels.',
1390
- 'lock_file / unlock_file': 'Exclusive file locking. Auto-releases on disconnect.',
1391
- 'log_decision': 'Persist decisions to prevent re-debating. Visible in get_briefing().',
1392
- 'create_task / update_task': 'Structured task management. Auto-creates channels at 5+ agents.',
1393
- 'kb_write / kb_read': 'Shared knowledge base. Any agent can read/write.',
1394
- 'suggest_task': 'AI-suggested next task based on your strengths and pending work.',
1395
- 'request_review / submit_review': 'Structured code review workflow with notifications.',
1396
- 'declare_dependency': 'Block a task until another completes. Auto-notifies on resolution.',
1397
- 'get_compressed_history': 'Summarized history for catching up without context overflow.',
1398
- };
1399
- }
1400
-
1401
- // Task reminder: show agent's pending/in_progress tasks so they remember to update them
1310
+ // Task reminder
1402
1311
  if (registeredName) {
1403
1312
  try {
1404
1313
  const myTasks = getTasks().filter(t => t.assignee === registeredName && (t.status === 'pending' || t.status === 'in_progress'));
1405
1314
  if (myTasks.length > 0) {
1406
- result.your_tasks = myTasks.map(t => ({ id: t.id, title: t.title, status: t.status }));
1407
- rules.push(`TASK STATUS: You have ${myTasks.length} task(s). Use update_task(task_id, "in_progress") when starting and update_task(task_id, "done") when complete. Your tasks: ${myTasks.map(t => t.id + ' "' + t.title.substring(0, 40) + '" (' + t.status + ')').join('; ')}`);
1315
+ rules.push(`You have ${myTasks.length} task(s): ${myTasks.map(t => t.id + ' "' + t.title.substring(0, 40) + '" (' + t.status + ')').join('; ')}. Update status with update_task().`);
1408
1316
  }
1409
1317
  } catch (e) { log.debug('task reminder in guide failed:', e.message); }
1410
1318
  }
1411
1319
 
1320
+ const result = {
1321
+ rules,
1322
+ first_steps: mode === 'direct'
1323
+ ? 'Call list_agents() to see who is online, then send a message or listen().'
1324
+ : 'Call get_briefing(), then listen() to join.',
1325
+ };
1326
+
1327
+ if (projectRules.length > 0) result.project_rules = projectRules;
1328
+
1412
1329
  // Cache the result for subsequent calls with same params
1413
1330
  _guideCache = { key: cacheKey, result };
1414
1331
  return result;
@@ -1567,79 +1484,82 @@ function toolRegister(name, provider = null, skills = null) {
1567
1484
  const config = getConfig();
1568
1485
  const mode = config.conversation_mode || 'direct';
1569
1486
  const otherAgents = Object.keys(getAgents()).filter(n => n !== name);
1487
+ const guide = buildGuide();
1570
1488
 
1571
- const result = {
1572
- success: true,
1573
- message: `Registered as Agent ${name} (PID ${process.pid})`,
1574
- conversation_mode: mode,
1575
- agents_online: otherAgents,
1576
- guide: buildGuide(),
1577
- };
1489
+ // Auto-assign roles when 2+ agents are online
1490
+ const aliveCount = Object.values(getAgents()).filter(a => isPidAlive(a.pid, a.last_activity)).length;
1491
+ let assignedRole = guide.your_role || undefined;
1492
+ if (aliveCount >= 2) {
1493
+ try {
1494
+ const roleAssignments = autoAssignRoles();
1495
+ if (roleAssignments && roleAssignments[name]) assignedRole = roleAssignments[name];
1496
+ } catch (e) { log.debug("role assignment failed:", e.message); }
1497
+ }
1578
1498
 
1579
- // Recovery: if this agent has prior data, include it
1499
+ // --- Recovery detection ---
1580
1500
  const myTasks = getTasks().filter(t => t.assignee === name && t.status !== 'done');
1581
1501
  const myWorkspace = getWorkspace(name);
1582
- // Scale fix: tail-read last 30 messages instead of entire history
1583
1502
  const recentHistory = tailReadJsonl(getHistoryFile(currentBranch), 30);
1584
1503
  const myRecentMsgs = recentHistory.filter(m => m.to === name || m.from === name).slice(-5);
1504
+ let isResuming = false;
1505
+ let resumeContext = null;
1585
1506
 
1586
- if (myTasks.length > 0 || Object.keys(myWorkspace).length > 0 || myRecentMsgs.length > 0) {
1587
- result.recovery = {};
1588
- if (myTasks.length > 0) result.recovery.your_active_tasks = myTasks.map(t => ({ id: t.id, title: t.title, status: t.status }));
1589
- if (Object.keys(myWorkspace).length > 0) result.recovery.your_workspace_keys = Object.keys(myWorkspace);
1590
- if (myRecentMsgs.length > 0) result.recovery.recent_messages = myRecentMsgs.map(m => ({ from: m.from, to: m.to, preview: m.content.substring(0, 100), timestamp: m.timestamp }));
1591
- result.recovery.hint = 'You have prior context from a previous session. Call get_briefing() for a full project summary.';
1592
- }
1593
-
1594
- // Auto-recovery: load crash snapshot if it exists (TTL: 1 hour)
1595
1507
  const recoveryFile = path.join(DATA_DIR, `recovery-${name}.json`);
1596
1508
  if (fs.existsSync(recoveryFile)) {
1597
1509
  try {
1598
1510
  const snapshot = JSON.parse(fs.readFileSync(recoveryFile, 'utf8'));
1599
1511
  const snapshotAge = Date.now() - new Date(snapshot.died_at).getTime();
1600
1512
  if (snapshotAge > 3600000) {
1601
- // Stale snapshot (>1 hour) — discard
1602
1513
  try { fs.unlinkSync(recoveryFile); } catch {}
1603
1514
  } else {
1604
- if (!result.recovery) result.recovery = {};
1605
- result.recovery.previous_session = true;
1606
- result.recovery.died_at = snapshot.died_at;
1607
- result.recovery.crashed_ago = Math.round(snapshotAge / 1000) + 's';
1608
- if (snapshot.active_tasks && snapshot.active_tasks.length > 0) result.recovery.your_active_tasks = snapshot.active_tasks;
1609
- if (snapshot.locked_files && snapshot.locked_files.length > 0) {
1610
- result.recovery.locked_files_released = snapshot.locked_files;
1611
- result.recovery.lock_note = 'These files were locked by your previous session. Locks have been auto-released. Re-lock them with lock_file() before editing.';
1612
- }
1613
- if (snapshot.channels && snapshot.channels.length > 0) result.recovery.your_channels = snapshot.channels;
1614
- if (snapshot.last_messages_sent) result.recovery.last_messages_sent = snapshot.last_messages_sent;
1615
- // Agent memory fields
1616
- if (snapshot.decisions_made && snapshot.decisions_made.length > 0) result.recovery.decisions_made = snapshot.decisions_made;
1617
- if (snapshot.tasks_completed && snapshot.tasks_completed.length > 0) result.recovery.tasks_completed = snapshot.tasks_completed;
1618
- if (snapshot.kb_entries_written && snapshot.kb_entries_written.length > 0) result.recovery.kb_entries_written = snapshot.kb_entries_written;
1619
- if (snapshot.graceful) result.recovery.was_graceful = true;
1620
- result.recovery.hint = snapshot.graceful
1621
- ? 'You are RESUMING from a previous session that exited gracefully. Your memory (decisions, completed tasks, KB entries) is below. Continue where you left off.'
1622
- : 'You are RESUMING a previous session that crashed. Review your active tasks and locked files below, then continue where you left off. Do NOT restart work from scratch.';
1623
- // Clean up snapshot after loading
1515
+ isResuming = true;
1516
+ resumeContext = {
1517
+ crashed_ago: Math.round(snapshotAge / 1000) + 's',
1518
+ was_graceful: !!snapshot.graceful,
1519
+ };
1520
+ if (snapshot.active_tasks && snapshot.active_tasks.length > 0) resumeContext.active_tasks = snapshot.active_tasks;
1521
+ if (snapshot.locked_files && snapshot.locked_files.length > 0) resumeContext.files_to_relock = snapshot.locked_files;
1522
+ if (snapshot.decisions_made && snapshot.decisions_made.length > 0) resumeContext.decisions_made = snapshot.decisions_made;
1523
+ if (snapshot.tasks_completed && snapshot.tasks_completed.length > 0) resumeContext.tasks_completed = snapshot.tasks_completed;
1624
1524
  try { fs.unlinkSync(recoveryFile); } catch {}
1625
1525
  }
1626
1526
  } catch (e) { log.debug("recovery file parse failed:", e.message); }
1627
1527
  }
1628
1528
 
1629
- // Notify other agents
1630
- fireEvent('agent_join', { agent: name });
1529
+ if (!isResuming && (myTasks.length > 0 || myRecentMsgs.length > 0)) {
1530
+ isResuming = true;
1531
+ resumeContext = {};
1532
+ if (myTasks.length > 0) resumeContext.active_tasks = myTasks.map(t => ({ id: t.id, title: t.title, status: t.status }));
1533
+ }
1631
1534
 
1632
- // Auto-assign roles when 2+ agents are online
1633
- const aliveCount = Object.values(getAgents()).filter(a => isPidAlive(a.pid, a.last_activity)).length;
1634
- if (aliveCount >= 2) {
1635
- try {
1636
- const roleAssignments = autoAssignRoles();
1637
- if (roleAssignments && roleAssignments[name]) {
1638
- result.your_role = roleAssignments[name];
1639
- }
1640
- } catch (e) { log.debug("role assignment failed:", e.message); }
1535
+ // --- Build next_action: the ONE thing the agent should do right now ---
1536
+ let nextAction;
1537
+ if (isResuming && resumeContext && resumeContext.active_tasks && resumeContext.active_tasks.length > 0) {
1538
+ nextAction = `You have ${resumeContext.active_tasks.length} unfinished task(s). Call list_tasks() to see them and resume work.`;
1539
+ } else if (guide.first_steps) {
1540
+ const firstStep = guide.first_steps.split(/\d+\.\s+/).filter(Boolean)[0];
1541
+ nextAction = firstStep ? firstStep.trim().replace(/\.$/, '') : guide.first_steps;
1542
+ } else {
1543
+ nextAction = 'Call get_briefing() to load project context';
1641
1544
  }
1642
1545
 
1546
+ // --- Build the result: next_action FIRST, then context ---
1547
+ const result = {
1548
+ success: true,
1549
+ registered: name,
1550
+ next_action: nextAction,
1551
+ mode,
1552
+ agents_online: otherAgents,
1553
+ };
1554
+
1555
+ if (assignedRole) result.your_role = assignedRole;
1556
+ if (isResuming) result.resuming = resumeContext;
1557
+
1558
+ result.guide = guide;
1559
+
1560
+ // Notify other agents
1561
+ fireEvent('agent_join', { agent: name });
1562
+
1643
1563
  return result;
1644
1564
  } finally {
1645
1565
  unlockAgentsFile();
@@ -1659,9 +1579,8 @@ function setListening(isListening) {
1659
1579
  if (!registeredName) return;
1660
1580
  _isCurrentlyListening = !!isListening;
1661
1581
 
1662
- // Track listen calls in heartbeat for auto-nudge system
1663
1582
  if (isListening) {
1664
- touchActivity(true); // Mark as listen call
1583
+ touchActivity(true);
1665
1584
  }
1666
1585
 
1667
1586
  try {
@@ -2044,19 +1963,6 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
2044
1963
  result.note = `Agent "${to}" is currently working (not in listen mode). Message queued — they'll see it when they finish their current task and call listen_group().`;
2045
1964
  }
2046
1965
 
2047
- // Mode awareness hint: warn if agent seems to be in wrong mode
2048
- const currentMode = getConfig().conversation_mode || 'direct';
2049
- if (currentMode === 'group' || currentMode === 'managed') {
2050
- result.mode_hint = `You're in ${currentMode} mode. Use listen_group() (or listen() — both auto-detect) to stay in the conversation.`;
2051
- }
2052
-
2053
- // Nudge: check if THIS agent has unread messages waiting
2054
- const myPending = getUnconsumedMessages(registeredName);
2055
- if (myPending.length > 0) {
2056
- result.you_have_messages = myPending.length;
2057
- result.urgent = `You have ${myPending.length} unread message(s) waiting. Call listen_group() after this to read them.`;
2058
- }
2059
-
2060
1966
  // Coordinator enforcement: warn if sending work assignment without creating a task first
2061
1967
  const senderProfile = getProfiles()[registeredName];
2062
1968
  const senderRole = senderProfile && senderProfile.role ? senderProfile.role.toLowerCase() : '';
@@ -2067,7 +1973,7 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
2067
1973
  const recentTasks = getTasks().filter(t => {
2068
1974
  if (t.assignee !== to) return false;
2069
1975
  const age = Date.now() - new Date(t.created_at).getTime();
2070
- return age < 60000; // created in last 60 seconds
1976
+ return age < 60000;
2071
1977
  });
2072
1978
  if (recentTasks.length === 0) {
2073
1979
  result.task_warning = `No task created for this assignment to ${to}. Use create_task(title, description, "${to}") to formally track this work.`;
@@ -2075,6 +1981,8 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
2075
1981
  }
2076
1982
  }
2077
1983
 
1984
+ result.next_action = 'Call listen() to receive replies.';
1985
+
2078
1986
  return result;
2079
1987
  }
2080
1988
 
@@ -2132,10 +2040,7 @@ function toolBroadcast(content) {
2132
2040
  sendsSinceLastListen++;
2133
2041
  unaddressedSends++; // broadcasts are always unaddressed
2134
2042
  const aliveOthers = otherAgents.filter(n => { const a = agents[n]; return isPidAlive(a.pid, a.last_activity); });
2135
- const result = { success: true, messageId: msg.id, recipient_count: aliveOthers.length, sent_to: aliveOthers.map(n => ({ to: n, messageId: msg.id })) };
2136
- // Nudge for own unread messages
2137
- const myPending = getUnconsumedMessages(registeredName);
2138
- if (myPending.length > 0) { result.you_have_messages = myPending.length; result.urgent = `You have ${myPending.length} unread message(s). Call listen_group() soon.`; }
2043
+ const result = { success: true, messageId: msg.id, recipient_count: aliveOthers.length, next_action: 'Call listen() to receive replies.' };
2139
2044
  return result;
2140
2045
  }
2141
2046
 
@@ -2161,21 +2066,8 @@ function toolBroadcast(content) {
2161
2066
  touchActivity();
2162
2067
  lastSentAt = Date.now();
2163
2068
 
2164
- const result = { success: true, sent_to: ids, recipient_count: ids.length };
2069
+ const result = { success: true, sent_to: ids, recipient_count: ids.length, next_action: 'Call listen() to receive replies.' };
2165
2070
  if (skipped.length > 0) result.skipped = skipped;
2166
- // Show which recipients are busy vs listening
2167
- const agentsNow = getAgents();
2168
- const busy = ids.filter(function(i) { return agentsNow[i.to] && !agentsNow[i.to].listening_since; }).map(function(i) { return i.to; });
2169
- if (busy.length > 0) {
2170
- result.busy_agents = busy;
2171
- result.note = busy.join(', ') + (busy.length === 1 ? ' is' : ' are') + ' currently working (not listening). Messages queued.';
2172
- }
2173
- // Nudge for own unread messages
2174
- const myPending = getUnconsumedMessages(registeredName);
2175
- if (myPending.length > 0) {
2176
- result.you_have_messages = myPending.length;
2177
- result.urgent = `You have ${myPending.length} unread message(s). Call listen_group() soon.`;
2178
- }
2179
2071
  return result;
2180
2072
  }
2181
2073
 
@@ -2442,8 +2334,9 @@ async function toolListen(from = null, outcome = null, task_id = null, summary =
2442
2334
 
2443
2335
  let watcher;
2444
2336
  let fallbackInterval;
2337
+ let timer;
2338
+ let heartbeatTimer;
2445
2339
 
2446
- // Helper: check for new messages
2447
2340
  const checkMessages = () => {
2448
2341
  const { messages: newMsgs, newOffset } = readNewMessages(lastReadOffset);
2449
2342
  lastReadOffset = newOffset;
@@ -2463,34 +2356,44 @@ async function toolListen(from = null, outcome = null, task_id = null, summary =
2463
2356
  return false;
2464
2357
  };
2465
2358
 
2466
- try {
2467
- const msgFile = getMessagesFile(currentBranch);
2468
- watcher = fs.watch(msgFile, () => { checkMessages(); });
2469
- watcher.on('error', () => {});
2470
- } catch {
2471
- // Fallback: adaptive polling
2472
- let pollCount = 0;
2473
- fallbackInterval = setInterval(() => {
2474
- if (checkMessages()) { clearInterval(fallbackInterval); return; }
2475
- pollCount++;
2476
- if (pollCount === 10) {
2477
- clearInterval(fallbackInterval);
2478
- fallbackInterval = setInterval(() => {
2479
- if (checkMessages()) clearInterval(fallbackInterval);
2480
- }, 2000);
2481
- }
2482
- }, 500);
2483
- }
2359
+ // Auto-restart: instead of returning retry:true and hoping the agent
2360
+ // calls listen() again, loop internally. The agent stays blocked here
2361
+ // until a real message arrives or the IDE/user interrupts the tool call.
2362
+ function setupWatcher() {
2363
+ try { if (watcher) watcher.close(); } catch {}
2364
+ if (fallbackInterval) clearInterval(fallbackInterval);
2365
+ clearTimeout(timer);
2366
+ clearTimeout(heartbeatTimer);
2484
2367
 
2485
- // Heartbeat every 15s
2486
- const heartbeatTimer = setInterval(() => { touchHeartbeat(registeredName); }, 15000);
2368
+ try {
2369
+ const msgFile = getMessagesFile(currentBranch);
2370
+ watcher = fs.watch(msgFile, () => { checkMessages(); });
2371
+ watcher.on('error', () => {});
2372
+ } catch {
2373
+ let pollCount = 0;
2374
+ fallbackInterval = setInterval(() => {
2375
+ if (checkMessages()) { clearInterval(fallbackInterval); return; }
2376
+ pollCount++;
2377
+ if (pollCount === 10) {
2378
+ clearInterval(fallbackInterval);
2379
+ fallbackInterval = setInterval(() => {
2380
+ if (checkMessages()) clearInterval(fallbackInterval);
2381
+ }, 2000);
2382
+ }
2383
+ }, 500);
2384
+ }
2487
2385
 
2488
- // 5 min timeout MCP has no tool timeout, heartbeat keeps agent alive
2489
- const timer = setTimeout(() => {
2490
- setListening(false);
2491
- touchActivity();
2492
- done({ retry: true, message: 'No direct messages in 5 minutes. Call listen() again to keep waiting.' });
2493
- }, 300000);
2386
+ heartbeatTimer = setInterval(() => { touchHeartbeat(registeredName); }, 15000);
2387
+
2388
+ timer = setTimeout(() => {
2389
+ touchActivity();
2390
+ autoCompact();
2391
+ if (checkMessages()) return;
2392
+ setupWatcher();
2393
+ }, 300000);
2394
+ }
2395
+
2396
+ setupWatcher();
2494
2397
  });
2495
2398
  }
2496
2399
 
@@ -2596,7 +2499,7 @@ async function toolListenCodex(from = null, outcome = null, task_id = null, summ
2596
2499
 
2597
2500
  const timer = setTimeout(() => {
2598
2501
  setListening(false);
2599
- done({ retry: true, message: 'No messages yet. Call listen_codex() again to keep waiting.' });
2502
+ done({ retry: true, next_action: 'Call listen() again.', message: 'No messages yet.' });
2600
2503
  }, 45000);
2601
2504
  });
2602
2505
  }
@@ -2888,8 +2791,8 @@ async function toolListenGroup(outcome = null, task_id = null, summary = null) {
2888
2791
  if (fallbackInterval) clearInterval(fallbackInterval);
2889
2792
  if (batch && batch.length > 0) {
2890
2793
  resolve(buildListenGroupResponse(batch, consumed, registeredName, listenStart));
2891
- } else {
2892
- // Timeout return minimal empty response
2794
+ } else if (autonomousTimeout) {
2795
+ // Autonomous mode: return so agent goes back to get_work() loop
2893
2796
  setListening(false);
2894
2797
  sendsSinceLastListen = 0;
2895
2798
  sendLimit = 2;
@@ -2897,74 +2800,94 @@ async function toolListenGroup(outcome = null, task_id = null, summary = null) {
2897
2800
  resolve({
2898
2801
  messages: [],
2899
2802
  message_count: 0,
2900
- retry: true,
2901
- batch_summary: isManagedMode() ? 'No new messages — call listen() again immediately to keep waiting.' : 'No new messages — call listen_group() again to keep listening.',
2803
+ next_action: 'Call get_work() for your next assignment.',
2804
+ batch_summary: 'No new messages.',
2902
2805
  });
2806
+ } else {
2807
+ // Standard/group mode: don't return — re-enter the wait loop internally.
2808
+ // This prevents the agent from breaking out of listen mode.
2809
+ resolved = false;
2810
+ touchHeartbeat(registeredName);
2811
+ autoCompact();
2812
+ const freshBatch = collectBatch();
2813
+ if (freshBatch.length > 0) {
2814
+ resolved = true;
2815
+ resolve(buildListenGroupResponse(freshBatch, consumed, registeredName, listenStart));
2816
+ return;
2817
+ }
2818
+ setupWatchers();
2903
2819
  }
2904
2820
  };
2905
2821
 
2906
2822
  let watcher;
2907
2823
  let channelWatchers = [];
2908
2824
  let fallbackInterval;
2825
+ let timer;
2826
+ let heartbeatTimer;
2909
2827
 
2910
- try {
2911
- // Watch main messages file for changes
2912
- const msgFile = getMessagesFile(currentBranch);
2913
- watcher = fs.watch(msgFile, () => {
2914
- const batch = collectBatch();
2915
- if (batch.length > 0) done(batch);
2916
- });
2917
- watcher.on('error', () => {});
2828
+ function setupWatchers() {
2829
+ // Clean up previous watchers/timers if re-entering
2830
+ try { if (watcher) watcher.close(); } catch {}
2831
+ try { if (channelWatchers.length) channelWatchers.forEach(w => { try { w.close(); } catch {} }); } catch {}
2832
+ if (fallbackInterval) clearInterval(fallbackInterval);
2833
+ clearTimeout(timer);
2834
+ clearTimeout(heartbeatTimer);
2835
+ channelWatchers = [];
2918
2836
 
2919
- // Also watch channel files
2920
- const myChannels = getAgentChannels(registeredName);
2921
- for (const ch of myChannels) {
2922
- if (ch === 'general') continue;
2923
- const chFile = getChannelMessagesFile(ch);
2924
- if (fs.existsSync(chFile)) {
2925
- try {
2926
- const chWatcher = fs.watch(chFile, () => {
2927
- const batch = collectBatch();
2928
- if (batch.length > 0) done(batch);
2929
- });
2930
- chWatcher.on('error', () => {});
2931
- channelWatchers.push(chWatcher);
2932
- } catch (e) { log.debug("channel watcher setup failed:", e.message); }
2837
+ try {
2838
+ const msgFile = getMessagesFile(currentBranch);
2839
+ watcher = fs.watch(msgFile, () => {
2840
+ const batch = collectBatch();
2841
+ if (batch.length > 0) done(batch);
2842
+ });
2843
+ watcher.on('error', () => {});
2844
+
2845
+ const myChannels = getAgentChannels(registeredName);
2846
+ for (const ch of myChannels) {
2847
+ if (ch === 'general') continue;
2848
+ const chFile = getChannelMessagesFile(ch);
2849
+ if (fs.existsSync(chFile)) {
2850
+ try {
2851
+ const chWatcher = fs.watch(chFile, () => {
2852
+ const batch = collectBatch();
2853
+ if (batch.length > 0) done(batch);
2854
+ });
2855
+ chWatcher.on('error', () => {});
2856
+ channelWatchers.push(chWatcher);
2857
+ } catch (e) { log.debug("channel watcher setup failed:", e.message); }
2858
+ }
2933
2859
  }
2860
+ } catch {
2861
+ let pollCount = 0;
2862
+ fallbackInterval = setInterval(() => {
2863
+ const batch = collectBatch();
2864
+ if (batch.length > 0) {
2865
+ clearInterval(fallbackInterval);
2866
+ done(batch);
2867
+ }
2868
+ pollCount++;
2869
+ if (pollCount === 10) {
2870
+ clearInterval(fallbackInterval);
2871
+ fallbackInterval = setInterval(() => {
2872
+ const batch = collectBatch();
2873
+ if (batch.length > 0) { clearInterval(fallbackInterval); done(batch); }
2874
+ }, 2000);
2875
+ }
2876
+ }, 500);
2934
2877
  }
2935
- } catch {
2936
- // fs.watch not available — fall back to adaptive polling
2937
- let pollCount = 0;
2938
- fallbackInterval = setInterval(() => {
2939
- const batch = collectBatch();
2940
- if (batch.length > 0) {
2941
- clearInterval(fallbackInterval);
2942
- done(batch);
2943
- }
2944
- pollCount++;
2945
- // Adaptive: slow down after initial fast checks
2946
- if (pollCount === 10) {
2947
- clearInterval(fallbackInterval);
2948
- fallbackInterval = setInterval(() => {
2949
- const batch = collectBatch();
2950
- if (batch.length > 0) { clearInterval(fallbackInterval); done(batch); }
2951
- }, 2000); // slow poll every 2s
2952
- }
2953
- }, 500); // fast poll first 5s
2954
- }
2955
2878
 
2956
- // Heartbeat every 15s while waiting — prevents dashboard from showing agent as dead
2957
- const heartbeatTimer = setInterval(() => {
2958
- touchHeartbeat(registeredName);
2959
- }, 15000);
2879
+ heartbeatTimer = setInterval(() => {
2880
+ touchHeartbeat(registeredName);
2881
+ }, 15000);
2882
+
2883
+ const effectiveTimeout = autonomousTimeout
2884
+ ? Math.min(autonomousTimeout, MAX_LISTEN_MS)
2885
+ : MAX_LISTEN_MS;
2960
2886
 
2961
- // Autonomous mode: shorter timeout
2962
- const effectiveTimeout = autonomousTimeout
2963
- ? Math.min(autonomousTimeout, MAX_LISTEN_MS)
2964
- : MAX_LISTEN_MS;
2887
+ timer = setTimeout(() => done([]), effectiveTimeout);
2888
+ }
2965
2889
 
2966
- // Timeout: don't block forever
2967
- const timer = setTimeout(() => done([]), effectiveTimeout);
2890
+ setupWatchers();
2968
2891
  });
2969
2892
  }
2970
2893
 
@@ -3142,10 +3065,15 @@ function buildListenGroupResponse(batch, consumed, agentName, listenStart) {
3142
3065
  }
3143
3066
  }
3144
3067
 
3145
- result.next_action = isAutonomousMode()
3146
- ? 'Process these messages, then call get_work() to continue the proactive work loop. Do NOT call listen_group() — use get_work() instead.'
3147
- : 'After processing these messages and sending your response, call listen_group() again immediately. Never stop listening.';
3148
- result.coordinator_mode = getConfig().coordinator_mode || 'responsive';
3068
+ if (isAutonomousMode()) {
3069
+ result.next_action = 'Process these messages, then call get_work().';
3070
+ } else if (result.should_respond === false) {
3071
+ result.next_action = 'Read these messages but do NOT respond. Call listen() to wait for your turn.';
3072
+ } else if (result.instructions && result.instructions.includes('MANAGER')) {
3073
+ result.next_action = 'You are the manager. Decide who speaks next, then call listen().';
3074
+ } else {
3075
+ result.next_action = 'Process these messages. Reply via send_message(), then call listen() again.';
3076
+ }
3149
3077
 
3150
3078
  // Task reminder: remind agent of their outstanding tasks
3151
3079
  try {
@@ -4203,6 +4131,7 @@ async function toolGetWork(params = {}) {
4203
4131
  if (myStep) {
4204
4132
  const result = {
4205
4133
  type: 'workflow_step', priority: 'assigned', step: myStep,
4134
+ next_action: 'Do this work now. When done, call verify_and_advance().',
4206
4135
  instruction: `You have assigned work: "${myStep.description}" (Workflow: "${myStep.workflow_name}"). Do this NOW. When done, call verify_and_advance().`
4207
4136
  };
4208
4137
  // Attach relevant KB skills for this task
@@ -4228,6 +4157,7 @@ async function toolGetWork(params = {}) {
4228
4157
  return {
4229
4158
  type: 'messages', priority: 'respond',
4230
4159
  messages: pending.slice(0, 10), total: pending.length,
4160
+ next_action: 'Process these messages, then call get_work() again.',
4231
4161
  instruction: 'Process these messages first, then call get_work() again.'
4232
4162
  };
4233
4163
  }
@@ -4252,6 +4182,7 @@ async function toolGetWork(params = {}) {
4252
4182
  if (claimed) {
4253
4183
  const claimResult = {
4254
4184
  type: 'claimed_task', priority: 'self_assigned', task: best,
4185
+ next_action: 'Start working on this task now. Call verify_and_advance() when done.',
4255
4186
  instruction: `No one was working on "${best.title}". I've assigned it to you. Start working on it now.`
4256
4187
  };
4257
4188
  const taskSkills = searchKBForTask(best.title + ' ' + (best.description || ''));
@@ -4269,6 +4200,7 @@ async function toolGetWork(params = {}) {
4269
4200
  if (helpReqs.length > 0) {
4270
4201
  return {
4271
4202
  type: 'help_teammate', priority: 'assist', request: helpReqs[0],
4203
+ next_action: `Help ${helpReqs[0].from || 'your teammate'}, then call get_work() again.`,
4272
4204
  instruction: `${helpReqs[0].from || 'A teammate'} needs help: "${helpReqs[0].content.substring(0, 200)}". Assist them.`
4273
4205
  };
4274
4206
  }
@@ -4278,6 +4210,7 @@ async function toolGetWork(params = {}) {
4278
4210
  if (reviews.length > 0) {
4279
4211
  return {
4280
4212
  type: 'review', priority: 'review', review: reviews[0],
4213
+ next_action: 'Review this work, then call submit_review().',
4281
4214
  instruction: `Review request from ${reviews[0].requested_by}: "${reviews[0].file}". Review their work and submit_review().`
4282
4215
  };
4283
4216
  }
@@ -4287,6 +4220,7 @@ async function toolGetWork(params = {}) {
4287
4220
  if (blocked.length > 0) {
4288
4221
  return {
4289
4222
  type: 'unblock', priority: 'unblock', task: blocked[0],
4223
+ next_action: 'Try to unblock this task, then call get_work() again.',
4290
4224
  instruction: `"${blocked[0].title}" is blocked. See if you can help unblock it.`
4291
4225
  };
4292
4226
  }
@@ -4310,6 +4244,7 @@ async function toolGetWork(params = {}) {
4310
4244
  return {
4311
4245
  type: 'stolen_task', priority: 'work_steal', task: stealable.task,
4312
4246
  from_agent: stealable.from_agent,
4247
+ next_action: 'Start working on this task now. Call verify_and_advance() when done.',
4313
4248
  instruction: stealable.message + ' Start working on it now.',
4314
4249
  };
4315
4250
  }
@@ -4322,6 +4257,7 @@ async function toolGetWork(params = {}) {
4322
4257
  return {
4323
4258
  type: 'messages', priority: 'respond',
4324
4259
  messages: newMsgs.slice(0, 10), total: newMsgs.length,
4260
+ next_action: 'Process these messages, then call get_work() again.',
4325
4261
  instruction: 'New messages arrived. Process them, then call get_work() again.'
4326
4262
  };
4327
4263
  }
@@ -4331,6 +4267,7 @@ async function toolGetWork(params = {}) {
4331
4267
  if (upcoming) {
4332
4268
  return {
4333
4269
  type: 'prep_work', priority: 'proactive', step: upcoming,
4270
+ next_action: 'Prepare for this upcoming step, then call get_work() again.',
4334
4271
  instruction: `Your next workflow step "${upcoming.description}" is coming up (Workflow: "${upcoming.workflow_name}"). Prepare for it: read relevant files, understand the dependencies, plan your approach.`
4335
4272
  };
4336
4273
  }
@@ -4340,9 +4277,10 @@ async function toolGetWork(params = {}) {
4340
4277
  touchActivity();
4341
4278
  const idleResult = {
4342
4279
  type: 'idle',
4280
+ next_action: isManagedMode() ? 'Call listen() to wait for work.' : 'Call get_work() again in 30 seconds.',
4343
4281
  instruction: isManagedMode()
4344
4282
  ? 'No work available right now. Call listen() to wait for the manager to assign work or give you the floor.'
4345
- : 'No work available right now. Call get_work() again in 30 seconds. Do NOT call listen_group() — use get_work() to stay in the proactive loop.'
4283
+ : 'No work available right now. Call get_work() again in 30 seconds.'
4346
4284
  };
4347
4285
  // Item 4: warn demoted agents
4348
4286
  const agentRep = getReputation();
@@ -4399,7 +4337,7 @@ async function toolVerifyAndAdvance(params) {
4399
4337
  const report = generateCompletionReport(wf);
4400
4338
  const retrospective = logRetrospective(wf.id); // Item 9: analyze retry patterns
4401
4339
  touchActivity();
4402
- return { status: flagged ? 'workflow_complete_flagged' : 'workflow_complete', workflow_id: wf.id, report, retrospective, message: `Workflow "${wf.name}" finished! Call get_work() for your next assignment.` };
4340
+ return { status: flagged ? 'workflow_complete_flagged' : 'workflow_complete', workflow_id: wf.id, next_action: 'Call get_work() for your next assignment.', report, retrospective };
4403
4341
  }
4404
4342
 
4405
4343
  const agents = getAgents();
@@ -4421,7 +4359,7 @@ async function toolVerifyAndAdvance(params) {
4421
4359
  status: flagged ? 'advanced_with_flag' : 'advanced', workflow_id: wf.id,
4422
4360
  completed_step: currentStep.id,
4423
4361
  next_steps: nextSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })),
4424
- message: flagged ? 'Advanced but flagged for later review. Call get_work().' : 'Step complete. Next step(s) kicked off. Call get_work() for your next assignment.'
4362
+ next_action: 'Call get_work() for your next assignment.',
4425
4363
  };
4426
4364
  }
4427
4365
 
@@ -4452,7 +4390,7 @@ async function toolVerifyAndAdvance(params) {
4452
4390
  touchActivity();
4453
4391
  return {
4454
4392
  status: 'needs_help', workflow_id: wf.id,
4455
- message: 'Low confidence. Help request broadcast to team. Call get_work() you may get a review assignment or other work while waiting.'
4393
+ next_action: 'Call get_work() for other work while waiting for help.',
4456
4394
  };
4457
4395
  }
4458
4396
 
@@ -7679,13 +7617,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7679
7617
  } catch { return 'your coordinator'; }
7680
7618
  })();
7681
7619
  return {
7682
- content: [{ type: 'text', text: JSON.stringify({
7683
- error: `BLOCKED: You must call listen() before using other tools. You have made ${consecutiveNonListenCalls} tool calls without listening. Call listen() now.`,
7684
- blocked_tool: name,
7685
- calls_without_listen: consecutiveNonListenCalls,
7686
- fix: `1. Call send_message(to='${coordinator}', content='BLOCKED: I made ${consecutiveNonListenCalls} tool calls without listen(). I was trying to call ${name}. Requesting instructions — should I proceed?') 2. Then call listen() immediately to unblock all tools.`,
7687
- _listen: 'After send_message(), call listen() immediately. It will reset the counter and unblock all tools.',
7688
- }, null, 2) }],
7620
+ content: [{ type: 'text', text:
7621
+ `BLOCKED ${name}() was not executed. You made ${consecutiveNonListenCalls} tool calls without calling listen(). ` +
7622
+ `Do these two steps IN ORDER:\n` +
7623
+ `1. send_message(to="${coordinator}", content="I was blocked after ${consecutiveNonListenCalls} calls without listen(). I need to call ${name}. Should I proceed?")\n` +
7624
+ `2. listen()\n` +
7625
+ `Do NOT skip step 1. Do NOT call any other tool. Start with send_message now.`
7626
+ }],
7689
7627
  isError: true,
7690
7628
  };
7691
7629
  }
@@ -7900,48 +7838,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7900
7838
  };
7901
7839
  }
7902
7840
 
7903
- // Global hook: on non-listen tools, check for pending messages and nudge with escalating urgency
7904
- // Enhanced nudge: includes sender names, addressed count, and message preview
7905
- const listenTools = ['listen', 'wait_for_reply'];
7906
- if (registeredName && !listenTools.includes(name) && (isGroupMode() || isManagedMode())) {
7907
- try {
7908
- const pending = getUnconsumedMessages(registeredName);
7909
- if (pending.length > 0 && !result.you_have_messages) {
7910
- // Build rich nudge: WHO sent, WHETHER addressed, WHAT preview
7911
- const senders = {};
7912
- let addressedCount = 0;
7913
- for (const m of pending) {
7914
- senders[m.from] = (senders[m.from] || 0) + 1;
7915
- if (m.addressed_to && m.addressed_to.includes(registeredName)) addressedCount++;
7916
- }
7917
- const senderSummary = Object.entries(senders).map(([n, c]) => `${c} from ${n}`).join(', ');
7918
- const latest = pending[pending.length - 1];
7919
- const preview = latest.content.substring(0, 80).replace(/\n/g, ' ');
7920
-
7921
- result._pending_messages = pending.length;
7922
- result._senders = senders;
7923
- result._addressed_to_you = addressedCount;
7924
- result._preview = `${latest.from}: "${preview}..."`;
7925
-
7926
- // Escalate urgency based on oldest pending message age
7927
- const oldestAge = pending.reduce((max, m) => {
7928
- const age = Date.now() - new Date(m.timestamp).getTime();
7929
- return age > max ? age : max;
7930
- }, 0);
7931
- const ageSec = Math.round(oldestAge / 1000);
7932
- const addressedHint = addressedCount > 0 ? ` (${addressedCount} addressed to you)` : '';
7933
- if (ageSec > 120) {
7934
- result._nudge = `CRITICAL: ${pending.length} messages waiting ${Math.round(ageSec / 60)}+ min${addressedHint}: ${senderSummary}. Latest: "${preview}...". Call listen_group() NOW.`;
7935
- } else if (ageSec > 30) {
7936
- result._nudge = `URGENT: ${pending.length} messages waiting ${ageSec}s${addressedHint}: ${senderSummary}. Latest: "${preview}...". Call listen_group() soon.`;
7937
- } else {
7938
- result._nudge = `${pending.length} messages waiting${addressedHint}: ${senderSummary}. Latest: "${preview}...". Call listen_group().`;
7939
- }
7940
- }
7941
- } catch (e) { log.debug("nudge detection failed:", e.message); }
7942
- }
7943
-
7944
7841
  // Global hook: reputation tracking
7842
+ const listenTools = ['listen', 'wait_for_reply'];
7945
7843
  if (registeredName && result.success) {
7946
7844
  try {
7947
7845
  const repMap = {
@@ -7952,9 +7850,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7952
7850
  'submit_review': 'review_submit',
7953
7851
  };
7954
7852
  if (repMap[name]) trackReputation(registeredName, repMap[name]);
7955
- // Track task completion specifically
7956
7853
  if (name === 'update_task' && args?.status === 'done') {
7957
- // Calculate task completion time
7958
7854
  const tasks = getTasks();
7959
7855
  const doneTask = tasks.find(t => t.id === args.task_id);
7960
7856
  const taskTimeSec = doneTask ? Math.round((Date.now() - new Date(doneTask.created_at).getTime()) / 1000) : 0;
@@ -7963,48 +7859,61 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7963
7859
  } catch (e) { log.debug('reputation tracking failed:', e.message); }
7964
7860
  }
7965
7861
 
7966
- // Global hook: auto-compress conversation periodically
7967
7862
  if (name === 'send_message' || name === 'broadcast') {
7968
7863
  try { autoCompress(); } catch (e) { log.debug('auto-compress failed:', e.message); }
7969
7864
  }
7970
7865
 
7971
- // Coordinator mode hint: inject into every tool response for lead/manager/coordinator agents
7972
- if (registeredName && typeof result === 'object' && result !== null) {
7973
- try {
7974
- const prof = getProfiles()[registeredName];
7975
- const role = prof && prof.role ? prof.role.toLowerCase() : '';
7976
- if (role === 'lead' || role === 'manager' || role === 'coordinator') {
7977
- const coordMode = getConfig().coordinator_mode || 'responsive';
7978
- result.coordinator_mode = coordMode;
7979
- result.coordinator_hint = coordMode === 'responsive'
7980
- ? 'MODE: Stay with me — do NOT call listen(). Use consume_messages/workflow_status between human interactions.'
7981
- : 'MODE: Run autonomously — use listen() to wait for agent results.';
7982
- }
7983
- } catch (e) { log.debug('coordinator mode hint failed:', e.message); }
7984
- }
7985
-
7986
- // Unread message hint: check if agent has pending messages on every tool call
7987
- // This ensures agents see messages even when they forget to call listen()
7866
+ // Unified next_action: the ONE field that tells the agent what to do next.
7867
+ // Tool-specific next_action (set by the handler) takes priority.
7868
+ // Middleware fills in a default if the tool didn't set one.
7988
7869
  if (registeredName && typeof result === 'object' && result !== null && !listenTools.includes(name)) {
7989
- try {
7990
- const unread = getUnconsumedMessages(registeredName);
7991
- if (unread.length > 0) {
7992
- const latest = unread[unread.length - 1];
7993
- result.unread_messages = unread.length;
7994
- result.unread_preview = `${latest.from}: "${latest.content.substring(0, 100).replace(/\n/g, ' ')}"`;
7995
- result.unread_action = `You have ${unread.length} unread message(s). Call listen() to receive them.`;
7996
- }
7997
- } catch (e) { log.debug('unread message hint failed:', e.message); }
7998
- }
7870
+ const isResponsiveCoordinator = (() => {
7871
+ try {
7872
+ const prof = getProfiles()[registeredName];
7873
+ const role = prof && prof.role ? prof.role.toLowerCase() : '';
7874
+ if (role === 'lead' || role === 'manager' || role === 'coordinator') {
7875
+ return (getConfig().coordinator_mode || 'responsive') === 'responsive';
7876
+ }
7877
+ } catch {}
7878
+ return false;
7879
+ })();
7999
7880
 
8000
- // Persistent listen() reminder — injected into EVERY non-listen tool response.
8001
- // Keeps the instruction in the agent's immediate context even after extended
8002
- // processing pushes earlier instructions out of working memory.
8003
- if (registeredName && typeof result === 'object' && result !== null && !listenTools.includes(name)) {
8004
- if (consecutiveNonListenCalls >= 3) {
8005
- result._listen = `WARNING: You have NOT called listen() in ${consecutiveNonListenCalls} tool calls. Tools will be BLOCKED at 5. Call listen() NOW.`;
7881
+ if (isResponsiveCoordinator) {
7882
+ // 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
+ }
7895
+ }
8006
7896
  } else {
8007
- result._listen = 'After processing this result, call listen() to receive messages. Do NOT skip this.';
7897
+ if (!result.next_action) {
7898
+ try {
7899
+ const pending = getUnconsumedMessages(registeredName);
7900
+ if (pending.length > 0) {
7901
+ const oldest = pending[0];
7902
+ const ageSec = Math.floor((Date.now() - new Date(oldest.timestamp).getTime()) / 1000);
7903
+ if (ageSec > 120) {
7904
+ result.next_action = `URGENT: ${pending.length} message(s) waiting ${Math.round(ageSec / 60)}+ min. Call listen() now.`;
7905
+ } else {
7906
+ result.next_action = `${pending.length} unread message(s). Call listen().`;
7907
+ }
7908
+ } else {
7909
+ result.next_action = 'Call listen() to receive messages.';
7910
+ }
7911
+ } catch {}
7912
+ }
7913
+
7914
+ if (consecutiveNonListenCalls >= 3) {
7915
+ result.next_action = `WARNING: ${consecutiveNonListenCalls} calls without listen(). Tools BLOCKED at 5. Call listen() NOW.`;
7916
+ }
8008
7917
  }
8009
7918
  }
8010
7919
 
@@ -117,7 +117,7 @@ module.exports = function (ctx) {
117
117
 
118
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);
119
119
  touchActivity();
120
- return { success: true, review_id: review.id, file: review.file, message: 'Review requested. Team has been notified.' };
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
123
  function toolSubmitReview(reviewId, status, feedback) {
@@ -191,7 +191,10 @@ module.exports = function (ctx) {
191
191
  writeJsonFile(REVIEWS_FILE, reviews);
192
192
  touchActivity();
193
193
 
194
- const result = { success: true, review_id: reviewId, status: review.status, message: `Review submitted: ${review.status}` };
194
+ const reviewNextAction = review.status === 'approved'
195
+ ? 'Call listen() to continue.'
196
+ : 'Call listen() — the author will fix and resubmit.';
197
+ const result = { success: true, review_id: reviewId, status: review.status, next_action: reviewNextAction };
195
198
  if (review.review_round) result.review_round = review.review_round;
196
199
  if (review.auto_approved) result.auto_approved = true;
197
200
  return result;
@@ -132,7 +132,7 @@ module.exports = function (ctx) {
132
132
  total_messages: compressed.segments.reduce((s, seg) => s + seg.message_count, 0) + recent.length,
133
133
  compressed_count: compressed.segments.reduce((s, seg) => s + seg.message_count, 0),
134
134
  recent_count: recent.length,
135
- hint: 'Compressed segments summarize older messages. Recent messages are shown verbatim.',
135
+ next_action: 'Call listen() to receive messages.',
136
136
  };
137
137
  }
138
138
 
@@ -223,9 +223,9 @@ module.exports = function (ctx) {
223
223
  progress,
224
224
  your_tasks: myActiveTasks.map(t => ({ id: t.id, title: t.title, status: t.status })),
225
225
  your_completed: myCompletedCount,
226
- hint: myActiveTasks.length > 0
227
- ? `You have ${myActiveTasks.length} active task(s). Continue working.`
228
- : 'You are now briefed. Check active tasks and start contributing.',
226
+ next_action: myActiveTasks.length > 0
227
+ ? `You have ${myActiveTasks.length} active task(s). Continue working, then call listen().`
228
+ : 'Call listen() to receive messages and start working.',
229
229
  };
230
230
  }
231
231
 
@@ -56,7 +56,7 @@ module.exports = function (ctx) {
56
56
  result.preview = `${latest.from}: "${latest.content.substring(0, 80).replace(/\n/g, ' ')}..."`;
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
- 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.';
59
+ result.next_action = 'Call listen() to receive and process these messages.';
60
60
  }
61
61
 
62
62
  return result;
package/tools/safety.js CHANGED
@@ -34,7 +34,7 @@ module.exports = function (ctx) {
34
34
  locks[normalized] = { agent: state.registeredName, since: new Date().toISOString() };
35
35
  writeJsonFile(LOCKS_FILE, locks);
36
36
  touchActivity();
37
- return { success: true, file: normalized, message: `File locked. Other agents cannot edit "${normalized}" until you call unlock_file().` };
37
+ return { success: true, file: normalized, next_action: 'Edit the file, then call unlock_file() when done.' };
38
38
  }
39
39
 
40
40
  function toolUnlockFile(filePath) {
package/tools/tasks.js CHANGED
@@ -81,7 +81,7 @@ module.exports = function (ctx) {
81
81
  saveTasks(tasks);
82
82
  touchActivity();
83
83
 
84
- const result = { success: true, task_id: task.id, assignee: task.assignee };
84
+ const result = { success: true, task_id: task.id, assignee: task.assignee, next_action: 'Call listen() to receive updates.' };
85
85
  if (taskChannel) result.channel = taskChannel;
86
86
  return result;
87
87
  }
@@ -160,6 +160,7 @@ module.exports = function (ctx) {
160
160
  task_id: task.id,
161
161
  status: 'in_review',
162
162
  review_id: reviewId,
163
+ next_action: 'Call listen() to wait for the reviewer to approve.',
163
164
  message: `Cannot mark done — a reviewer is online and no approval exists. Review ${reviewId} auto-created. Wait for approval, then try again.`,
164
165
  };
165
166
  }
@@ -291,7 +292,11 @@ module.exports = function (ctx) {
291
292
  for (const n of notifications) { helpers.sendSystemMessage(n.agent, n.message); }
292
293
  } catch (e) { /* hooks not available */ }
293
294
 
294
- return { success: true, task_id: task.id, status: task.status, title: task.title };
295
+ const nextAction = status === 'done' ? 'Send a summary of what you did via send_message(), then call listen().'
296
+ : status === 'in_progress' ? `Do the work on "${task.title}", then call update_task("${task.id}", "done") when finished.`
297
+ : status === 'blocked' ? 'Send a message explaining the blocker, then call listen().'
298
+ : 'Call listen() to receive updates.';
299
+ return { success: true, task_id: task.id, status: task.status, title: task.title, next_action: nextAction };
295
300
  }
296
301
 
297
302
  // --- List Tasks ---
@@ -102,7 +102,7 @@ module.exports = function (ctx) {
102
102
  autonomous,
103
103
  parallel,
104
104
  started_steps: startedSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })),
105
- message: autonomous ? 'Autonomous workflow created. All agents should call get_work() to enter the proactive work loop.' : undefined,
105
+ next_action: autonomous ? 'Call get_work() for your assignment.' : 'Call listen() to receive updates.',
106
106
  };
107
107
  }
108
108
 
@@ -179,6 +179,7 @@ module.exports = function (ctx) {
179
179
  next_steps: nextSteps.length > 0 ? nextSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })) : null,
180
180
  progress: `${doneCount}/${wf.steps.length} (${pct}%)`,
181
181
  workflow_status: wf.status,
182
+ next_action: wf.autonomous ? 'Call get_work() for your next assignment.' : 'Call listen() to receive the next step.',
182
183
  };
183
184
  }
184
185