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
@@ -32,9 +32,13 @@ vi.mock("../paths.mjs", async () => {
32
32
  PID_PATH: path.join(tmpDir, "sidecar.pid"),
33
33
  MAP_DIR: path.join(tmpDir, "map"),
34
34
  SIDECAR_LOG_PATH: path.join(tmpDir, "sidecar.log"),
35
+ OPENTASKS_DIR: path.join(tmpDir, "opentasks"),
35
36
  teamDir: vi.fn((name) => `${tmpDir}/.swarm/claude-swarm/tmp/teams/${name}`),
36
37
  ensureSwarmDir: vi.fn(),
37
38
  ensureMapDir: vi.fn(),
39
+ ensureOpentasksDir: vi.fn(),
40
+ ensureSessionDir: vi.fn(),
41
+ listSessionDirs: vi.fn().mockReturnValue([]),
38
42
  pluginDir: vi.fn(() => tmpDir),
39
43
  };
40
44
  });
@@ -54,6 +58,12 @@ const mockSwarmkit = {
54
58
  initProjectPackage: vi.fn().mockResolvedValue({ package: "openteams", success: true }),
55
59
  };
56
60
 
61
+ vi.mock("../opentasks-client.mjs", () => ({
62
+ findSocketPath: vi.fn(() => "/tmp/test-daemon.sock"),
63
+ isDaemonAlive: vi.fn().mockResolvedValue(false),
64
+ ensureDaemon: vi.fn().mockResolvedValue(true),
65
+ }));
66
+
57
67
  vi.mock("../swarmkit-resolver.mjs", () => ({
58
68
  resolveSwarmkit: vi.fn().mockResolvedValue(mockSwarmkit),
59
69
  configureNodePath: vi.fn(),
@@ -63,7 +73,8 @@ const { bootstrap } = await import("../bootstrap.mjs");
63
73
  const { readConfig } = await import("../config.mjs");
64
74
  const { killSidecar, startSidecar } = await import("../sidecar-client.mjs");
65
75
  const { checkSessionlogStatus, syncSessionlog } = await import("../sessionlog.mjs");
66
- const { pluginDir } = await import("../paths.mjs");
76
+ const { pluginDir, ensureOpentasksDir, ensureSessionDir, listSessionDirs } = await import("../paths.mjs");
77
+ const { findSocketPath, isDaemonAlive, ensureDaemon } = await import("../opentasks-client.mjs");
67
78
  const { resolveSwarmkit, configureNodePath } = await import("../swarmkit-resolver.mjs");
68
79
 
69
80
  describe("bootstrap", () => {
@@ -350,4 +361,116 @@ describe("bootstrap", () => {
350
361
  expect(ensureSwarmDir).toHaveBeenCalled();
351
362
  });
352
363
  });
364
+
365
+ describe("opentasks", () => {
366
+ it("returns opentasksStatus 'disabled' when not enabled", async () => {
367
+ readConfig.mockReturnValue(makeConfig({ opentasksEnabled: false }));
368
+ const result = await bootstrap();
369
+ expect(result.opentasksStatus).toBe("disabled");
370
+ });
371
+
372
+ it("calls ensureOpentasksDir when enabled", async () => {
373
+ readConfig.mockReturnValue(makeConfig({ opentasksEnabled: true }));
374
+ await bootstrap();
375
+ expect(ensureOpentasksDir).toHaveBeenCalled();
376
+ });
377
+
378
+ it("calls ensureDaemon when enabled with autoStart", async () => {
379
+ readConfig.mockReturnValue(makeConfig({ opentasksEnabled: true, opentasksAutoStart: true }));
380
+ await bootstrap();
381
+ expect(ensureDaemon).toHaveBeenCalled();
382
+ });
383
+
384
+ it("returns 'connected' when ensureDaemon succeeds", async () => {
385
+ readConfig.mockReturnValue(makeConfig({ opentasksEnabled: true }));
386
+ ensureDaemon.mockResolvedValue(true);
387
+ const result = await bootstrap();
388
+ expect(result.opentasksStatus).toBe("connected");
389
+ });
390
+
391
+ it("returns 'starting' when ensureDaemon fails", async () => {
392
+ readConfig.mockReturnValue(makeConfig({ opentasksEnabled: true }));
393
+ ensureDaemon.mockResolvedValue(false);
394
+ const result = await bootstrap();
395
+ expect(result.opentasksStatus).toBe("starting");
396
+ });
397
+
398
+ it("calls isDaemonAlive when autoStart is false", async () => {
399
+ readConfig.mockReturnValue(makeConfig({ opentasksEnabled: true, opentasksAutoStart: false }));
400
+ isDaemonAlive.mockResolvedValue(true);
401
+ await bootstrap();
402
+ expect(isDaemonAlive).toHaveBeenCalled();
403
+ expect(ensureDaemon).not.toHaveBeenCalled();
404
+ });
405
+
406
+ it("returns 'connected' when daemon is already alive (autoStart false)", async () => {
407
+ readConfig.mockReturnValue(makeConfig({ opentasksEnabled: true, opentasksAutoStart: false }));
408
+ isDaemonAlive.mockResolvedValue(true);
409
+ const result = await bootstrap();
410
+ expect(result.opentasksStatus).toBe("connected");
411
+ });
412
+
413
+ it("returns 'not running' when daemon is not alive (autoStart false)", async () => {
414
+ readConfig.mockReturnValue(makeConfig({ opentasksEnabled: true, opentasksAutoStart: false }));
415
+ isDaemonAlive.mockResolvedValue(false);
416
+ const result = await bootstrap();
417
+ expect(result.opentasksStatus).toContain("not running");
418
+ });
419
+
420
+ it("checks opentasks package via swarmkit when enabled", async () => {
421
+ readConfig.mockReturnValue(makeConfig({ opentasksEnabled: true }));
422
+ mockSwarmkit.getInstalledVersion.mockResolvedValue("1.0.0");
423
+ await bootstrap();
424
+ expect(mockSwarmkit.getInstalledVersion).toHaveBeenCalledWith("opentasks");
425
+ });
426
+
427
+ it("does not check opentasks package when disabled", async () => {
428
+ readConfig.mockReturnValue(makeConfig({ opentasksEnabled: false }));
429
+ mockSwarmkit.getInstalledVersion.mockResolvedValue("1.0.0");
430
+ await bootstrap();
431
+ const calls = mockSwarmkit.getInstalledVersion.mock.calls.map((c) => c[0]);
432
+ expect(calls).not.toContain("opentasks");
433
+ });
434
+ });
435
+
436
+ describe("per-session sidecar", () => {
437
+ it("calls ensureSessionDir when sessionId provided and MAP enabled", async () => {
438
+ readConfig.mockReturnValue(makeConfig({ mapEnabled: true }));
439
+ await bootstrap(undefined, "session-abc");
440
+ expect(ensureSessionDir).toHaveBeenCalledWith("session-abc");
441
+ });
442
+
443
+ it("passes sessionId to killSidecar and startSidecar", async () => {
444
+ readConfig.mockReturnValue(makeConfig({ mapEnabled: true, sidecar: "session" }));
445
+ await bootstrap(undefined, "session-xyz");
446
+ expect(killSidecar).toHaveBeenCalledWith("session-xyz");
447
+ expect(startSidecar).toHaveBeenCalledWith(
448
+ expect.anything(),
449
+ expect.anything(),
450
+ "session-xyz"
451
+ );
452
+ });
453
+
454
+ it("passes sessionId to syncSessionlog", async () => {
455
+ readConfig.mockReturnValue(makeConfig({ mapEnabled: true, sessionlogSync: "full" }));
456
+ await bootstrap(undefined, "session-sync");
457
+ expect(syncSessionlog).toHaveBeenCalledWith(expect.anything(), "session-sync");
458
+ });
459
+
460
+ it("returns sessionId in result", async () => {
461
+ const result = await bootstrap(undefined, "session-ret");
462
+ expect(result.sessionId).toBe("session-ret");
463
+ });
464
+
465
+ it("returns null sessionId when not provided", async () => {
466
+ const result = await bootstrap();
467
+ expect(result.sessionId).toBeUndefined();
468
+ });
469
+
470
+ it("calls listSessionDirs during cleanup (session sidecar mode)", async () => {
471
+ readConfig.mockReturnValue(makeConfig({ mapEnabled: true, sidecar: "session" }));
472
+ await bootstrap(undefined, "session-cleanup");
473
+ expect(listSessionDirs).toHaveBeenCalled();
474
+ });
475
+ });
353
476
  });
@@ -16,7 +16,11 @@ describe("config", () => {
16
16
 
17
17
  describe("readConfig", () => {
18
18
  let tmpDir;
19
- beforeEach(() => { tmpDir = makeTmpDir(); });
19
+ let noGlobal;
20
+ beforeEach(() => {
21
+ tmpDir = makeTmpDir();
22
+ noGlobal = path.join(tmpDir, "no-global.json");
23
+ });
20
24
  afterEach(() => { cleanupTmpDir(tmpDir); });
21
25
 
22
26
  it("reads and parses a valid config file", () => {
@@ -24,7 +28,7 @@ describe("config", () => {
24
28
  template: "gsd",
25
29
  map: { enabled: true, server: "ws://example.com:9090" },
26
30
  }));
27
- const config = readConfig(configPath);
31
+ const config = readConfig(configPath, noGlobal);
28
32
  expect(config.template).toBe("gsd");
29
33
  expect(config.map.enabled).toBe(true);
30
34
  expect(config.map.server).toBe("ws://example.com:9090");
@@ -35,7 +39,7 @@ describe("config", () => {
35
39
  template: "test",
36
40
  map: { enabled: true },
37
41
  }));
38
- const config = readConfig(configPath);
42
+ const config = readConfig(configPath, noGlobal);
39
43
  expect(config.map.server).toBe(DEFAULTS.mapServer);
40
44
  expect(config.map.scope).toBe("");
41
45
  expect(config.map.systemId).toBe(DEFAULTS.mapSystemId);
@@ -44,13 +48,38 @@ describe("config", () => {
44
48
 
45
49
  it("normalizes sessionlog fields with defaults", () => {
46
50
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({ template: "test" }));
47
- const config = readConfig(configPath);
51
+ const config = readConfig(configPath, noGlobal);
48
52
  expect(config.sessionlog.enabled).toBe(false);
49
53
  expect(config.sessionlog.sync).toBe("off");
50
54
  });
51
55
 
56
+ it("normalizes opentasks fields with defaults", () => {
57
+ const configPath = writeFile(tmpDir, "config.json", JSON.stringify({ template: "test" }));
58
+ const config = readConfig(configPath, noGlobal);
59
+ expect(config.opentasks.enabled).toBe(false);
60
+ expect(config.opentasks.autoStart).toBe(true);
61
+ });
62
+
63
+ it("reads opentasks.enabled from config file", () => {
64
+ const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
65
+ template: "test",
66
+ opentasks: { enabled: true },
67
+ }));
68
+ const config = readConfig(configPath, noGlobal);
69
+ expect(config.opentasks.enabled).toBe(true);
70
+ });
71
+
72
+ it("reads opentasks.autoStart false from config file", () => {
73
+ const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
74
+ template: "test",
75
+ opentasks: { enabled: true, autoStart: false },
76
+ }));
77
+ const config = readConfig(configPath, noGlobal);
78
+ expect(config.opentasks.autoStart).toBe(false);
79
+ });
80
+
52
81
  it("returns defaults when file does not exist", () => {
53
- const config = readConfig(path.join(tmpDir, "nonexistent.json"));
82
+ const config = readConfig(path.join(tmpDir, "nonexistent.json"), noGlobal);
54
83
  expect(config.template).toBe("");
55
84
  expect(config.map.enabled).toBe(false);
56
85
  expect(config.sessionlog.enabled).toBe(false);
@@ -58,20 +87,20 @@ describe("config", () => {
58
87
 
59
88
  it("returns defaults when file contains invalid JSON", () => {
60
89
  const configPath = writeFile(tmpDir, "config.json", "not json{{{");
61
- const config = readConfig(configPath);
90
+ const config = readConfig(configPath, noGlobal);
62
91
  expect(config.template).toBe("");
63
92
  expect(config.map.enabled).toBe(false);
64
93
  });
65
94
 
66
95
  it("returns defaults when file is empty", () => {
67
96
  const configPath = writeFile(tmpDir, "config.json", "");
68
- const config = readConfig(configPath);
97
+ const config = readConfig(configPath, noGlobal);
69
98
  expect(config.template).toBe("");
70
99
  });
71
100
 
72
101
  it("handles config with only template field", () => {
73
102
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({ template: "my-team" }));
74
- const config = readConfig(configPath);
103
+ const config = readConfig(configPath, noGlobal);
75
104
  expect(config.template).toBe("my-team");
76
105
  expect(config.map.enabled).toBe(false);
77
106
  });
@@ -81,7 +110,7 @@ describe("config", () => {
81
110
  template: "test",
82
111
  map: { enabled: true, scope: "custom:scope" },
83
112
  }));
84
- const config = readConfig(configPath);
113
+ const config = readConfig(configPath, noGlobal);
85
114
  expect(config.map.scope).toBe("custom:scope");
86
115
  });
