claude-code-swarm 0.3.3 → 0.3.5

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 (273) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +22 -1
  3. package/.claude-plugin/run-agent-inbox-mcp.sh +76 -0
  4. package/.claude-plugin/run-minimem-mcp.sh +98 -0
  5. package/.claude-plugin/run-opentasks-mcp.sh +65 -0
  6. package/CLAUDE.md +200 -36
  7. package/README.md +65 -0
  8. package/e2e/helpers/cleanup.mjs +17 -3
  9. package/e2e/helpers/map-mock-server.mjs +201 -25
  10. package/e2e/helpers/sidecar.mjs +222 -0
  11. package/e2e/helpers/workspace.mjs +2 -1
  12. package/e2e/tier5-sidecar-inbox.test.mjs +900 -0
  13. package/e2e/tier6-inbox-mcp.test.mjs +173 -0
  14. package/e2e/tier6-live-agent.test.mjs +759 -0
  15. package/e2e/vitest.config.e2e.mjs +1 -1
  16. package/hooks/hooks.json +15 -8
  17. package/package.json +13 -1
  18. package/references/agent-inbox/CLAUDE.md +151 -0
  19. package/references/agent-inbox/README.md +238 -0
  20. package/references/agent-inbox/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
  21. package/references/agent-inbox/docs/DESIGN.md +1156 -0
  22. package/references/agent-inbox/hooks/inbox-hook.mjs +119 -0
  23. package/references/agent-inbox/hooks/register-hook.mjs +69 -0
  24. package/references/agent-inbox/package-lock.json +3347 -0
  25. package/references/agent-inbox/package.json +58 -0
  26. package/references/agent-inbox/rules/agent-inbox.md +78 -0
  27. package/references/agent-inbox/src/federation/address.ts +61 -0
  28. package/references/agent-inbox/src/federation/connection-manager.ts +573 -0
  29. package/references/agent-inbox/src/federation/delivery-queue.ts +222 -0
  30. package/references/agent-inbox/src/federation/index.ts +6 -0
  31. package/references/agent-inbox/src/federation/routing-engine.ts +188 -0
  32. package/references/agent-inbox/src/federation/trust.ts +71 -0
  33. package/references/agent-inbox/src/index.ts +390 -0
  34. package/references/agent-inbox/src/ipc/ipc-server.ts +207 -0
  35. package/references/agent-inbox/src/jsonrpc/mail-server.ts +382 -0
  36. package/references/agent-inbox/src/map/map-client.ts +414 -0
  37. package/references/agent-inbox/src/mcp/mcp-server.ts +272 -0
  38. package/references/agent-inbox/src/mesh/delivery-bridge.ts +110 -0
  39. package/references/agent-inbox/src/mesh/mesh-connector.ts +41 -0
  40. package/references/agent-inbox/src/mesh/mesh-transport.ts +157 -0
  41. package/references/agent-inbox/src/mesh/type-mapper.ts +239 -0
  42. package/references/agent-inbox/src/push/notifier.ts +233 -0
  43. package/references/agent-inbox/src/registry/warm-registry.ts +255 -0
  44. package/references/agent-inbox/src/router/message-router.ts +175 -0
  45. package/references/agent-inbox/src/storage/interface.ts +48 -0
  46. package/references/agent-inbox/src/storage/memory.ts +145 -0
  47. package/references/agent-inbox/src/storage/sqlite.ts +671 -0
  48. package/references/agent-inbox/src/traceability/traceability.ts +183 -0
  49. package/references/agent-inbox/src/types.ts +303 -0
  50. package/references/agent-inbox/test/federation/address.test.ts +101 -0
  51. package/references/agent-inbox/test/federation/connection-manager.test.ts +546 -0
  52. package/references/agent-inbox/test/federation/delivery-queue.test.ts +159 -0
  53. package/references/agent-inbox/test/federation/integration.test.ts +857 -0
  54. package/references/agent-inbox/test/federation/routing-engine.test.ts +117 -0
  55. package/references/agent-inbox/test/federation/sdk-integration.test.ts +744 -0
  56. package/references/agent-inbox/test/federation/trust.test.ts +89 -0
  57. package/references/agent-inbox/test/ipc-jsonrpc.test.ts +113 -0
  58. package/references/agent-inbox/test/ipc-server.test.ts +197 -0
  59. package/references/agent-inbox/test/mail-server.test.ts +285 -0
  60. package/references/agent-inbox/test/map-client.test.ts +408 -0
  61. package/references/agent-inbox/test/mesh/delivery-bridge.test.ts +178 -0
  62. package/references/agent-inbox/test/mesh/e2e-mesh.test.ts +527 -0
  63. package/references/agent-inbox/test/mesh/e2e-real-meshpeer.test.ts +629 -0
  64. package/references/agent-inbox/test/mesh/federation-mesh.test.ts +269 -0
  65. package/references/agent-inbox/test/mesh/mesh-connector.test.ts +66 -0
  66. package/references/agent-inbox/test/mesh/mesh-transport.test.ts +191 -0
  67. package/references/agent-inbox/test/mesh/meshpeer-integration.test.ts +442 -0
  68. package/references/agent-inbox/test/mesh/mock-mesh.ts +125 -0
  69. package/references/agent-inbox/test/mesh/mock-meshpeer.ts +266 -0
  70. package/references/agent-inbox/test/mesh/type-mapper.test.ts +226 -0
  71. package/references/agent-inbox/test/message-router.test.ts +184 -0
  72. package/references/agent-inbox/test/push-notifier.test.ts +139 -0
  73. package/references/agent-inbox/test/registry/warm-registry.test.ts +171 -0
  74. package/references/agent-inbox/test/sqlite-prefix.test.ts +192 -0
  75. package/references/agent-inbox/test/sqlite-storage.test.ts +243 -0
  76. package/references/agent-inbox/test/storage.test.ts +196 -0
  77. package/references/agent-inbox/test/traceability.test.ts +123 -0
  78. package/references/agent-inbox/test/wake.test.ts +330 -0
  79. package/references/agent-inbox/tsconfig.json +20 -0
  80. package/references/agent-inbox/tsup.config.ts +10 -0
  81. package/references/agent-inbox/vitest.config.ts +8 -0
  82. package/references/minimem/.claude/settings.json +7 -0
  83. package/references/minimem/.sudocode/issues.jsonl +18 -0
  84. package/references/minimem/.sudocode/specs.jsonl +1 -0
  85. package/references/minimem/CLAUDE.md +329 -0
  86. package/references/minimem/README.md +565 -0
  87. package/references/minimem/claude-plugin/.claude-plugin/plugin.json +10 -0
  88. package/references/minimem/claude-plugin/.mcp.json +7 -0
  89. package/references/minimem/claude-plugin/README.md +158 -0
  90. package/references/minimem/claude-plugin/commands/recall.md +47 -0
  91. package/references/minimem/claude-plugin/commands/remember.md +41 -0
  92. package/references/minimem/claude-plugin/hooks/__tests__/hooks.test.ts +272 -0
  93. package/references/minimem/claude-plugin/hooks/hooks.json +27 -0
  94. package/references/minimem/claude-plugin/hooks/session-end.sh +86 -0
  95. package/references/minimem/claude-plugin/hooks/session-start.sh +85 -0
  96. package/references/minimem/claude-plugin/skills/memory/SKILL.md +108 -0
  97. package/references/minimem/media/banner.png +0 -0
  98. package/references/minimem/package-lock.json +5373 -0
  99. package/references/minimem/package.json +76 -0
  100. package/references/minimem/scripts/postbuild.js +49 -0
  101. package/references/minimem/src/__tests__/edge-cases.test.ts +371 -0
  102. package/references/minimem/src/__tests__/errors.test.ts +265 -0
  103. package/references/minimem/src/__tests__/helpers.ts +199 -0
  104. package/references/minimem/src/__tests__/internal.test.ts +407 -0
  105. package/references/minimem/src/__tests__/knowledge-frontmatter.test.ts +148 -0
  106. package/references/minimem/src/__tests__/knowledge.test.ts +148 -0
  107. package/references/minimem/src/__tests__/minimem.integration.test.ts +1127 -0
  108. package/references/minimem/src/__tests__/session.test.ts +190 -0
  109. package/references/minimem/src/cli/__tests__/commands.test.ts +760 -0
  110. package/references/minimem/src/cli/__tests__/contained-layout.test.ts +286 -0
  111. package/references/minimem/src/cli/commands/__tests__/conflicts.test.ts +141 -0
  112. package/references/minimem/src/cli/commands/append.ts +76 -0
  113. package/references/minimem/src/cli/commands/config.ts +262 -0
  114. package/references/minimem/src/cli/commands/conflicts.ts +415 -0
  115. package/references/minimem/src/cli/commands/daemon.ts +169 -0
  116. package/references/minimem/src/cli/commands/index.ts +12 -0
  117. package/references/minimem/src/cli/commands/init.ts +166 -0
  118. package/references/minimem/src/cli/commands/mcp.ts +221 -0
  119. package/references/minimem/src/cli/commands/push-pull.ts +213 -0
  120. package/references/minimem/src/cli/commands/search.ts +223 -0
  121. package/references/minimem/src/cli/commands/status.ts +84 -0
  122. package/references/minimem/src/cli/commands/store.ts +189 -0
  123. package/references/minimem/src/cli/commands/sync-init.ts +290 -0
  124. package/references/minimem/src/cli/commands/sync.ts +70 -0
  125. package/references/minimem/src/cli/commands/upsert.ts +197 -0
  126. package/references/minimem/src/cli/config.ts +611 -0
  127. package/references/minimem/src/cli/index.ts +299 -0
  128. package/references/minimem/src/cli/shared.ts +189 -0
  129. package/references/minimem/src/cli/sync/__tests__/central.test.ts +152 -0
  130. package/references/minimem/src/cli/sync/__tests__/conflicts.test.ts +209 -0
  131. package/references/minimem/src/cli/sync/__tests__/daemon.test.ts +118 -0
  132. package/references/minimem/src/cli/sync/__tests__/detection.test.ts +207 -0
  133. package/references/minimem/src/cli/sync/__tests__/integration.test.ts +476 -0
  134. package/references/minimem/src/cli/sync/__tests__/registry.test.ts +363 -0
  135. package/references/minimem/src/cli/sync/__tests__/state.test.ts +255 -0
  136. package/references/minimem/src/cli/sync/__tests__/validation.test.ts +193 -0
  137. package/references/minimem/src/cli/sync/__tests__/watcher.test.ts +178 -0
  138. package/references/minimem/src/cli/sync/central.ts +292 -0
  139. package/references/minimem/src/cli/sync/conflicts.ts +205 -0
  140. package/references/minimem/src/cli/sync/daemon.ts +407 -0
  141. package/references/minimem/src/cli/sync/detection.ts +138 -0
  142. package/references/minimem/src/cli/sync/index.ts +107 -0
  143. package/references/minimem/src/cli/sync/operations.ts +373 -0
  144. package/references/minimem/src/cli/sync/registry.ts +279 -0
  145. package/references/minimem/src/cli/sync/state.ts +358 -0
  146. package/references/minimem/src/cli/sync/validation.ts +206 -0
  147. package/references/minimem/src/cli/sync/watcher.ts +237 -0
  148. package/references/minimem/src/cli/version.ts +34 -0
  149. package/references/minimem/src/core/index.ts +9 -0
  150. package/references/minimem/src/core/indexer.ts +628 -0
  151. package/references/minimem/src/core/searcher.ts +221 -0
  152. package/references/minimem/src/db/schema.ts +183 -0
  153. package/references/minimem/src/db/sqlite-vec.ts +24 -0
  154. package/references/minimem/src/embeddings/__tests__/embeddings.test.ts +431 -0
  155. package/references/minimem/src/embeddings/batch-gemini.ts +392 -0
  156. package/references/minimem/src/embeddings/batch-openai.ts +409 -0
  157. package/references/minimem/src/embeddings/embeddings.ts +434 -0
  158. package/references/minimem/src/index.ts +132 -0
  159. package/references/minimem/src/internal.ts +299 -0
  160. package/references/minimem/src/minimem.ts +1291 -0
  161. package/references/minimem/src/search/__tests__/hybrid.test.ts +247 -0
  162. package/references/minimem/src/search/graph.ts +234 -0
  163. package/references/minimem/src/search/hybrid.ts +151 -0
  164. package/references/minimem/src/search/search.ts +256 -0
  165. package/references/minimem/src/server/__tests__/mcp.test.ts +347 -0
  166. package/references/minimem/src/server/__tests__/tools.test.ts +364 -0
  167. package/references/minimem/src/server/mcp.ts +326 -0
  168. package/references/minimem/src/server/tools.ts +720 -0
  169. package/references/minimem/src/session.ts +460 -0
  170. package/references/minimem/src/store/__tests__/manifest.test.ts +177 -0
  171. package/references/minimem/src/store/__tests__/materialize.test.ts +52 -0
  172. package/references/minimem/src/store/__tests__/store-graph.test.ts +228 -0
  173. package/references/minimem/src/store/index.ts +27 -0
  174. package/references/minimem/src/store/manifest.ts +203 -0
  175. package/references/minimem/src/store/materialize.ts +185 -0
  176. package/references/minimem/src/store/store-graph.ts +252 -0
  177. package/references/minimem/tsconfig.json +19 -0
  178. package/references/minimem/tsup.config.ts +26 -0
  179. package/references/minimem/vitest.config.ts +29 -0
  180. package/references/openteams/src/cli/generate.ts +23 -1
  181. package/references/openteams/src/generators/agent-prompt-generator.test.ts +94 -0
  182. package/references/openteams/src/generators/agent-prompt-generator.ts +42 -13
  183. package/references/openteams/src/generators/package-generator.ts +9 -1
  184. package/references/openteams/src/generators/skill-generator.test.ts +28 -0
  185. package/references/openteams/src/generators/skill-generator.ts +10 -4
  186. package/references/skill-tree/.claude/settings.json +6 -0
  187. package/references/skill-tree/.sudocode/issues.jsonl +19 -0
  188. package/references/skill-tree/.sudocode/specs.jsonl +3 -0
  189. package/references/skill-tree/CLAUDE.md +132 -0
  190. package/references/skill-tree/README.md +396 -0
  191. package/references/skill-tree/docs/GAPS_v1.md +221 -0
  192. package/references/skill-tree/docs/INTEGRATION_PLAN.md +467 -0
  193. package/references/skill-tree/docs/TODOS.md +91 -0
  194. package/references/skill-tree/docs/anthropic_skill_guide.md +1364 -0
  195. package/references/skill-tree/docs/design/federated-skill-trees.md +524 -0
  196. package/references/skill-tree/docs/design/multi-agent-sync.md +759 -0
  197. package/references/skill-tree/docs/scraper/BRAINSTORM.md +583 -0
  198. package/references/skill-tree/docs/scraper/POC_PLAN.md +420 -0
  199. package/references/skill-tree/docs/scraper/README.md +170 -0
  200. package/references/skill-tree/examples/basic-usage.ts +157 -0
  201. package/references/skill-tree/package-lock.json +1852 -0
  202. package/references/skill-tree/package.json +66 -0
  203. package/references/skill-tree/plan.md +78 -0
  204. package/references/skill-tree/scraper/README.md +123 -0
  205. package/references/skill-tree/scraper/docs/DESIGN.md +683 -0
  206. package/references/skill-tree/scraper/docs/PLAN.md +336 -0
  207. package/references/skill-tree/scraper/drizzle.config.ts +10 -0
  208. package/references/skill-tree/scraper/package-lock.json +6329 -0
  209. package/references/skill-tree/scraper/package.json +68 -0
  210. package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-description.md +7 -0
  211. package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-name.md +7 -0
  212. package/references/skill-tree/scraper/test/fixtures/minimal-skill/SKILL.md +27 -0
  213. package/references/skill-tree/scraper/test/fixtures/skill-json/SKILL.json +21 -0
  214. package/references/skill-tree/scraper/test/fixtures/skill-with-meta/SKILL.md +54 -0
  215. package/references/skill-tree/scraper/test/fixtures/skill-with-meta/_meta.json +24 -0
  216. package/references/skill-tree/scraper/test/fixtures/valid-skill/SKILL.md +93 -0
  217. package/references/skill-tree/scraper/test/fixtures/valid-skill/_meta.json +22 -0
  218. package/references/skill-tree/scraper/tsup.config.ts +14 -0
  219. package/references/skill-tree/scraper/vitest.config.ts +17 -0
  220. package/references/skill-tree/scripts/convert-to-vitest.ts +166 -0
  221. package/references/skill-tree/skills/skill-writer/SKILL.md +339 -0
  222. package/references/skill-tree/skills/skill-writer/references/examples.md +326 -0
  223. package/references/skill-tree/skills/skill-writer/references/patterns.md +210 -0
  224. package/references/skill-tree/skills/skill-writer/references/quality-checklist.md +123 -0
  225. package/references/skill-tree/test/run-all.ts +106 -0
  226. package/references/skill-tree/test/utils.ts +128 -0
  227. package/references/skill-tree/vitest.config.ts +16 -0
  228. package/references/swarmkit/src/commands/init/phases/configure.ts +0 -22
  229. package/references/swarmkit/src/commands/init/phases/global-setup.ts +5 -3
  230. package/references/swarmkit/src/commands/init/wizard.ts +2 -2
  231. package/references/swarmkit/src/packages/setup.test.ts +53 -7
  232. package/references/swarmkit/src/packages/setup.ts +37 -1
  233. package/scripts/bootstrap.mjs +26 -1
  234. package/scripts/generate-agents.mjs +5 -1
  235. package/scripts/map-hook.mjs +97 -64
  236. package/scripts/map-sidecar.mjs +179 -25
  237. package/scripts/team-loader.mjs +12 -41
  238. package/skills/swarm/SKILL.md +89 -25
  239. package/src/__tests__/agent-generator.test.mjs +6 -13
  240. package/src/__tests__/bootstrap.test.mjs +124 -1
  241. package/src/__tests__/config.test.mjs +200 -27
  242. package/src/__tests__/e2e-live-map.test.mjs +536 -0
  243. package/src/__tests__/e2e-mesh-sidecar.test.mjs +570 -0
  244. package/src/__tests__/e2e-native-task-hooks.test.mjs +376 -0
  245. package/src/__tests__/e2e-sidecar-bridge.test.mjs +477 -0
  246. package/src/__tests__/helpers.mjs +13 -0
  247. package/src/__tests__/inbox.test.mjs +22 -89
  248. package/src/__tests__/index.test.mjs +35 -9
  249. package/src/__tests__/integration.test.mjs +513 -0
  250. package/src/__tests__/map-events.test.mjs +514 -150
  251. package/src/__tests__/mesh-connection.test.mjs +308 -0
  252. package/src/__tests__/opentasks-client.test.mjs +517 -0
  253. package/src/__tests__/paths.test.mjs +185 -41
  254. package/src/__tests__/sidecar-client.test.mjs +35 -0
  255. package/src/__tests__/sidecar-server.test.mjs +124 -0
  256. package/src/__tests__/skilltree-client.test.mjs +80 -0
  257. package/src/agent-generator.mjs +104 -33
  258. package/src/bootstrap.mjs +150 -10
  259. package/src/config.mjs +81 -17
  260. package/src/context-output.mjs +58 -8
  261. package/src/inbox.mjs +9 -54
  262. package/src/index.mjs +39 -8
  263. package/src/map-connection.mjs +4 -3
  264. package/src/map-events.mjs +350 -80
  265. package/src/mesh-connection.mjs +148 -0
  266. package/src/opentasks-client.mjs +269 -0
  267. package/src/paths.mjs +182 -27
  268. package/src/sessionlog.mjs +14 -9
  269. package/src/sidecar-client.mjs +81 -27
  270. package/src/sidecar-server.mjs +175 -16
  271. package/src/skilltree-client.mjs +173 -0
  272. package/src/template.mjs +68 -4
  273. package/vitest.config.mjs +1 -0
