agenthub-multiagent-mcp 1.76.1 → 1.78.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.
Files changed (67) hide show
  1. package/dist/channel.js +26 -4
  2. package/dist/channel.js.map +1 -1
  3. package/dist/client.d.ts +37 -2
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +18 -0
  6. package/dist/client.js.map +1 -1
  7. package/dist/daemon.js +42 -1
  8. package/dist/daemon.js.map +1 -1
  9. package/dist/daemonPolicy.d.ts +23 -0
  10. package/dist/daemonPolicy.d.ts.map +1 -1
  11. package/dist/daemonPolicy.js +22 -0
  12. package/dist/daemonPolicy.js.map +1 -1
  13. package/dist/daemonPolicy.test.js +54 -1
  14. package/dist/daemonPolicy.test.js.map +1 -1
  15. package/dist/inboxHookCli.d.ts +1 -1
  16. package/dist/inboxHookCli.d.ts.map +1 -1
  17. package/dist/inboxHookCli.js +16 -3
  18. package/dist/inboxHookCli.js.map +1 -1
  19. package/dist/inboxHookCli.test.js +23 -0
  20. package/dist/inboxHookCli.test.js.map +1 -1
  21. package/dist/lib/brainstormInvites.d.ts +68 -0
  22. package/dist/lib/brainstormInvites.d.ts.map +1 -0
  23. package/dist/lib/brainstormInvites.js +143 -0
  24. package/dist/lib/brainstormInvites.js.map +1 -0
  25. package/dist/lib/brainstormInvites.test.d.ts +8 -0
  26. package/dist/lib/brainstormInvites.test.d.ts.map +1 -0
  27. package/dist/lib/brainstormInvites.test.js +102 -0
  28. package/dist/lib/brainstormInvites.test.js.map +1 -0
  29. package/dist/lib/pending-items.d.ts +7 -0
  30. package/dist/lib/pending-items.d.ts.map +1 -1
  31. package/dist/lib/pending-items.js +3 -2
  32. package/dist/lib/pending-items.js.map +1 -1
  33. package/dist/session-capture.sh +21 -0
  34. package/dist/sessionCaptureScript.test.js +6 -5
  35. package/dist/sessionCaptureScript.test.js.map +1 -1
  36. package/dist/skillPackInstaller.d.ts +1 -1
  37. package/dist/skillPackInstaller.d.ts.map +1 -1
  38. package/dist/skillPackInstaller.js +56 -2
  39. package/dist/skillPackInstaller.js.map +1 -1
  40. package/dist/skillPackInstaller.test.js +113 -0
  41. package/dist/skillPackInstaller.test.js.map +1 -1
  42. package/dist/tools/brainLessons.test.d.ts +6 -0
  43. package/dist/tools/brainLessons.test.d.ts.map +1 -0
  44. package/dist/tools/brainLessons.test.js +61 -0
  45. package/dist/tools/brainLessons.test.js.map +1 -0
  46. package/dist/tools/brainstormInvites.test.d.ts +7 -0
  47. package/dist/tools/brainstormInvites.test.d.ts.map +1 -0
  48. package/dist/tools/brainstormInvites.test.js +139 -0
  49. package/dist/tools/brainstormInvites.test.js.map +1 -0
  50. package/dist/tools/index.d.ts.map +1 -1
  51. package/dist/tools/index.js +239 -12
  52. package/dist/tools/index.js.map +1 -1
  53. package/dist/tools/tools.test.js +125 -3
  54. package/dist/tools/tools.test.js.map +1 -1
  55. package/dist/tools/whatsapp.d.ts +73 -0
  56. package/dist/tools/whatsapp.d.ts.map +1 -0
  57. package/dist/tools/whatsapp.js +278 -0
  58. package/dist/tools/whatsapp.js.map +1 -0
  59. package/dist/tools/whatsapp.test.d.ts +2 -0
  60. package/dist/tools/whatsapp.test.d.ts.map +1 -0
  61. package/dist/tools/whatsapp.test.js +139 -0
  62. package/dist/tools/whatsapp.test.js.map +1 -0
  63. package/package.json +1 -1
  64. package/skills/catalog.json +106 -104
  65. package/skills/manifest.json +69 -67
  66. package/skills/skills/inbox/SKILL.md +25 -3
  67. package/skills/skills/multiagent-brainstorm/SKILL.md +26 -6
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * MCP tool definitions and handlers
3
3
  */