87
116
 
@@ -89,7 +118,7 @@ describe("config", () => {
89
118
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
90
119
  map: { server: "ws://example.com:9090" },
91
120
  }));
92
- const config = readConfig(configPath);
121
+ const config = readConfig(configPath, noGlobal);
93
122
  expect(config.map.enabled).toBe(true);
94
123
  });
95
124
 
@@ -97,8 +126,114 @@ describe("config", () => {
97
126
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
98
127
  template: "test",
99
128
  }));
100
- const config = readConfig(configPath);
129
+ const config = readConfig(configPath, noGlobal);
130
+ expect(config.map.enabled).toBe(false);
131
+ });
132
+ });
133
+
134
+ describe("global config fallthrough", () => {
135
+ let tmpDir;
136
+ beforeEach(() => { tmpDir = makeTmpDir(); });
137
+ afterEach(() => { cleanupTmpDir(tmpDir); });
138
+
139
+ it("uses global config values when project config is missing", () => {
140
+ const globalPath = writeFile(tmpDir, "global.json", JSON.stringify({
141
+ map: { server: "ws://global-server:9090", sidecar: "persistent" },
142
+ sessionlog: { sync: "full" },
143
+ }));
144
+ const projectPath = path.join(tmpDir, "nonexistent.json");
145
+ const config = readConfig(projectPath, globalPath);
146
+ expect(config.map.server).toBe("ws://global-server:9090");
147
+ expect(config.map.sidecar).toBe("persistent");
148
+ expect(config.sessionlog.sync).toBe("full");
149
+ });
150
+
151
+ it("project config overrides global config", () => {
152
+ const globalPath = writeFile(tmpDir, "global.json", JSON.stringify({
153
+ map: { server: "ws://global-server:9090", sidecar: "persistent" },
154
+ sessionlog: { sync: "full" },
155
+ }));
156
+ const projectPath = writeFile(tmpDir, "project.json", JSON.stringify({
157
+ map: { server: "ws://project-server:8080" },
158
+ sessionlog: { sync: "lifecycle" },
159
+ }));
160
+ const config = readConfig(projectPath, globalPath);
161
+ expect(config.map.server).toBe("ws://project-server:8080");
162
+ expect(config.map.sidecar).toBe("persistent"); // falls through from global
163
+ expect(config.sessionlog.sync).toBe("lifecycle");
164
+ });
165
+
166
+ it("project template overrides global template", () => {
167
+ const globalPath = writeFile(tmpDir, "global.json", JSON.stringify({
168
+ template: "global-template",
169
+ }));
170
+ const projectPath = writeFile(tmpDir, "project.json", JSON.stringify({
171
+ template: "project-template",
172
+ }));
173
+ const config = readConfig(projectPath, globalPath);
174
+ expect(config.template).toBe("project-template");
175
+ });
176
+
177
+ it("global template is used when project has no template", () => {
178
+ const globalPath = writeFile(tmpDir, "global.json", JSON.stringify({
179
+ template: "global-template",
180
+ }));
181
+ const projectPath = writeFile(tmpDir, "project.json", JSON.stringify({}));
182
+ const config = readConfig(projectPath, globalPath);
183
+ expect(config.template).toBe("global-template");
184
+ });
185
+
186
+ it("global map.enabled enables MAP when project has no map config", () => {
187
+ const globalPath = writeFile(tmpDir, "global.json", JSON.stringify({
188
+ map: { enabled: true, server: "ws://global-server:9090" },
189
+ }));
190
+ const projectPath = path.join(tmpDir, "nonexistent.json");
191
+ const config = readConfig(projectPath, globalPath);
192
+ expect(config.map.enabled).toBe(true);
193
+ expect(config.map.server).toBe("ws://global-server:9090");
194
+ });
195
+
196
+ it("global map.server implicitly enables MAP when project has no config", () => {
197
+ const globalPath = writeFile(tmpDir, "global.json", JSON.stringify({
198
+ map: { server: "ws://global-server:9090" },
199
+ }));
200
+ const projectPath = path.join(tmpDir, "nonexistent.json");
201
+ const config = readConfig(projectPath, globalPath);
202
+ expect(config.map.enabled).toBe(true);
203
+ });
204
+
205
+ it("global sessionlog.enabled is used when project has no sessionlog config", () => {
206
+ const globalPath = writeFile(tmpDir, "global.json", JSON.stringify({
207
+ sessionlog: { enabled: true, sync: "metrics" },
208
+ }));
209
+ const projectPath = path.join(tmpDir, "nonexistent.json");
210
+ const config = readConfig(projectPath, globalPath);
211
+ expect(config.sessionlog.enabled).toBe(true);
212
+ expect(config.sessionlog.sync).toBe("metrics");
213
+ });
214
+
215
+ it("falls back to defaults when both configs are missing", () => {
216
+ const globalPath = path.join(tmpDir, "nonexistent-global.json");
217
+ const projectPath = path.join(tmpDir, "nonexistent-project.json");
218
+ const config = readConfig(projectPath, globalPath);
219
+ expect(config.template).toBe("");
101
220
  expect(config.map.enabled).toBe(false);
221
+ expect(config.map.server).toBe(DEFAULTS.mapServer);
222
+ expect(config.sessionlog.enabled).toBe(false);
223
+ expect(config.sessionlog.sync).toBe(DEFAULTS.sessionlogSync);
224
+ });
225
+
226
+ it("per-field fallthrough: project scope + global server", () => {
227
+ const globalPath = writeFile(tmpDir, "global.json", JSON.stringify({
228
+ map: { server: "ws://global-server:9090", systemId: "global-system" },
229
+ }));
230
+ const projectPath = writeFile(tmpDir, "project.json", JSON.stringify({
231
+ map: { scope: "my-project" },
232
+ }));
233
+ const config = readConfig(projectPath, globalPath);
234
+ expect(config.map.scope).toBe("my-project");
235
+ expect(config.map.server).toBe("ws://global-server:9090");
236
+ expect(config.map.systemId).toBe("global-system");
102
237
  });
103
238
  });