@@ -1,13 +1,13 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import {
3
- buildSpawnCommand,
4
- buildDoneCommand,
5
3
  buildSubagentSpawnCommand,
6
4
  buildSubagentDoneCommand,
7
5
  buildStateCommand,
8
- buildTaskDispatchedPayload,
9
- buildTaskCompletedPayload,
10
- buildTaskStatusPayload,
6
+ buildTaskSyncPayload,
7
+ buildOpentasksBridgeCommands,
8
+ handleTaskCreated,
9
+ handleTaskCompleted,
10
+ handleTaskStatusCompleted,
11
11
  } from "../map-events.mjs";
12
12
  import {
13
13
  makeHookData,
@@ -20,86 +20,6 @@ import {
20
20
  describe("map-events", () => {
21
21
  // ── Agent lifecycle commands ──────────────────────────────────────────────
22
22
 
23
- describe("buildSpawnCommand", () => {
24
- it("returns action 'spawn'", () => {
25
- const cmd = buildSpawnCommand("agent-1", "executor", "gsd", makeHookData());
26
- expect(cmd.action).toBe("spawn");
27
- });
28
-
29
- it("sets agentId to teamName-role when matched", () => {
30
- const cmd = buildSpawnCommand("agent-1", "executor", "gsd", makeHookData());
31
- expect(cmd.agent.agentId).toBe("gsd-executor");
32
- });
33
-
34
- it("sets agentId to agentName when no role match", () => {
35
- const cmd = buildSpawnCommand("my-agent", null, "gsd", makeHookData());
36
- expect(cmd.agent.agentId).toBe("my-agent");
37
- });
38
-
39
- it("sets name to matchedRole when provided", () => {
40
- const cmd = buildSpawnCommand("a", "executor", "t", makeHookData());
41
- expect(cmd.agent.name).toBe("executor");
42
- });
43
-
44
- it("sets name to agentName when no match", () => {
45
- const cmd = buildSpawnCommand("my-agent", null, "t", makeHookData());
46
- expect(cmd.agent.name).toBe("my-agent");
47
- });
48
-
49
- it("sets role from matchedRole or 'internal'", () => {
50
- expect(buildSpawnCommand("a", "executor", "t", makeHookData()).agent.role).toBe("executor");
51
- expect(buildSpawnCommand("a", null, "t", makeHookData()).agent.role).toBe("internal");
52
- });
53
-
54
- it("sets scopes to swarm:teamName", () => {
55
- const cmd = buildSpawnCommand("a", null, "my-team", makeHookData());
56
- expect(cmd.agent.scopes).toEqual(["swarm:my-team"]);
57
- });
58
-
59
- it("sets metadata.isTeamRole based on matchedRole", () => {
60
- expect(buildSpawnCommand("a", "exec", "t", makeHookData()).agent.metadata.isTeamRole).toBe(true);
61
- expect(buildSpawnCommand("a", null, "t", makeHookData()).agent.metadata.isTeamRole).toBe(false);
62
- });
63
-
64
- it("sets metadata.template to teamName", () => {
65
- const cmd = buildSpawnCommand("a", null, "gsd", makeHookData());
66
- expect(cmd.agent.metadata.template).toBe("gsd");
67
- });
68
-
69
- it("truncates task in metadata to 300 characters", () => {
70
- const longPrompt = "x".repeat(500);
71
- const cmd = buildSpawnCommand("a", null, "t", makeHookData({ prompt: longPrompt }));
72
- expect(cmd.agent.metadata.task.length).toBe(300);
73
- });
74
-
75
- it("uses tool_input.prompt for task metadata", () => {
76
- const cmd = buildSpawnCommand("a", null, "t", makeHookData({ prompt: "do X" }));
77
- expect(cmd.agent.metadata.task).toBe("do X");
78
- });
79
- });
80
-
81
- describe("buildDoneCommand", () => {
82
- it("returns action 'done'", () => {
83
- const cmd = buildDoneCommand("a", "executor", "t");
84
- expect(cmd.action).toBe("done");
85
- });
86
-
87
- it("sets agentId to teamName-role when matched", () => {
88
- const cmd = buildDoneCommand("a", "executor", "gsd");
89
- expect(cmd.agentId).toBe("gsd-executor");
90
- });
91
-
92
- it("sets agentId to agentName when no match", () => {
93
- const cmd = buildDoneCommand("my-agent", null, "gsd");
94
- expect(cmd.agentId).toBe("my-agent");
95
- });
96
-
97
- it("sets reason to 'completed'", () => {
98
- const cmd = buildDoneCommand("a", null, "t");
99
- expect(cmd.reason).toBe("completed");
100
- });
101
- });
102
-
103
23
  describe("buildSubagentSpawnCommand", () => {
104
24
  it("returns action 'spawn'", () => {
105
25
  const cmd = buildSubagentSpawnCommand(makeSubagentStartData(), "gsd");
@@ -216,110 +136,554 @@ describe("map-events", () => {
216
136
  });
217
137
  });
218
138
 
219
- // ── Task lifecycle payloads ───────────────────────────────────────────────
139
+ // ── Task lifecycle handlers (opentasks daemon + MAP event bridge) ─────────
140
+ //
141
+ // These test the full two-step flow:
142
+ // 1. Task CRUD in opentasks daemon (via mocked opentasks-client)
143
+ // 2. Bridge event emission to MAP (via mocked sidecar-client capturing commands)
144
+
145
+ describe("handleTaskCreated", () => {
146
+ let mockCreateTask;
147
+ let mockFindSocketPath;
148
+ let sidecarCommands;
149
+
150
+ beforeEach(async () => {
151
+ mockCreateTask = vi.fn().mockResolvedValue({ id: "created-task-1" });
152
+ mockFindSocketPath = vi.fn().mockReturnValue("/tmp/test.sock");
153
+ sidecarCommands = [];
154
+
155
+ // Mock opentasks-client (dynamic import target)
156
+ vi.doMock("../opentasks-client.mjs", () => ({
157
+ createTask: mockCreateTask,
158
+ findSocketPath: mockFindSocketPath,
159
+ }));
160
+
161
+ // Mock sidecar-client to capture commands sent via sendCommand
162
+ vi.doMock("../sidecar-client.mjs", () => ({
163
+ sendToSidecar: vi.fn(async (cmd) => {
164
+ sidecarCommands.push(cmd);
165
+ return true;
166
+ }),
167
+ ensureSidecar: vi.fn().mockResolvedValue(false),
168
+ }));
169
+
170
+ // Mock paths to avoid filesystem access
171
+ vi.doMock("../paths.mjs", () => ({
172
+ sessionPaths: vi.fn(() => ({
173
+ socketPath: "/tmp/sidecar.sock",
174
+ inboxSocketPath: "/tmp/inbox.sock",
175
+ })),
176
+ }));
177
+ });
178
+
179
+ afterEach(() => {
180
+ vi.restoreAllMocks();
181
+ vi.resetModules();
182
+ });
183
+
184
+ it("creates task in opentasks with correct params", async () => {
185
+ const { handleTaskCreated } = await import("../map-events.mjs");
186
+ const hookData = makeHookData({ prompt: "Fix the bug" });
187
+ const config = { map: { enabled: true } };
188
+
189
+ await handleTaskCreated(config, hookData, "gsd", "executor", "test-agent", null);
190
+
191
+ expect(mockCreateTask).toHaveBeenCalledWith("/tmp/test.sock", expect.objectContaining({
192
+ title: "Fix the bug",
193
+ status: "open",
194
+ content: "Fix the bug",
195
+ assignee: "gsd-executor",
196
+ metadata: expect.objectContaining({
197
+ source: "claude-code-swarm",
198
+ teamName: "gsd",
199
+ role: "executor",
200
+ }),
201
+ }));
202
+ });
203
+
204
+ it("emits bridge-task-created command with task data", async () => {
205
+ const { handleTaskCreated } = await import("../map-events.mjs");
206
+ const hookData = makeHookData({ prompt: "Fix the bug" });
207
+ const config = { map: { enabled: true } };
208
+
209
+ await handleTaskCreated(config, hookData, "gsd", "executor", "test-agent", "sess-1");
210
+
211
+ const createdCmd = sidecarCommands.find((c) => c.action === "bridge-task-created");
212
+ expect(createdCmd).toBeDefined();
213
+ expect(createdCmd.task.id).toBe("created-task-1");
214
+ expect(createdCmd.task.title).toBe("Fix the bug");
215
+ expect(createdCmd.task.status).toBe("open");
216
+ expect(createdCmd.task.assignee).toBe("gsd-executor");
217
+ expect(createdCmd.agentId).toBe("gsd-executor");
218
+ });
219
+
220
+ it("emits bridge-task-assigned command when assignee exists", async () => {
221
+ const { handleTaskCreated } = await import("../map-events.mjs");
222
+ const hookData = makeHookData({ prompt: "Do X" });
223
+ const config = { map: { enabled: true } };
224
+
225
+ await handleTaskCreated(config, hookData, "gsd", "executor", "test-agent", null);
226
+
227
+ const assignedCmd = sidecarCommands.find((c) => c.action === "bridge-task-assigned");
228
+ expect(assignedCmd).toBeDefined();
229
+ expect(assignedCmd.taskId).toBe("created-task-1");
230
+ expect(assignedCmd.assignee).toBe("gsd-executor");
231
+ });
232
+
233
+ it("uses agentName as assignee when no role matched", async () => {
234
+ const { handleTaskCreated } = await import("../map-events.mjs");
235
+ const hookData = makeHookData();
236
+ const config = { map: { enabled: true } };
237
+
238
+ await handleTaskCreated(config, hookData, "gsd", null, "my-agent", null);
239
+
240
+ expect(mockCreateTask).toHaveBeenCalledWith("/tmp/test.sock", expect.objectContaining({
241
+ assignee: "my-agent",
242
+ }));
243
+ });
244
+
245
+ it("falls back to tool_use_id for taskId when createTask returns null", async () => {
246
+ mockCreateTask.mockResolvedValue(null);
247
+ const { handleTaskCreated } = await import("../map-events.mjs");
248
+ const hookData = makeHookData({ toolUseId: "tu-fallback" });
249
+ const config = { map: { enabled: true } };
250
+
251
+ await handleTaskCreated(config, hookData, "gsd", null, "agent", null);
252
+
253
+ const createdCmd = sidecarCommands.find((c) => c.action === "bridge-task-created");
254
+ expect(createdCmd.task.id).toBe("tu-fallback");
255
+ });
256
+ });
257
+
258
+ describe("handleTaskCompleted", () => {
259
+ let mockUpdateTask;
260
+ let mockFindSocketPath;
261
+ let sidecarCommands;
262
+
263
+ beforeEach(() => {
264
+ mockUpdateTask = vi.fn().mockResolvedValue({ id: "task-1" });
265
+ mockFindSocketPath = vi.fn().mockReturnValue("/tmp/test.sock");
266
+ sidecarCommands = [];
220
267
 
221
- describe("buildTaskDispatchedPayload", () => {
222
- it("sets type to 'task.dispatched'", () => {
223
- const p = buildTaskDispatchedPayload(makeHookData(), "t", null, "agent");
224
- expect(p.type).toBe("task.dispatched");
268
+ vi.doMock("../opentasks-client.mjs", () => ({
269
+ updateTask: mockUpdateTask,
270
+ findSocketPath: mockFindSocketPath,
271
+ }));
272
+
273
+ vi.doMock("../sidecar-client.mjs", () => ({
274
+ sendToSidecar: vi.fn(async (cmd) => {
275
+ sidecarCommands.push(cmd);
276
+ return true;
277
+ }),
278
+ ensureSidecar: vi.fn().mockResolvedValue(false),
279
+ }));
280
+
281
+ vi.doMock("../paths.mjs", () => ({
282
+ sessionPaths: vi.fn(() => ({
283
+ socketPath: "/tmp/sidecar.sock",
284
+ inboxSocketPath: "/tmp/inbox.sock",
285
+ })),
286
+ }));
225
287
  });
226
288
 
227
- it("sets taskId from hook data", () => {
228
- const p = buildTaskDispatchedPayload(makeHookData({ toolUseId: "xyz" }), "t", null, "a");
229
- expect(p.taskId).toBe("xyz");
289
+ afterEach(() => {
290
+ vi.restoreAllMocks();
291
+ vi.resetModules();
230
292
  });
231
293
 
232
- it("sets from to teamName-sidecar", () => {
233
- const p = buildTaskDispatchedPayload(makeHookData(), "my-team", null, "a");
234
- expect(p.from).toBe("my-team-sidecar");
294
+ it("updates task to closed status in opentasks", async () => {
295
+ const { handleTaskCompleted } = await import("../map-events.mjs");
296
+ const hookData = makeHookData({ toolUseId: "task-42" });
297
+ const config = { map: { enabled: true } };
298
+
299
+ await handleTaskCompleted(config, hookData, "gsd", "executor", "test-agent", null);
300
+
301
+ expect(mockUpdateTask).toHaveBeenCalledWith("/tmp/test.sock", "task-42", expect.objectContaining({
302
+ status: "closed",
303
+ metadata: expect.objectContaining({
304
+ completedBy: "gsd-executor",
305
+ source: "claude-code-swarm",
306
+ }),
307
+ }));
235
308
  });
236
309
 
237
- it("sets targetAgent to teamName-role when matched", () => {
238
- const p = buildTaskDispatchedPayload(makeHookData(), "gsd", "executor", "a");
239
- expect(p.targetAgent).toBe("gsd-executor");
310
+ it("emits bridge-task-status with completed status", async () => {
311
+ const { handleTaskCompleted } = await import("../map-events.mjs");
312
+ const hookData = makeHookData({ toolUseId: "task-42" });
313
+ const config = { map: { enabled: true } };
314
+
315
+ await handleTaskCompleted(config, hookData, "gsd", "executor", "test-agent", "sess-2");
316
+
317
+ const statusCmd = sidecarCommands.find((c) => c.action === "bridge-task-status");
318
+ expect(statusCmd).toBeDefined();
319
+ expect(statusCmd.taskId).toBe("task-42");
320
+ expect(statusCmd.previous).toBe("open");
321
+ expect(statusCmd.current).toBe("completed");
322
+ expect(statusCmd.agentId).toBe("gsd-executor");
240
323
  });
241
324
 
242
- it("sets targetAgent to agentName when no match", () => {
243
- const p = buildTaskDispatchedPayload(makeHookData(), "gsd", null, "my-agent");
244
- expect(p.targetAgent).toBe("my-agent");
325
+ it("skips opentasks update when no taskId", async () => {
326
+ const { handleTaskCompleted } = await import("../map-events.mjs");
327
+ const hookData = { tool_input: {} }; // no tool_use_id
328
+ const config = { map: { enabled: true } };
329
+
330
+ await handleTaskCompleted(config, hookData, "gsd", null, "agent", null);
331
+
332
+ expect(mockUpdateTask).not.toHaveBeenCalled();
333
+ });
334
+
335
+ it("still emits bridge event even when no taskId", async () => {
336
+ const { handleTaskCompleted } = await import("../map-events.mjs");
337
+ const hookData = { tool_input: {} };
338
+ const config = { map: { enabled: true } };
339
+
340
+ await handleTaskCompleted(config, hookData, "gsd", null, "agent", null);
341
+
342
+ const statusCmd = sidecarCommands.find((c) => c.action === "bridge-task-status");
343
+ expect(statusCmd).toBeDefined();
344
+ expect(statusCmd.current).toBe("completed");
245
345
  });
346
+ });
246
347
 
247
- it("truncates description to 300 characters", () => {
248
- const long = "y".repeat(500);
249
- const p = buildTaskDispatchedPayload(makeHookData({ prompt: long }), "t", null, "a");
250
- expect(p.description.length).toBe(300);
348
+ describe("handleTaskStatusCompleted", () => {
349
+ let mockUpdateTask;
350
+ let mockFindSocketPath;
351
+ let sidecarCommands;
352
+
353
+ beforeEach(() => {
354
+ mockUpdateTask = vi.fn().mockResolvedValue({ id: "task-1" });
355
+ mockFindSocketPath = vi.fn().mockReturnValue("/tmp/test.sock");
356
+ sidecarCommands = [];
357
+
358
+ vi.doMock("../opentasks-client.mjs", () => ({
359
+ updateTask: mockUpdateTask,
360
+ findSocketPath: mockFindSocketPath,
361
+ }));
362
+
363
+ vi.doMock("../sidecar-client.mjs", () => ({
364
+ sendToSidecar: vi.fn(async (cmd) => {
365
+ sidecarCommands.push(cmd);
366
+ return true;
367
+ }),
368
+ ensureSidecar: vi.fn().mockResolvedValue(false),
369
+ }));
370
+
371
+ vi.doMock("../paths.mjs", () => ({
372
+ sessionPaths: vi.fn(() => ({
373
+ socketPath: "/tmp/sidecar.sock",
374
+ inboxSocketPath: "/tmp/inbox.sock",
375
+ })),
376
+ }));
377
+ });
378
+
379
+ afterEach(() => {
380
+ vi.restoreAllMocks();
381
+ vi.resetModules();
382
+ });
383
+
384
+ it("updates task with richer metadata from TaskCompleted hook", async () => {
385
+ const { handleTaskStatusCompleted } = await import("../map-events.mjs");
386
+ const hookData = makeTaskCompletedData({
387
+ taskId: "task-99",
388
+ taskSubject: "Fix bug",
389
+ teammateName: "builder",
390
+ teamName: "gsd",
391
+ });
392
+ const config = { map: { enabled: true } };
393
+
394
+ await handleTaskStatusCompleted(config, hookData, "gsd", "builder", null);
395
+
396
+ expect(mockUpdateTask).toHaveBeenCalledWith("/tmp/test.sock", "task-99", expect.objectContaining({
397
+ status: "closed",
398
+ title: "Fix bug",
399
+ metadata: expect.objectContaining({
400
+ completedBy: "builder",
401
+ teamName: "gsd",
402
+ role: "builder",
403
+ isTeamRole: true,
404
+ source: "claude-code-swarm",
405
+ }),
406
+ }));
407
+ });
408
+
409
+ it("emits bridge-task-status with in_progress → completed", async () => {
410
+ const { handleTaskStatusCompleted } = await import("../map-events.mjs");
411
+ const hookData = makeTaskCompletedData({ taskId: "task-99", teammateName: "builder" });
412
+ const config = { map: { enabled: true } };
413
+
414
+ await handleTaskStatusCompleted(config, hookData, "gsd", "builder", "sess-3");
415
+
416
+ const statusCmd = sidecarCommands.find((c) => c.action === "bridge-task-status");
417
+ expect(statusCmd).toBeDefined();
418
+ expect(statusCmd.taskId).toBe("task-99");
419
+ expect(statusCmd.previous).toBe("in_progress");
420
+ expect(statusCmd.current).toBe("completed");
421
+ expect(statusCmd.agentId).toBe("builder");
422
+ });
423
+
424
+ it("skips opentasks update when no taskId", async () => {
425
+ const { handleTaskStatusCompleted } = await import("../map-events.mjs");
426
+ const hookData = { teammate_name: "builder" }; // no task_id
427
+ const config = { map: { enabled: true } };
428
+
429
+ await handleTaskStatusCompleted(config, hookData, "gsd", "builder", null);
430
+
431
+ expect(mockUpdateTask).not.toHaveBeenCalled();
251
432
  });
252
433
  });
253
434
 
254
- describe("buildTaskCompletedPayload", () => {
255
- it("sets type to 'task.completed'", () => {
256
- const p = buildTaskCompletedPayload(makeHookData(), "t", null, "a");
257
- expect(p.type).toBe("task.completed");
435
+ // ── Task sync payloads (opentasks ↔ MAP bridge) ───────────────────────────
436
+
437
+ describe("buildTaskSyncPayload", () => {
438
+ it("sets type to 'task.sync'", () => {
439
+ const p = buildTaskSyncPayload({ tool_input: { taskId: "t-1" } }, "gsd");
440
+ expect(p.type).toBe("task.sync");
258
441
  });
259
442
 
260
- it("sets agent to teamName-role when matched", () => {
261
- const p = buildTaskCompletedPayload(makeHookData(), "gsd", "exec", "a");
262
- expect(p.agent).toBe("gsd-exec");
443
+ it("sets uri from tool_input.taskId", () => {
444
+ const p = buildTaskSyncPayload({ tool_input: { taskId: "t-1" } }, "gsd");
445
+ expect(p.uri).toBe("claude://gsd/t-1");
263
446
  });
264
447
 
265
- it("sets agent to agentName when no match", () => {
266
- const p = buildTaskCompletedPayload(makeHookData(), "gsd", null, "my-agent");
267
- expect(p.agent).toBe("my-agent");
448
+ it("falls back to hookData.task_id for uri", () => {
449
+ const p = buildTaskSyncPayload({ task_id: "t-2", tool_input: {} }, "gsd");
450
+ expect(p.uri).toBe("claude://gsd/t-2");
268
451
  });
269
452
 
270
- it("sets status to 'completed'", () => {
271
- const p = buildTaskCompletedPayload(makeHookData(), "t", null, "a");
453
+ it("maps status pending to open", () => {
454
+ const p = buildTaskSyncPayload({ tool_input: { status: "pending" } }, "t");
455
+ expect(p.status).toBe("open");
456
+ });
457
+
458
+ it("maps status completed to completed", () => {
459
+ const p = buildTaskSyncPayload({ tool_input: { status: "completed" } }, "t");
272
460
  expect(p.status).toBe("completed");
273
461
  });
274
- });
275
462
 
276
- describe("buildTaskStatusPayload", () => {
277
- it("sets type to 'task.completed'", () => {
278
- const p = buildTaskStatusPayload(makeTaskCompletedData(), "gsd", null);
279
- expect(p.type).toBe("task.completed");
463
+ it("maps status in_progress to in_progress", () => {
464
+ const p = buildTaskSyncPayload({ tool_input: { status: "in_progress" } }, "t");
465
+ expect(p.status).toBe("in_progress");
280
466
  });
281
467
 
282
- it("sets taskId from hookData", () => {
283
- const p = buildTaskStatusPayload(makeTaskCompletedData({ taskId: "t-99" }), "t", null);
284
- expect(p.taskId).toBe("t-99");
468
+ it("defaults status to open when not provided", () => {
469
+ const p = buildTaskSyncPayload({ tool_input: {} }, "t");
470
+ expect(p.status).toBe("open");
285
471
  });
286
472
 
287
- it("sets taskSubject from hookData", () => {
288
- const p = buildTaskStatusPayload(makeTaskCompletedData({ taskSubject: "Fix bug" }), "t", null);
289
- expect(p.taskSubject).toBe("Fix bug");
473
+ it("uses tool_input.subject for subject", () => {
474
+ const p = buildTaskSyncPayload({ tool_input: { subject: "Fix bug" } }, "t");
475
+ expect(p.subject).toBe("Fix bug");
290
476
  });
291
477
 
292
- it("truncates taskDescription to 300 characters", () => {
293
- const longDesc = "d".repeat(500);
294
- const p = buildTaskStatusPayload(makeTaskCompletedData({ taskDescription: longDesc }), "t", null);
295
- expect(p.taskDescription.length).toBe(300);
478
+ it("falls back to task_subject from hookData", () => {
479
+ const p = buildTaskSyncPayload({ task_subject: "Add feature", tool_input: {} }, "t");
480
+ expect(p.subject).toBe("Add feature");
296
481
  });
297
482
 
298
- it("sets agent from hookData.teammate_name", () => {
299
- const p = buildTaskStatusPayload(makeTaskCompletedData({ teammateName: "builder" }), "t", "builder");
300
- expect(p.agent).toBe("builder");
483
+ it("sets source to claude-code", () => {
484
+ const p = buildTaskSyncPayload({ tool_input: {} }, "t");
485
+ expect(p.source).toBe("claude-code");
301
486
  });
487
+ });
302
488
 
303
- it("sets teamName from hookData", () => {
304
- const p = buildTaskStatusPayload(makeTaskCompletedData({ teamName: "gsd" }), "t", null);
305
- expect(p.teamName).toBe("gsd");
489
+ describe("buildOpentasksBridgeCommands", () => {
490
+ // ── create_task ──────────────────────────────────────────────────────
491
+
492
+ it("create_task returns bridge-task-created + bridge-task-assigned", () => {
493
+ const cmds = buildOpentasksBridgeCommands({
494
+ tool_name: "mcp__opentasks__create_task",
495
+ tool_input: { title: "Fix bug", assignee: "worker-1" },
496
+ tool_output: JSON.stringify({ content: [{ text: JSON.stringify({ id: "task-42", title: "Fix bug", status: "open", assignee: "worker-1" }) }] }),
497
+ });
498
+ expect(cmds).toHaveLength(2);
499
+ expect(cmds[0].action).toBe("bridge-task-created");
500
+ expect(cmds[0].task.id).toBe("task-42");
501
+ expect(cmds[0].task.title).toBe("Fix bug");
502
+ expect(cmds[0].task.assignee).toBe("worker-1");
503
+ expect(cmds[0].agentId).toBe("worker-1");
504
+ expect(cmds[1].action).toBe("bridge-task-assigned");
505
+ expect(cmds[1].taskId).toBe("task-42");
506
+ expect(cmds[1].assignee).toBe("worker-1");
507
+ });
508
+
509
+ it("create_task uses input fields when tool_output is missing", () => {
510
+ const cmds = buildOpentasksBridgeCommands({
511
+ tool_name: "mcp__opentasks__create_task",
512
+ tool_input: { title: "New task", status: "open" },
513
+ });
514
+ expect(cmds).toHaveLength(1); // no assignee → no assigned command
515
+ expect(cmds[0].action).toBe("bridge-task-created");
516
+ expect(cmds[0].task.title).toBe("New task");
517
+ expect(cmds[0].task.id).toBe("");
518
+ expect(cmds[0].agentId).toBe("opentasks"); // default when no assignee
519
+ });
520
+
521
+ it("create_task returns empty when no id and no title", () => {
522
+ const cmds = buildOpentasksBridgeCommands({
523
+ tool_name: "mcp__opentasks__create_task",
524
+ tool_input: {},
525
+ });
526
+ expect(cmds).toHaveLength(0);
527
+ });
528
+
529
+ it("create_task parses already-parsed tool_output", () => {
530
+ const cmds = buildOpentasksBridgeCommands({
531
+ tool_name: "mcp__opentasks__create_task",
532
+ tool_input: {},
533
+ tool_output: { id: "direct-1", title: "Direct", status: "open" },
534
+ });
535
+ expect(cmds[0].task.id).toBe("direct-1");
536
+ });
537
+
538
+ // ── update_task ──────────────────────────────────────────────────────
539
+
540
+ it("update_task returns bridge-task-status", () => {
541
+ const cmds = buildOpentasksBridgeCommands({
542
+ tool_name: "mcp__opentasks__update_task",
543
+ tool_input: { id: "task-1", status: "completed" },
544
+ tool_output: JSON.stringify({ content: [{ text: JSON.stringify({ id: "task-1", status: "completed", assignee: "builder" }) }] }),
545
+ });
546
+ expect(cmds).toHaveLength(1);
547
+ expect(cmds[0].action).toBe("bridge-task-status");
548
+ expect(cmds[0].taskId).toBe("task-1");
549
+ expect(cmds[0].current).toBe("completed");
550
+ expect(cmds[0].agentId).toBe("builder");
551
+ });
552
+
553
+ it("update_task uses input.transition as status fallback", () => {
554
+ const cmds = buildOpentasksBridgeCommands({
555
+ tool_name: "mcp__opentasks__update_task",
556
+ tool_input: { id: "task-2", transition: "close" },
557
+ });
558
+ expect(cmds[0].current).toBe("close");
559
+ });
560
+
561
+ it("update_task returns empty when no id", () => {
562
+ const cmds = buildOpentasksBridgeCommands({
563
+ tool_name: "mcp__opentasks__update_task",
564
+ tool_input: { status: "completed" },
565
+ });
566
+ expect(cmds).toHaveLength(0);
567
+ });
568
+
569
+ it("update_task returns empty when no status change", () => {
570
+ const cmds = buildOpentasksBridgeCommands({
571
+ tool_name: "mcp__opentasks__update_task",
572
+ tool_input: { id: "task-3", title: "Renamed" }, // no status/transition
573
+ });
574
+ expect(cmds).toHaveLength(0);
575
+ });
576
+
577
+ it("update_task sets previous to undefined when explicit status in input", () => {
578
+ const cmds = buildOpentasksBridgeCommands({
579
+ tool_name: "mcp__opentasks__update_task",
580
+ tool_input: { id: "task-1", status: "in_progress" },
581
+ });
582
+ expect(cmds[0].previous).toBeUndefined();
583
+ });
584
+
585
+ // ── link ──────────────────────────────────────────────────────────────
586
+
587
+ it("link returns emit command with task.linked payload", () => {
588
+ const cmds = buildOpentasksBridgeCommands({
589
+ tool_name: "mcp__opentasks__link",
590
+ tool_input: { fromId: "task-a", toId: "task-b", type: "blocks" },
591
+ });
592
+ expect(cmds).toHaveLength(1);
593
+ expect(cmds[0].action).toBe("emit");
594
+ expect(cmds[0].event.type).toBe("task.linked");
595
+ expect(cmds[0].event.from).toBe("task-a");
596
+ expect(cmds[0].event.to).toBe("task-b");
597
+ expect(cmds[0].event.linkType).toBe("blocks");
598
+ expect(cmds[0].event.source).toBe("opentasks");
599
+ });
600
+
601
+ it("link defaults linkType to related and remove to false", () => {
602
+ const cmds = buildOpentasksBridgeCommands({
603
+ tool_name: "mcp__opentasks__link",
604
+ tool_input: { fromId: "a", toId: "b" },
605
+ });
606
+ expect(cmds[0].event.linkType).toBe("related");
607
+ expect(cmds[0].event.remove).toBe(false);
608
+ });
609
+
610
+ it("link returns empty when fromId or toId is missing", () => {
611
+ expect(buildOpentasksBridgeCommands({
612
+ tool_name: "mcp__opentasks__link",
613
+ tool_input: { fromId: "a" },
614
+ })).toHaveLength(0);
615
+ expect(buildOpentasksBridgeCommands({
616
+ tool_name: "mcp__opentasks__link",
617
+ tool_input: { toId: "b" },
618
+ })).toHaveLength(0);
619
+ });
620
+
621
+ // ── annotate ──────────────────────────────────────────────────────────
622
+
623
+ it("annotate returns emit command with task.sync payload", () => {
624
+ const cmds = buildOpentasksBridgeCommands({
625
+ tool_name: "mcp__opentasks__annotate",
626
+ tool_input: { target: "task://1", feedback: { type: "suggestion" } },
627
+ });
628
+ expect(cmds).toHaveLength(1);
629
+ expect(cmds[0].action).toBe("emit");
630
+ expect(cmds[0].event.type).toBe("task.sync");
631
+ expect(cmds[0].event.uri).toBe("task://1");
632
+ expect(cmds[0].event.annotation).toBe("suggestion");
633
+ expect(cmds[0].event.source).toBe("opentasks");
634
+ });
635
+
636
+ it("annotate defaults annotation to comment", () => {
637
+ const cmds = buildOpentasksBridgeCommands({
638
+ tool_name: "mcp__opentasks__annotate",
639
+ tool_input: { target: "task://1" },
640
+ });
641
+ expect(cmds[0].event.annotation).toBe("comment");
642
+ });
643
+
644
+ it("annotate returns empty when target is missing", () => {
645
+ expect(buildOpentasksBridgeCommands({
646
+ tool_name: "mcp__opentasks__annotate",
647
+ tool_input: {},
648
+ })).toHaveLength(0);
306
649
  });
307
650
 
308
- it("falls back to config teamName", () => {
309
- const p = buildTaskStatusPayload({ task_id: "1" }, "fallback", null);
310
- expect(p.teamName).toBe("fallback");
651
+ // ── read-only tools ──────────────────────────────────────────────────
652
+
653
+ it("query tool returns empty array (read-only)", () => {
654
+ expect(buildOpentasksBridgeCommands({
655
+ tool_name: "mcp__opentasks__query",
656
+ tool_input: { filter: "status:open" },
657
+ })).toHaveLength(0);
311
658
  });
312
659
 
313
- it("sets role from matchedRole", () => {
314
- const p = buildTaskStatusPayload(makeTaskCompletedData(), "t", "implementer");
315
- expect(p.role).toBe("implementer");
316
- expect(p.isTeamRole).toBe(true);
660
+ it("list_tasks returns empty array (read-only)", () => {
661
+ expect(buildOpentasksBridgeCommands({
662
+ tool_name: "mcp__opentasks__list_tasks",
663
+ tool_input: {},
664
+ })).toHaveLength(0);
317
665
  });
318
666
 
319
- it("sets role to 'unknown' when no match", () => {
320
- const p = buildTaskStatusPayload(makeTaskCompletedData(), "t", null);
321
- expect(p.role).toBe("unknown");
322
- expect(p.isTeamRole).toBe(false);
667
+ it("get_task returns empty array (read-only)", () => {
668
+ expect(buildOpentasksBridgeCommands({
669
+ tool_name: "mcp__opentasks__get_task",
670
+ tool_input: { id: "task-1" },
671
+ })).toHaveLength(0);
672
+ });
673
+
674
+ // ── edge cases ───────────────────────────────────────────────────────
675
+
676
+ it("returns empty array when hook data is empty", () => {
677
+ expect(buildOpentasksBridgeCommands({})).toHaveLength(0);
678
+ });
679
+
680
+ it("handles plugin-namespaced tool names", () => {
681
+ const cmds = buildOpentasksBridgeCommands({
682
+ tool_name: "mcp__plugin_claude-code-swarm_opentasks__create_task",
683
+ tool_input: { title: "Namespaced" },
684
+ });
685
+ expect(cmds).toHaveLength(1);
686
+ expect(cmds[0].action).toBe("bridge-task-created");
323
687
  });
324
688
  });
325
689
  });