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
package/README.md CHANGED
@@ -58,6 +58,71 @@ npm install -g openteams
58
58
  openteams editor
59
59
  ```
60
60
 
61
+ ## Configuration
62
+
63
+ ### Config resolution
64
+
65
+ Configuration is resolved with tiered fallthrough — each level overrides the one below:
66
+
67
+ ```
68
+ SWARM_* env vars > project config > global config > defaults
69
+ ```
70
+
71
+ ### Project config
72
+
73
+ Lives in your project directory at `.swarm/claude-swarm/config.json` (or `.claude-swarm/config.json` with `--no-prefix`):
74
+
75
+ ```json
76
+ {
77
+ "template": "get-shit-done",
78
+ "map": {
79
+ "scope": "my-project",
80
+ "systemId": "my-project-swarm"
81
+ }
82
+ }
83
+ ```
84
+
85
+ Project config is typically committed to the repo or gitignored per preference.
86
+
87
+ ### Global config
88
+
89
+ Lives at `~/.claude-swarm/config.json`. Created by `swarmkit init` or manually. Sets user-wide defaults that apply to all projects unless overridden:
90
+
91
+ ```json
92
+ {
93
+ "map": {
94
+ "server": "ws://my-map-server:8080",
95
+ "sidecar": "session",
96
+ "auth": {
97
+ "token": "my-token"
98
+ }
99
+ },
100
+ "sessionlog": {
101
+ "enabled": true,
102
+ "sync": "metrics"
103
+ }
104
+ }
105
+ ```
106
+
107
+ This is useful for settings you don't want to repeat in every project — MAP server address, auth tokens, sidecar mode, sessionlog preferences. Fields like `template` and `map.scope` are typically project-specific and belong in the project config.
108
+
109
+ ### Environment variable overrides
110
+
111
+ All config values can be overridden via `SWARM_*` environment variables:
112
+
113
+ | Config field | Environment variable |
114
+ |---|---|
115
+ | `template` | `SWARM_TEMPLATE` |
116
+ | `map.server` | `SWARM_MAP_SERVER` |
117
+ | `map.enabled` | `SWARM_MAP_ENABLED` |
118
+ | `map.scope` | `SWARM_MAP_SCOPE` |
119
+ | `map.systemId` | `SWARM_MAP_SYSTEM_ID` |
120
+ | `map.sidecar` | `SWARM_MAP_SIDECAR` |
121
+ | `map.auth.token` | `SWARM_MAP_AUTH_TOKEN` |
122
+ | `map.auth.param` | `SWARM_MAP_AUTH_PARAM` |
123
+ | `sessionlog.enabled` | `SWARM_SESSIONLOG_ENABLED` |
124
+ | `sessionlog.sync` | `SWARM_SESSIONLOG_SYNC` |
125
+
61
126
  ## How it works
62
127
 
63
128
  1. **SessionStart hook** ensures `openteams` is installed and reads your team configuration
@@ -19,11 +19,25 @@ export function killByPidFile(pidPath) {
19
19
 
20
20
  /**
21
21
  * Clean up generated artifacts and sidecar processes in a workspace.
22
+ * Kills both default and per-session sidecar processes.
22
23
  */
23
24
  export function cleanupWorkspace(dir) {
24
- // Kill MAP sidecar if running
25
- const pidPath = path.join(dir, ".swarm", "claude-swarm", "tmp", "map", "sidecar.pid");
26
- killByPidFile(pidPath);
25
+ const mapDir = path.join(dir, ".swarm", "claude-swarm", "tmp", "map");
26
+
27
+ // Kill default MAP sidecar if running
28
+ killByPidFile(path.join(mapDir, "sidecar.pid"));
29
+
30
+ // Kill all per-session sidecars
31
+ const sessionsDir = path.join(mapDir, "sessions");
32
+ try {
33
+ if (fs.existsSync(sessionsDir)) {
34
+ for (const entry of fs.readdirSync(sessionsDir)) {
35
+ killByPidFile(path.join(sessionsDir, entry, "sidecar.pid"));
36
+ }
37
+ }
38
+ } catch {
39
+ // ignore
40
+ }
27
41
 
28
42
  // Remove all generated/tmp artifacts
29
43
  const tmpDir = path.join(dir, ".swarm", "claude-swarm", "tmp");
@@ -1,23 +1,41 @@
1
1
  /**
2
- * map-mock-server.mjs — Minimal mock MAP WebSocket server for e2e tests
2
+ * map-mock-server.mjs — Protocol-compliant mock MAP WebSocket server for e2e tests
3
3
  *
4
- * Records all received messages and handles basic MAP protocol handshakes
5
- * so the sidecar doesn't crash on connection.
4
+ * Speaks enough of the MAP JSON-RPC protocol for the SDK's AgentConnection.connect()
5
+ * to succeed: handles map/connect, map/agents/register, map/agents/spawn,
6
+ * map/agents/unregister, map/agents/update, map/send, and trajectory/checkpoint.
7
+ *
8
+ * Also supports:
9
+ * - Sending inbound messages to connected clients
10
+ * - Configurable response delays for resilience testing
11
+ * - Method-specific message tracking
6
12
  */
7
13
 
8
14
  import { WebSocketServer } from "ws";
9
15
 
16
+ let _counter = 0;
17
+ function nextId() {
18
+ return `mock-${++_counter}`;
19
+ }
20
+
10
21
  export class MockMapServer {
11
22
  constructor() {
12
23
  this.wss = null;
13
24
  this.port = 0;
14
- this.receivedMessages = [];
15
25
  this.connections = [];
26
+ this.receivedMessages = [];
27
+ this.responseDelayMs = 0;
28
+ this.trajectorySupported = false;
29
+
30
+ // Method-specific tracking
31
+ this.spawnedAgents = [];
32
+ this.sentMessages = [];
33
+ this.callExtensions = [];
34
+ this.stateUpdates = [];
16
35
  }
17
36
 
18
37
  /**
19
38
  * Start the mock server on a random available port.
20
- * Returns a promise that resolves with the assigned port.
21
39
  */
22
40
  start() {
23
41
  return new Promise((resolve, reject) => {
@@ -38,7 +56,6 @@ export class MockMapServer {
38
56
  });
39
57
  this._handleMessage(ws, msg);
40
58
  } catch {
41
- // Invalid JSON, record raw
42
59
  this.receivedMessages.push({
43
60
  timestamp: Date.now(),
44
61
  raw: data.toString(),
@@ -56,30 +73,168 @@ export class MockMapServer {
56
73
  }
57
74
 
58
75
  /**
59
- * Handle incoming MAP protocol messages with minimal responses.
76
+ * Handle incoming MAP protocol messages with method-specific responses.
60
77
  */
61
- _handleMessage(ws, msg) {
62
- // JSON-RPC style: respond with result if there's an id
63
- if (msg.id !== undefined) {
64
- ws.send(
65
- JSON.stringify({
66
- jsonrpc: "2.0",
67
- id: msg.id,
68
- result: { ok: true, agentId: msg.params?.name || "mock-agent" },
69
- })
70
- );
71
- return;
78
+ async _handleMessage(ws, msg) {
79
+ if (this.responseDelayMs > 0) {
80
+ await new Promise((r) => setTimeout(r, this.responseDelayMs));
72
81
  }
73
82
 
74
- // MAP connect message
75
- if (msg.type === "connect" || msg.method === "connect") {
76
- ws.send(
77
- JSON.stringify({
83
+ const { id, method, params } = msg;
84
+
85
+ // Non-RPC messages (no id) — legacy compat
86
+ if (id === undefined) {
87
+ if (msg.type === "connect" || msg.method === "connect") {
88
+ ws.send(JSON.stringify({
78
89
  type: "connected",
79
90
  agentId: msg.name || "mock-agent",
80
- })
81
- );
91
+ }));
92
+ }
93
+ return;
82
94
  }
95
+
96
+ // JSON-RPC dispatch by method
97
+ switch (method) {
98
+ case "map/connect": {
99
+ this._respond(ws, id, {
100
+ sessionId: `session-${nextId()}`,
101
+ capabilities: {},
102
+ protocolVersion: "1.0",
103
+ });
104
+ break;
105
+ }
106
+
107
+ case "map/agents/register": {
108
+ const agent = {
109
+ id: params?.agentId || params?.name || nextId(),
110
+ state: "idle",
111
+ name: params?.name || "agent",
112
+ role: params?.role || "agent",
113
+ };
114
+ this._respond(ws, id, { agent });
115
+ break;
116
+ }
117
+
118
+ case "map/agents/spawn": {
119
+ const agent = {
120
+ id: params?.agentId || nextId(),
121
+ state: "idle",
122
+ name: params?.name || "spawned",
123
+ role: params?.role || "agent",
124
+ };
125
+ this.spawnedAgents.push({ ...params, _timestamp: Date.now() });
126
+ this._respond(ws, id, { agent });
127
+ break;
128
+ }
129
+
130
+ case "map/agents/unregister": {
131
+ this.callExtensions.push({
132
+ method: "map/agents/unregister",
133
+ params,
134
+ _timestamp: Date.now(),
135
+ });
136
+ this._respond(ws, id, { ok: true });
137
+ break;
138
+ }
139
+
140
+ case "map/agents/update": {
141
+ this.stateUpdates.push({ ...params, _timestamp: Date.now() });
142
+ this._respond(ws, id, { ok: true });
143
+ break;
144
+ }
145
+
146
+ case "map/send": {
147
+ this.sentMessages.push({ ...params, _timestamp: Date.now() });
148
+ this._respond(ws, id, { messageId: nextId() });
149
+ break;
150
+ }
151
+
152
+ case "trajectory/checkpoint": {
153
+ if (this.trajectorySupported) {
154
+ this._respond(ws, id, { ok: true });
155
+ } else {
156
+ this._respondError(ws, id, -32601, "Method not found: trajectory/checkpoint");
157
+ }
158
+ break;
159
+ }
160
+
161
+ default: {
162
+ // Check if it's a callExtension-style method
163
+ if (method) {
164
+ this.callExtensions.push({ method, params, _timestamp: Date.now() });
165
+ this._respond(ws, id, { ok: true });
166
+ } else {
167
+ this._respondError(ws, id, -32601, `Unknown method: ${method}`);
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ _respond(ws, id, result) {
174
+ try {
175
+ ws.send(JSON.stringify({ jsonrpc: "2.0", id, result }));
176
+ } catch { /* client gone */ }
177
+ }
178
+
179
+ _respondError(ws, id, code, message) {
180
+ try {
181
+ ws.send(JSON.stringify({
182
+ jsonrpc: "2.0",
183
+ id,
184
+ error: { code, message },
185
+ }));
186
+ } catch { /* client gone */ }
187
+ }
188
+
189
+ /**
190
+ * Send a MAP message notification to all connected clients.
191
+ * Simulates an external agent sending a message into the scope.
192
+ */
193
+ sendToAll(payload, meta = {}) {
194
+ const notification = this._buildMessageNotification(payload, meta);
195
+ for (const ws of this.connections) {
196
+ try {
197
+ ws.send(notification);
198
+ } catch { /* ignore */ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Send a MAP message notification to a specific client.
204
+ */
205
+ sendToClient(ws, payload, meta = {}) {
206
+ try {
207
+ ws.send(this._buildMessageNotification(payload, meta));
208
+ } catch { /* ignore */ }
209
+ }
210
+
211
+ /**
212
+ * Build a MAP message notification in the exact JSON-RPC 2.0 format
213
+ * the SDK expects: no top-level `id` (notification, not request),
214
+ * method "map/message", params.message with id/from/to/timestamp/payload/meta.
215
+ */
216
+ _buildMessageNotification(payload, meta = {}) {
217
+ return JSON.stringify({
218
+ jsonrpc: "2.0",
219
+ method: "map/message",
220
+ params: {
221
+ message: {
222
+ id: nextId(),
223
+ from: meta.from || "external-agent",
224
+ to: meta.to || { scope: "default" },
225
+ timestamp: new Date().toISOString(),
226
+ payload,
227
+ meta,
228
+ },
229
+ },
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Set response delay for resilience testing.
235
+ */
236
+ setResponseDelay(ms) {
237
+ this.responseDelayMs = ms;
83
238
  }
84
239
 
85
240
  /**
@@ -95,8 +250,19 @@ export class MockMapServer {
95
250
  );
96
251
  }
97
252
 
253
+ /**
254
+ * Get received messages filtered by JSON-RPC method.
255
+ */
256
+ getByMethod(method) {
257
+ return this.receivedMessages.filter((m) => m.data?.method === method);
258
+ }
259
+
98
260
  clearMessages() {
99
261
  this.receivedMessages = [];
262
+ this.spawnedAgents = [];
263
+ this.sentMessages = [];
264
+ this.callExtensions = [];
265
+ this.stateUpdates = [];
100
266
  }
101
267
 
102
268
  /**
@@ -104,15 +270,25 @@ export class MockMapServer {
104
270
  */
105
271
  stop() {
106
272
  return new Promise((resolve) => {
273
+ // Force-terminate all connections immediately to prevent
274
+ // auto-reconnecting clients from keeping the server alive.
107
275
  for (const ws of this.connections) {
108
276
  try {
109
- ws.close();
277
+ ws.terminate();
110
278
  } catch {
111
279
  // ignore
112
280
  }
113
281
  }
282
+ this.connections = [];
283
+
114
284
  if (this.wss) {
285
+ // Reject any new connections that arrive during shutdown
286
+ this.wss.on("connection", (ws) => {
287
+ try { ws.terminate(); } catch { /* ignore */ }
288
+ });
115
289
  this.wss.close(() => resolve());
290
+ // Safety timeout — resolve after 3s even if wss.close() hangs
291
+ setTimeout(resolve, 3000);
116
292
  } else {
117
293
  resolve();
118
294
  }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * sidecar.mjs — Test helper for starting/stopping real sidecar processes
3
+ *
4
+ * Spawns the actual map-sidecar.mjs as a detached child process,
5
+ * waits for sockets to appear, and provides cleanup.
6
+ */
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import net from "net";
11
+ import { spawn } from "child_process";
12
+ import { fileURLToPath } from "url";
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const PLUGIN_DIR = path.resolve(__dirname, "../..");
16
+ const SIDECAR_SCRIPT = path.join(PLUGIN_DIR, "scripts", "map-sidecar.mjs");
17
+
18
+ /**
19
+ * Send a command to a UNIX socket and return the parsed response.
20
+ * Longer timeout than production (5s) for test reliability.
21
+ */
22
+ export function sendCommand(socketPath, command, timeoutMs = 5000) {
23
+ return new Promise((resolve) => {
24
+ const client = net.createConnection(socketPath, () => {
25
+ client.write(JSON.stringify(command) + "\n");
26
+ });
27
+ let buffer = "";
28
+ client.on("data", (data) => {
29
+ buffer += data.toString();
30
+ const idx = buffer.indexOf("\n");
31
+ if (idx !== -1) {
32
+ const line = buffer.slice(0, idx);
33
+ client.end();
34
+ try {
35
+ resolve(JSON.parse(line));
36
+ } catch {
37
+ resolve(null);
38
+ }
39
+ }
40
+ });
41
+ client.on("error", () => resolve(null));
42
+ setTimeout(() => {
43
+ client.destroy();
44
+ resolve(null);
45
+ }, timeoutMs);
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Wait for a condition to become true, polling at intervalMs.
51
+ */
52
+ async function waitFor(predicate, timeoutMs = 10000, intervalMs = 200) {
53
+ const start = Date.now();
54
+ while (Date.now() - start < timeoutMs) {
55
+ if (await predicate()) return true;
56
+ await new Promise((r) => setTimeout(r, intervalMs));
57
+ }
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * Compute the expected socket paths for a sidecar running in a workspace.
63
+ * Mirrors the path resolution in src/paths.mjs when .swarm/claude-swarm/ exists.
64
+ */
65
+ function getSocketPaths(workspaceDir, sessionId) {
66
+ // Resolve symlinks (macOS: /tmp -> /private/tmp) so paths match the sidecar's process.cwd()
67
+ const realDir = fs.realpathSync(workspaceDir);
68
+ const mapDir = path.join(realDir, ".swarm", "claude-swarm", "tmp", "map");
69
+ if (sessionId) {
70
+ const dir = path.join(mapDir, "sessions", sessionId);
71
+ return {
72
+ socketPath: path.join(dir, "sidecar.sock"),
73
+ inboxSocketPath: path.join(dir, "inbox.sock"),
74
+ pidPath: path.join(dir, "sidecar.pid"),
75
+ mapDir,
76
+ };
77
+ }
78
+ return {
79
+ socketPath: path.join(mapDir, "sidecar.sock"),
80
+ inboxSocketPath: path.join(mapDir, "inbox.sock"),
81
+ pidPath: path.join(mapDir, "sidecar.pid"),
82
+ mapDir,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Start the real sidecar process for testing.
88
+ *
89
+ * @param {object} options
90
+ * @param {string} options.workspaceDir - workspace with .swarm/claude-swarm/ directory
91
+ * @param {number} options.mockServerPort - port of the mock MAP server
92
+ * @param {string} [options.scope] - MAP scope (default: "swarm:test")
93
+ * @param {string} [options.systemId] - system ID (default: "system-test")
94
+ * @param {string} [options.sessionId] - per-session sidecar ID
95
+ * @param {object} [options.inboxConfig] - inbox config object (passed as --inbox-config JSON)
96
+ * @param {number} [options.inactivityTimeoutMs] - inactivity timeout override
97
+ * @returns {Promise<object>} handle with socketPath, pid, cleanup, etc.
98
+ */
99
+ export async function startTestSidecar(options) {
100
+ const {
101
+ workspaceDir,
102
+ mockServerPort,
103
+ scope = "swarm:test",
104
+ systemId = "system-test",
105
+ sessionId,
106
+ inboxConfig,
107
+ inactivityTimeoutMs,
108
+ } = options;
109
+
110
+ const paths = getSocketPaths(workspaceDir, sessionId);
111
+
112
+ // Ensure map directory exists
113
+ fs.mkdirSync(path.dirname(paths.pidPath), { recursive: true });
114
+
115
+ // Build CLI args
116
+ const args = [
117
+ SIDECAR_SCRIPT,
118
+ "--server", `ws://localhost:${mockServerPort}`,
119
+ "--scope", scope,
120
+ "--system-id", systemId,
121
+ ];
122
+ if (sessionId) {
123
+ args.push("--session-id", sessionId);
124
+ }
125
+ if (inboxConfig) {
126
+ args.push("--inbox-config", JSON.stringify(inboxConfig));
127
+ }
128
+ if (inactivityTimeoutMs) {
129
+ args.push("--inactivity-timeout", String(inactivityTimeoutMs));
130
+ }
131
+
132
+ // Spawn detached
133
+ const child = spawn("node", args, {
134
+ detached: true,
135
+ stdio: ["ignore", "ignore", "pipe"],
136
+ cwd: workspaceDir,
137
+ });
138
+ child.unref();
139
+
140
+ // Collect stderr for debugging
141
+ let stderr = "";
142
+ child.stderr.on("data", (data) => {
143
+ stderr += data.toString();
144
+ });
145
+
146
+ // Write PID file
147
+ fs.writeFileSync(paths.pidPath, String(child.pid));
148
+
149
+ // Wait for lifecycle socket to appear and respond to ping
150
+ const lifecycleReady = await waitFor(async () => {
151
+ if (!fs.existsSync(paths.socketPath)) return false;
152
+ const resp = await sendCommand(paths.socketPath, { action: "ping" });
153
+ return resp?.ok === true;
154
+ }, 15000);
155
+
156
+ if (!lifecycleReady) {
157
+ // Try to clean up
158
+ try { process.kill(child.pid, "SIGTERM"); } catch { /* ignore */ }
159
+ throw new Error(
160
+ `Sidecar lifecycle socket not ready after 15s.\nStderr: ${stderr}`
161
+ );
162
+ }
163
+
164
+ // If inbox is configured, wait for inbox socket too
165
+ let inboxReady = false;
166
+ if (inboxConfig) {
167
+ inboxReady = await waitFor(
168
+ () => fs.existsSync(paths.inboxSocketPath),
169
+ 10000
170
+ );
171
+ }
172
+
173
+ return {
174
+ pid: child.pid,
175
+ child,
176
+ stderr: () => stderr,
177
+ ...paths,
178
+ inboxReady,
179
+ cleanup: () => stopTestSidecar({ pid: child.pid, ...paths }),
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Stop a test sidecar and clean up.
185
+ */
186
+ export async function stopTestSidecar(handle) {
187
+ const { pid, socketPath, inboxSocketPath, pidPath } = handle;
188
+
189
+ // Send SIGTERM
190
+ try {
191
+ process.kill(pid, "SIGTERM");
192
+ } catch {
193
+ // Already dead
194
+ }
195
+
196
+ // Wait for process to die
197
+ await waitFor(() => {
198
+ try {
199
+ process.kill(pid, 0);
200
+ return false;
201
+ } catch {
202
+ return true;
203
+ }
204
+ }, 5000);
205
+
206
+ // Clean up files
207
+ for (const f of [socketPath, inboxSocketPath, pidPath]) {
208
+ try { fs.unlinkSync(f); } catch { /* ignore */ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Check if a process is alive.
214
+ */
215
+ export function isProcessAlive(pid) {
216
+ try {
217
+ process.kill(pid, 0);
218
+ return true;
219
+ } catch {
220
+ return false;
221
+ }
222
+ }
@@ -17,9 +17,10 @@ export function createWorkspace(options = {}) {
17
17
  config = undefined,
18
18
  gitInit = true,
19
19
  files = {},
20
+ tmpdir = os.tmpdir(),
20
21
  } = options;
21
22
 
22
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
23
+ const dir = fs.mkdtempSync(path.join(tmpdir, prefix));
23
24
 
24
25
  if (gitInit) {
25
26
  execSync("git init -q", { cwd: dir, stdio: "ignore" });