104
239
 
@@ -122,10 +257,12 @@ describe("config", () => {
122
257
 
123
258
  describe("env var overrides", () => {
124
259
  let tmpDir;
260
+ let noGlobal;
125
261
  const savedEnv = {};
126
262
 
127
263
  beforeEach(() => {
128
264
  tmpDir = makeTmpDir();
265
+ noGlobal = path.join(tmpDir, "no-global.json");
129
266
  for (const key of Object.keys(process.env)) {
130
267
  if (key.startsWith("SWARM_")) {
131
268
  savedEnv[key] = process.env[key];
@@ -152,7 +289,7 @@ describe("config", () => {
152
289
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
153
290
  template: "file-template",
154
291
  }));
155
- const config = readConfig(configPath);
292
+ const config = readConfig(configPath, noGlobal);
156
293
  expect(config.template).toBe("env-template");
157
294
  });
158
295
 
@@ -161,7 +298,7 @@ describe("config", () => {
161
298
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
162
299
  map: { enabled: false },
163
300
  }));
164
- const config = readConfig(configPath);
301
+ const config = readConfig(configPath, noGlobal);
165
302
  expect(config.map.enabled).toBe(true);
166
303
  });
167
304
 
@@ -169,13 +306,13 @@ describe("config", () => {
169
306
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({}));
170
307
 
171
308
  process.env.SWARM_MAP_ENABLED = "1";
172
- expect(readConfig(configPath).map.enabled).toBe(true);
309
+ expect(readConfig(configPath, noGlobal).map.enabled).toBe(true);
173
310
 
174
311
  process.env.SWARM_MAP_ENABLED = "yes";
175
- expect(readConfig(configPath).map.enabled).toBe(true);
312
+ expect(readConfig(configPath, noGlobal).map.enabled).toBe(true);
176
313
 
177
314
  process.env.SWARM_MAP_ENABLED = "YES";
178
- expect(readConfig(configPath).map.enabled).toBe(true);
315
+ expect(readConfig(configPath, noGlobal).map.enabled).toBe(true);
179
316
  });
180
317
 
181
318
  it("SWARM_MAP_ENABLED=false overrides true in config", () => {
@@ -183,7 +320,7 @@ describe("config", () => {
183
320
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
184
321
  map: { enabled: true },
185
322
  }));
186
- expect(readConfig(configPath).map.enabled).toBe(false);
323
+ expect(readConfig(configPath, noGlobal).map.enabled).toBe(false);
187
324
  });