4
+ import { AgentHubApiError, } from "../client.js";
4
5
  import { resolveAgentIdentity, missingStateGuidance, credentialIsDead, } from "../agentIdentity.js";
5
6
  import { boundInbox, resolveInboxLimit, CHECK_INBOX_DEFAULT_LIMIT, CHECK_INBOX_MAX_LIMIT, } from "./inboxBounds.js";
6
7
  import * as state from "../state.js";
@@ -16,9 +17,11 @@ import { handleEmployeeTrainingToolCall, isEmployeeTrainingTool, registerEmploye
16
17
  import { registerSessionControlTools, handleSessionControlToolCall, isSessionControlTool, } from "./sessionControl.js";
17
18
  import { registerGoshtiReadTools, handleGoshtiReadToolCall, isGoshtiReadTool, } from "./goshtiRead.js";
18
19
  import { registerRunTools, handleRunToolCall, isRunTool } from "./runs.js";
20
+ import { isWhatsAppTool, runWhatsAppTool, whatsappToolDefinitions } from "./whatsapp.js";
19
21
  import { installPackFromArchive } from "../skillPackInstaller.js";
20
22
  import { getServerTime, useTimeInjection } from "../lib/server-time.js";
21
23
  import { getPendingItems } from "../lib/pending-items.js";
24
+ import { brainstormAutoAcceptEnabled, inviteGuidance, } from "../lib/brainstormInvites.js";
22
25
  import { ENG_IC_CHARTER } from "../employee.js";
23
26
  import { promises as fsp } from "node:fs";
24
27
  import * as os from "node:os";
@@ -83,6 +86,19 @@ function backgroundSync(client, agentId, workingDir) {
83
86
  }
84
87
  })();
85
88
  }