188
325
 
189
326
  it("SWARM_MAP_ENABLED=false disables even when server is configured", () => {
@@ -191,13 +328,13 @@ describe("config", () => {
191
328
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
192
329
  map: { server: "ws://example.com:9090" },
193
330
  }));
194
- expect(readConfig(configPath).map.enabled).toBe(false);
331
+ expect(readConfig(configPath, noGlobal).map.enabled).toBe(false);
195
332
  });
196
333
 
197
334
  it("SWARM_MAP_SERVER implicitly enables MAP", () => {
198
335
  process.env.SWARM_MAP_SERVER = "ws://env-server:9090";
199
336
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({}));
200
- const config = readConfig(configPath);
337
+ const config = readConfig(configPath, noGlobal);
201
338
  expect(config.map.enabled).toBe(true);
202
339
  expect(config.map.server).toBe("ws://env-server:9090");
203
340
  });
@@ -207,7 +344,7 @@ describe("config", () => {
207
344
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
208
345
  map: { server: "ws://file-server:8080" },
209
346
  }));
210
- expect(readConfig(configPath).map.server).toBe("ws://env-server:9090");
347
+ expect(readConfig(configPath, noGlobal).map.server).toBe("ws://env-server:9090");
211
348
  });
212
349
 
213
350
  it("SWARM_MAP_SCOPE overrides config file", () => {
@@ -215,7 +352,7 @@ describe("config", () => {
215
352
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
216
353
  map: { scope: "custom:file-scope" },
217
354
  }));
218
- expect(readConfig(configPath).map.scope).toBe("custom:env-scope");
355
+ expect(readConfig(configPath, noGlobal).map.scope).toBe("custom:env-scope");
219
356
  });
220
357
 
221
358
  it("SWARM_MAP_SYSTEM_ID overrides config file", () => {
@@ -223,7 +360,7 @@ describe("config", () => {
223
360
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
224
361
  map: { systemId: "file-system" },
225
362
  }));
226
- expect(readConfig(configPath).map.systemId).toBe("env-system");
363
+ expect(readConfig(configPath, noGlobal).map.systemId).toBe("env-system");
227
364
  });
228
365
 
229
366
  it("SWARM_MAP_SIDECAR overrides config file", () => {
@@ -231,7 +368,7 @@ describe("config", () => {
231
368
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
232
369
  map: { sidecar: "session" },
233
370
  }));
234
- expect(readConfig(configPath).map.sidecar).toBe("persistent");
371
+ expect(readConfig(configPath, noGlobal).map.sidecar).toBe("persistent");
235
372
  });
236
373
 
237
374
  it("SWARM_SESSIONLOG_ENABLED overrides config file", () => {
@@ -239,7 +376,7 @@ describe("config", () => {
239
376
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
240
377
  sessionlog: { enabled: false },
241
378
  }));
242
- expect(readConfig(configPath).sessionlog.enabled).toBe(true);
379
+ expect(readConfig(configPath, noGlobal).sessionlog.enabled).toBe(true);
243
380
  });