89
+ /**
90
+ * The invite fields every pending-items block carries (check_inbox and the
91
+ * tool wrapper). The guidance rides along only when there is an invite, so an
92
+ * empty inbox costs no extra text.
93
+ */
94
+ function inviteFields(invites) {
95
+ const list = invites ?? [];
96
+ return {
97
+ pending_brainstorm_invites: list,
98
+ pending_brainstorm_invites_count: list.length,
99
+ ...(list.length > 0 ? { brainstorm_invite_guidance: inviteGuidance(brainstormAutoAcceptEnabled()) } : {}),
100
+ };
101
+ }
86
102
  /**
87
103
  * Wraps a tool response with pending messages and tasks
88
104
  * This enables automatic delivery without extra API calls
@@ -160,6 +176,7 @@ async function wrapWithPendingItems(client, agentId, result, context) {
160
176
  pending_messages_count: messages.length,
161
177
  pending_tasks: tasks,
162
178
  pending_tasks_count: tasks.length,
179
+ ...inviteFields(cachedItems.invites),
163
180
  urgent_action: urgentAction,
164
181
  };
165
182
  if (useTimeInjection()) {
@@ -976,7 +993,7 @@ export function registerTools() {
976
993
  },
977
994
  {
978
995
  name: "skill_pack_install",
979
- description: "Install an opt-in skill pack into ~/.claude/skills/. Hash-safe (never overwrites files you've edited unless force=true). Destructive packs (e.g. cybersecurity, which ships offensive PoC scripts) require the skill-pack:<name> capability attached to this agent. Returns a per-file install report.",
996
+ description: "Install an opt-in skill pack into ~/.claude/skills/. Hash-safe (never overwrites files you've edited unless force=true). On upgrade, files the new version dropped are removed if unedited (removed) and kept if you edited them (kept_modified). Destructive packs (e.g. cybersecurity, which ships offensive PoC scripts) require the skill-pack:<name> capability attached to this agent. Returns a per-file install report.",
980
997
  inputSchema: {
981
998
  type: "object",
982
999
  properties: {
@@ -989,6 +1006,52 @@ export function registerTools() {
989
1006
  required: ["pack"],
990
1007
  },
991
1008
  },
1009
+ // add-product-lifecycle-pack §2.1–2.2 — product-lifecycle hard gates.
1010
+ // 503 lifecycle_gates_disabled while AGENTHUB_LIFECYCLE_GATES_ENABLED is off.
1011
+ {
1012
+ name: "lifecycle_gate_request",
1013
+ description: "Open a product-lifecycle hard gate (stage 3, 6, 9 or dvt) on the stage's gate task. Creates a blocking lifecycle_gate review that only an org owner or admin can sign, on the dashboard (/reviews) — not an agent, an API key or a chat button. Returns the review id and status; poll lifecycle_gate_list until it is decided. A second pending gate on the same task is refused (gate_pending). For a re-gate after a stale or revised one, pass parent_review_id.",
1014
+ inputSchema: {
1015
+ type: "object",
1016
+ properties: {
1017
+ project_id: { type: "string", description: "Project id (defaults to the project this agent is bound to)" },
1018
+ task_id: { type: "string", description: "The stage's gate task id (must be in project_id)" },
1019
+ stage: { type: "string", enum: ["3", "6", "9", "dvt"], description: "Hard-gate stage" },
1020
+ title: { type: "string", description: "Gate title" },
1021
+ content: { type: "string", description: "The gate pack, as markdown" },
1022
+ evidence_hash: { type: "string", description: "sha256 of the evidence, 64 lowercase hex characters" },
1023
+ items: {
1024
+ type: "array",
1025
+ description: "The stage's tasks and their verdicts",
1026
+ items: {
1027
+ type: "object",
1028
+ properties: {
1029
+ task_id: { type: "string" },
1030
+ status: { type: "string" },
1031
+ verdict: { type: "string" },
1032
+ refs: { type: "array", items: { type: "string" }, description: "Evidence refs (PRs, docs, runs)" },
1033
+ },
1034
+ required: ["task_id", "status", "verdict"],
1035
+ },
1036
+ },
1037
+ parent_review_id: { type: "string", description: "Earlier lifecycle_gate on the same task this one supersedes" },
1038
+ },
1039
+ required: ["task_id", "stage", "title", "content", "evidence_hash", "items"],
1040
+ },
1041
+ },
1042
+ {
1043
+ name: "lifecycle_gate_list",
1044
+ description: "List a project's product-lifecycle hard gates, newest first, with their parsed metadata (stage, evidence hash, items) and status (pending/approved/denied/…). Filter by stage, task_id or status.",
1045
+ inputSchema: {
1046
+ type: "object",
1047
+ properties: {
1048
+ project_id: { type: "string", description: "Project id (defaults to the project this agent is bound to)" },
1049
+ stage: { type: "string", enum: ["3", "6", "9", "dvt"], description: "Only this stage" },
1050
+ task_id: { type: "string", description: "Only gates on this task" },
1051
+ status: { type: "string", description: "Only this review status, e.g. pending" },
1052
+ },
1053
+ },
1054
+ },
992
1055
  {
993
1056
  name: "api_search",
994
1057
  description: "Search the API knowledge graph — endpoints (REST/MCP/GraphQL/gRPC) extracted from indexed repos across the org. Use this BEFORE building a new endpoint to check if it already exists or to match a sibling project's contract. Org-wide read.",
@@ -1140,6 +1203,21 @@ export function registerTools() {
1140
1203
  required: ["channel"],
1141
1204
  },
1142
1205
  },
1206
+ {
1207
+ name: "room_history",
1208
+ description: "Read a room's (channel's) earlier posts, oldest first, paged — e.g. after joining late. " +
1209
+ "Pass next_since from the previous page as `since` to read on. Reading marks nothing read. " +
1210
+ "Rooms belong to your organization; another org's room is 'not found'.",
1211
+ inputSchema: {
1212
+ type: "object",
1213
+ properties: {
1214
+ room: { type: "string", description: "Room (channel) name" },
1215
+ since: { type: "number", description: "Return posts after this message seq (default 0: from the start)" },
1216
+ limit: { type: "number", description: "Posts per page (default 50, at most 200)" },
1217
+ },
1218
+ required: ["room"],
1219
+ },
1220
+ },
1143
1221
  {
1144
1222
  name: "leave_channel",
1145
1223
  description: "Unsubscribe from a channel",
@@ -1778,13 +1856,18 @@ export function registerTools() {
1778
1856
  required: ["topic"],
1779
1857
  },
1780
1858
  },
1859
+ {
1860
+ name: "brainstorm_invites",
1861
+ description: "List your pending brainstorm invites (session id, display number, topic, initiator). An invite is not chat: bring it to your human before brainstorm_accept unless they asked for that brainstorm or AGENTHUB_BRAINSTORM_AUTO_ACCEPT=true. The same list rides on check_inbox and every tool response as pending_brainstorm_invites. (requires agent_register first)",
1862
+ inputSchema: { type: "object", properties: {} },
1863
+ },
1781
1864
  {
1782
1865
  name: "brainstorm_accept",
1783
- description: "Accept a brainstorm invite (transition the session to active). Idempotent — a redelivered invite can't double-join. You are the responder. (requires agent_register first)",
1866
+ description: "Accept a brainstorm invite (transition the session to active). Idempotent — a redelivered invite can't double-join. You are the responder. Unless your human asked for this brainstorm or AGENTHUB_BRAINSTORM_AUTO_ACCEPT=true, ask them first. (requires agent_register first)",
1784
1867
  inputSchema: {
1785
1868
  type: "object",
1786
1869
  properties: {
1787
- session_id: { type: "string", description: "The session id from the invite message." },
1870
+ session_id: { type: "string", description: "The session id, from pending_brainstorm_invites / brainstorm_invites (or a legacy invite message)." },
1788
1871
  },
1789
1872
  required: ["session_id"],
1790
1873
  },
@@ -2222,6 +2305,9 @@ export function registerTools() {
2222
2305
  ...registerEmployeeTrainingTools(),
2223
2306
  // Reading Goshti as the agent's owner (add-agent-goshti-read §3.1)
2224
2307
  ...registerGoshtiReadTools(),
2308
+ // Reading the owner's WhatsApp through the local reader on this PC
2309
+ // (add-whatsapp-personal-reader §3). Never touches the AgentHub server.
2310
+ ...whatsappToolDefinitions(),
2225
2311
  // Orchestrated runs for the dashboard Run view (add-live-animated-views §1.8)
2226
2312
  ...registerRunTools(),
2227
2313
  // OpenSpec Visualizer tools
@@ -2368,10 +2454,14 @@ export function registerTools() {
2368
2454
  query: { type: "string", description: "Search query" },
2369
2455
  category: {
2370
2456
  type: "string",
2371
- enum: ["decision", "convention", "architecture", "blocker", "alert", "insight"],
2457
+ enum: ["decision", "convention", "architecture", "blocker", "alert", "insight", "lesson"],
2372
2458
  description: "Filter by knowledge category",
2373
2459
  },
2374
2460
  project: { type: "string", description: "Filter by project" },
2461
+ tag: {
2462
+ type: "string",
2463
+ description: "Exact tag filter: matches a row only when one of its tags equals this string (e.g. `stage:6` does not match `stage:60`)",
2464
+ },
2375
2465
  limit: { type: "number", description: "Max results (default 20)" },
2376
2466
  },
2377
2467
  required: ["query"],
@@ -2379,20 +2469,38 @@ export function registerTools() {
2379
2469
  },
2380
2470
  {
2381
2471
  name: "brain_write_knowledge",
2382
- description: "Write an org knowledge fact (decision, convention, architecture insight, blocker, or alert) to the shared org brain. The result carries `check`: `created` for a new row (with its `id`), or `deduped` when an identical active fact already exists — then `canonical_id` names the existing row if it is your organization's, and nothing is named if the match belongs to another organization. A refusal (missing fact, unknown category) is an error.",
2472
+ description: "Write an org knowledge fact (decision, convention, architecture insight, blocker, or alert) to the shared org brain. The result carries `check`: `created` for a new row (with its `id`), or `deduped` when an identical active fact already exists — then `canonical_id` names the existing row. Dedup matches only your organization's own facts: identical text held by another organization is stored as your own new row. A refusal (missing fact, unknown category) is an error.",
2383
2473
  inputSchema: {
2384
2474
  type: "object",
2385
2475
  properties: {
2386
2476
  category: {
2387
2477
  type: "string",
2388
- enum: ["decision", "convention", "architecture", "blocker", "alert", "insight"],
2478
+ enum: ["decision", "convention", "architecture", "blocker", "alert", "insight", "lesson"],
2389
2479
  description: "Knowledge category",
2390
2480
  },
2391
- fact: { type: "string", description: "The fact or decision (concise)" },
2481
+ fact: { type: "string", description: "The fact or decision (concise). Required except for a lesson, whose fact defaults to `LESSON s<stage>: <rule>`." },
2392
2482
  detail: { type: "string", description: "Extended context or rationale" },
2393
2483
  project: { type: "string", description: "Project this applies to (optional)" },
2484
+ tags: {
2485
+ type: "array",
2486
+ items: { type: "string" },
2487
+ description: "Tags for the exact tag filter (e.g. [\"lesson\",\"stage:6\"]). A lesson always also gets lesson, stage:<n>, track:<t>, product:<p>.",
2488
+ },
2489
+ lesson: {
2490
+ type: "object",
2491
+ description: "Required for category=lesson (400 invalid_lesson otherwise). All six fields are non-empty strings.",
2492
+ properties: {
2493
+ stage: { type: "string", description: "\"0\"–\"10\", \"HW-POC\", \"HW-EVT\", \"HW-DVT\", \"HW-PVT\" or \"HW-CERT\"" },
2494
+ product: { type: "string" },
2495
+ track: { type: "string", enum: ["gov", "commercial", "hardware", "any"] },
2496
+ rule: { type: "string", description: "One imperative sentence" },
2497
+ evidence: { type: "string", description: "A verifiable reference" },
2498
+ gate_kind: { type: "string", enum: ["gate item", "template field", "stage instruction"] },
2499
+ },
2500
+ required: ["stage", "product", "track", "rule", "evidence", "gate_kind"],
2501
+ },
2394
2502
  },
2395
- required: ["category", "fact"],
2503
+ required: ["category"],
2396
2504
  },
2397
2505
  },
2398
2506
  {
@@ -3229,6 +3337,23 @@ function brainstormError(tool, result) {
3229
3337
  message: result.message || `${tool} failed: ${result.error} (HTTP ${result.status})`,
3230
3338
  };
3231
3339
  }
3340
+ // lifecycleGateProject resolves a lifecycle gate tool's project: the argument,
3341
+ // else the project this agent is bound to (add-product-lifecycle-pack §2.1).
3342
+ function lifecycleGateProject(args, context) {
3343
+ if (typeof args.project_id === "string" && args.project_id !== "")
3344
+ return args.project_id;
3345
+ return state.loadState(context.getWorkingDir())?.project_id ?? "";
3346
+ }
3347
+ // lifecycleGateError passes a server refusal through with its code and status,
3348
+ // and says in plain words what a 503 lifecycle_gates_disabled means.
3349
+ function lifecycleGateError(err) {
3350
+ if (!(err instanceof AgentHubApiError))
3351
+ throw err;
3352
+ const message = err.code === "lifecycle_gates_disabled"
3353
+ ? "Lifecycle gates are switched off on this AgentHub server (AGENTHUB_LIFECYCLE_GATES_ENABLED is not true). No gate can be opened or listed until an operator turns it on; existing gates stay readable and decidable on /reviews."
3354
+ : err.message;
3355
+ return { ok: false, error: err.code || "request_failed", status: err.status, message };
3356
+ }
3232
3357
  export async function handleToolCall(name, args, client, context) {
3233
3358
  const agentId = context.getCurrentAgentId();
3234
3359
  // Session steering + takeover (add-multiplayer-sessions §4.7, §5.9). Handled
@@ -3252,6 +3377,17 @@ export async function handleToolCall(name, args, client, context) {
3252
3377
  if (isGoshtiReadTool(name)) {
3253
3378
  return handleGoshtiReadToolCall(name, args, client);
3254
3379
  }
3380
+ // WhatsApp reads (add-whatsapp-personal-reader §3). Local only: no agent
3381
+ // registration needed, no server call, refused unless the session is private.
3382
+ if (isWhatsAppTool(name)) {
3383
+ const out = await runWhatsAppTool(name, args, {
3384
+ env: process.env,
3385
+ homeDir: os.homedir(),
3386
+ workingDir: context.getWorkingDir(),
3387
+ fetch: (url, init) => fetch(url, init),
3388
+ });
3389
+ return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
3390
+ }
3255
3391
  // Orchestrated runs (add-live-animated-views §1.8). Same module pattern. The
3256
3392
  // run id run_open returns is kept in .agenthub/state, so the other run tools
3257
3393
  // (and sub-agents sharing the checkout's state) default to it.
@@ -3957,7 +4093,7 @@ export async function handleToolCall(name, args, client, context) {
3957
4093
  throw new Error("Not registered. Call agent_register first.");
3958
4094
  const inboxLimit = resolveInboxLimit(args.limit);
3959
4095
  const raw = await client.getInbox(agentId, args.unread_only, inboxLimit);
3960
- return boundInbox(raw);
4096
+ return { ...boundInbox(raw), ...inviteFields(raw.pending_brainstorm_invites) };
3961
4097
  }
3962
4098
  case "mark_read": {
3963
4099
  const result = await client.markRead(args.message_id);
@@ -4103,6 +4239,12 @@ export async function handleToolCall(name, args, client, context) {
4103
4239
  const result = await client.joinChannel(args.channel, agentId);
4104
4240
  return wrapWithPendingItems(client, agentId, result, context);
4105
4241
  }
4242
+ case "room_history": {
4243
+ if (!agentId)
4244
+ throw new Error("Not registered. Call agent_register first.");
4245
+ const result = await client.roomHistory(args.room, args.since, args.limit);
4246
+ return wrapWithPendingItems(client, agentId, result, context);
4247
+ }
4106
4248
  case "leave_channel": {
4107
4249
  if (!agentId)
4108
4250
  throw new Error("Not registered. Call agent_register first.");
@@ -4530,6 +4672,55 @@ export async function handleToolCall(name, args, client, context) {
4530
4672
  message: `Filed bug ${result.key} (${result.severity}/${result.bug_state}): ${args.title}`,
4531
4673
  };
4532
4674
  }
4675
+ case "lifecycle_gate_request": {
4676
+ const projectId = lifecycleGateProject(args, context);
4677
+ if (!projectId) {
4678
+ return {
4679
+ ok: false,
4680
+ error: "invalid_args",
4681
+ message: "project_id is required (or bind this agent to a project with agent_register).",
4682
+ };
4683
+ }
4684
+ const body = { project_id: projectId };
4685
+ for (const k of ["task_id", "stage", "title", "content", "evidence_hash", "items", "parent_review_id"]) {
4686
+ if (args[k] !== undefined)
4687
+ body[k] = args[k];
4688
+ }
4689
+ try {
4690
+ const review = await client.post("/lifecycle/gates", body);
4691
+ return {
4692
+ ok: true,
4693
+ review_id: review.id,
4694
+ status: review.status,
4695
+ review,
4696
+ message: `Opened lifecycle gate ${review.id} (stage ${String(args.stage)}, ${review.status}). An org owner or admin signs it on /reviews.`,
4697
+ };
4698
+ }
4699
+ catch (err) {
4700
+ return lifecycleGateError(err);
4701
+ }
4702
+ }
4703
+ case "lifecycle_gate_list": {
4704
+ const projectId = lifecycleGateProject(args, context);
4705
+ if (!projectId) {
4706
+ return {
4707
+ ok: false,
4708
+ error: "invalid_args",
4709
+ message: "project_id is required (or bind this agent to a project with agent_register).",
4710
+ };
4711
+ }
4712
+ const params = new URLSearchParams({ project_id: projectId });
4713
+ for (const k of ["stage", "task_id", "status"]) {
4714
+ if (typeof args[k] === "string" && args[k] !== "")
4715
+ params.set(k, args[k]);
4716
+ }
4717
+ try {
4718
+ return await client.get(`/lifecycle/gates?${params.toString()}`);
4719
+ }
4720
+ catch (err) {
4721
+ return lifecycleGateError(err);
4722
+ }
4723
+ }
4533
4724
  case "skill_pack_list": {
4534
4725
  try {
4535
4726
  const { packs } = await client.listSkillPacks();
@@ -4597,7 +4788,8 @@ export async function handleToolCall(name, args, client, context) {
4597
4788
  const c = report.counts;
4598
4789
  const summary = `${pack} v${manifest.version}: ${c.installed} installed, ${c.updated} updated, ` +
4599
4790
  `${c.skipped_current} unchanged, ${c.skipped_user_modified} skipped (user-modified), ` +
4600
- `${c.overwritten_force} overwritten, ${c.error} errors.`;
4791
+ `${c.overwritten_force} overwritten, ${c.removed} removed (dropped by this version), ` +
4792
+ `${c.kept_modified} kept (dropped but user-modified), ${c.error} errors.`;
4601
4793
  return {
4602
4794
  ok: c.error === 0,
4603
4795
  pack,
@@ -4692,7 +4884,37 @@ export async function handleToolCall(name, args, client, context) {
4692
4884
  invites: result.data.invites,
4693
4885
  mirror_thread_id: result.data.mirror_thread_id,
4694
4886
  mirror_warning: result.data.mirror_warning,
4695
- message: `Brainstorm ${result.data.display_number} opened with ${invitees}. Invite delivered${result.data.invite_delivered === false ? " FAILED" : ""}.`,
4887
+ // separate-agent-chat-from-brainstorms §1.4: event | message |
4888
+ // event+message | none. "none" means recorded but the invitee is not
4889
+ // connected — it will see the invite in pending_brainstorm_invites.
4890
+ invite_delivery: result.data.invite_delivery,
4891
+ warning: result.data.warning,
4892
+ message: `Brainstorm ${result.data.display_number} opened with ${invitees}. ${result.data.invite_delivered === false
4893
+ ? result.data.warning
4894
+ ? "Invite delivery FAILED."
4895
+ : "Invite recorded; the invitee is not connected and will see it in pending_brainstorm_invites."
4896
+ : "Invite delivered."}`,
4897
+ };
4898
+ }
4899
+ case "brainstorm_invites": {
4900
+ // separate-agent-chat-from-brainstorms §2.1. Not wrapped: the wrapper
4901
+ // would repeat this same list as pending_brainstorm_invites.
4902
+ if (!agentId)
4903
+ throw new Error("Not registered. Call agent_register first.");
4904
+ const result = await client.brainstormInvites();
4905
+ if (!result.ok)
4906
+ return brainstormError("brainstorm_invites", result);
4907
+ const invites = Array.isArray(result.data.invites) ? result.data.invites : [];
4908
+ const autoAccept = brainstormAutoAcceptEnabled();
4909
+ return {
4910
+ ok: true,
4911
+ count: invites.length,
4912
+ invites,
4913
+ auto_accept: autoAccept,
4914
+ guidance: inviteGuidance(autoAccept),
4915
+ message: invites.length === 0
4916
+ ? "No pending brainstorm invites."
4917
+ : `${invites.length} pending brainstorm invite(s). ${autoAccept ? "Auto-accept is on." : "Ask your human before accepting any you were not asked to join."}`,
4696
4918
  };
4697
4919
  }
4698
4920
  case "brainstorm_accept": {
@@ -5251,6 +5473,8 @@ export async function handleToolCall(name, args, client, context) {
5251
5473
  brainParams.set("category", args.category);
5252
5474
  if (args.project)
5253
5475
  brainParams.set("project", args.project);
5476
+ if (args.tag)
5477
+ brainParams.set("tag", args.tag);
5254
5478
  if (args.limit)
5255
5479
  brainParams.set("limit", String(args.limit));
5256
5480
  return client.get(`/brain/knowledge?${brainParams.toString()}`);
@@ -5260,10 +5484,13 @@ export async function handleToolCall(name, args, client, context) {
5260
5484
  throw new Error("Not registered. Call agent_register first.");
5261
5485
  return client.createBrainKnowledge({
5262
5486
  category: args.category,
5263
- fact: args.fact,
5487
+ fact: args.fact || "",
5264
5488
  detail: args.detail || "",
5265
5489
  source_agent: agentId,
5266
5490
  project: args.project,
5491
+ // The server stores tags as a JSON array string.
5492
+ ...(Array.isArray(args.tags) ? { tags: JSON.stringify(args.tags) } : {}),
5493
+ ...(args.lesson ? { lesson: args.lesson } : {}),
5267
5494
  });
5268
5495
  }
5269
5496
  case "brain_get_context": {