244
381
 
245
382
  it("SWARM_SESSIONLOG_SYNC overrides config file", () => {
@@ -247,13 +384,27 @@ describe("config", () => {
247
384
  const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
248
385
  sessionlog: { sync: "off" },
249
386
  }));
250
- expect(readConfig(configPath).sessionlog.sync).toBe("full");
387
+ expect(readConfig(configPath, noGlobal).sessionlog.sync).toBe("full");
388
+ });
389
+
390
+ it("SWARM_OPENTASKS_ENABLED overrides config file", () => {
391
+ process.env.SWARM_OPENTASKS_ENABLED = "true";
392
+ const configPath = writeFile(tmpDir, "config.json", JSON.stringify({
393
+ opentasks: { enabled: false },
394
+ }));
395
+ expect(readConfig(configPath, noGlobal).opentasks.enabled).toBe(true);
396
+ });
397
+
398
+ it("SWARM_OPENTASKS_AUTOSTART overrides default", () => {
399
+ process.env.SWARM_OPENTASKS_AUTOSTART = "false";
400
+ const configPath = writeFile(tmpDir, "config.json", JSON.stringify({}));
401
+ expect(readConfig(configPath, noGlobal).opentasks.autoStart).toBe(false);
251
402
  });
252
403
 
253
404
  it("env vars override defaults when no config file exists", () => {
254
405
  process.env.SWARM_MAP_SERVER = "ws://env-server:9090";
255
406
  process.env.SWARM_MAP_SYSTEM_ID = "env-system";
256
- const config = readConfig(path.join(tmpDir, "nonexistent.json"));
407
+ const config = readConfig(path.join(tmpDir, "nonexistent.json"), noGlobal);
257
408
  expect(config.map.enabled).toBe(true);
258
409
  expect(config.map.server).toBe("ws://env-server:9090");
259
410
  expect(config.map.systemId).toBe("env-system");
@@ -264,10 +415,32 @@ describe("config", () => {
264
415
  template: "file-template",
265
416
  map: { server: "ws://file-server:8080" },
266
417
  }));
267
- const config = readConfig(configPath);
418
+ const config = readConfig(configPath, noGlobal);
268
419
  expect(config.template).toBe("file-template");
269
420
  expect(config.map.server).toBe("ws://file-server:8080");
270
421
  });
422
+
423
+ it("env vars override global config values", () => {
424
+ process.env.SWARM_MAP_SERVER = "ws://env-server:9090";
425
+ const globalPath = writeFile(tmpDir, "global.json", JSON.stringify({
426
+ map: { server: "ws://global-server:8080" },
427
+ }));
428
+ const projectPath = path.join(tmpDir, "nonexistent.json");
429
+ const config = readConfig(projectPath, globalPath);
430
+ expect(config.map.server).toBe("ws://env-server:9090");
431
+ });
432
+
433
+ it("env vars override both project and global config", () => {
434
+ process.env.SWARM_MAP_SIDECAR = "persistent";
435
+ const globalPath = writeFile(tmpDir, "global.json", JSON.stringify({
436
+ map: { sidecar: "session" },
437
+ }));
438
+ const projectPath = writeFile(tmpDir, "project.json", JSON.stringify({
439
+ map: { sidecar: "session" },
440
+ }));
441
+ const config = readConfig(projectPath, globalPath);
442
+ expect(config.map.sidecar).toBe("persistent");
443
+ });
271
444
  });
272
445
 
273
446
  describe("resolveTeamName", () => {