swarmkit 0.0.2 → 0.0.3

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.
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, statSync, realpathSync, } from "node:fs";
2
+ import { mkdirSync, rmSync, existsSync, lstatSync, readFileSync, readlinkSync, writeFileSync, realpathSync, } from "node:fs";
3
3
  import { join, basename } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { randomUUID } from "node:crypto";
@@ -19,7 +19,7 @@ vi.mock("node:os", async () => {
19
19
  const actual = await import("node:os");
20
20
  return { ...actual, homedir: () => testHome };
21
21
  });
22
- const { PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } = await import("./setup.js");
22
+ const { PROJECT_CONFIG_DIRS, FLAT_PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } = await import("./setup.js");
23
23
  // ─── Helpers ─────────────────────────────────────────────────────────────────
24
24
  let testDir;
25
25
  beforeEach(() => {
@@ -58,6 +58,7 @@ function projectCtx(overrides = {}) {
58
58
  packages: overrides.packages ?? [],
59
59
  embeddingProvider: overrides.embeddingProvider ?? null,
60
60
  apiKeys: overrides.apiKeys ?? {},
61
+ usePrefix: overrides.usePrefix ?? true,
61
62
  };
62
63
  }
63
64
  function globalCtx(overrides = {}) {
@@ -69,13 +70,16 @@ function globalCtx(overrides = {}) {
69
70
  }
70
71
  // ─── Constants ───────────────────────────────────────────────────────────────
71
72
  describe("constants", () => {
72
- it("PROJECT_INIT_ORDER is opentasks → minimem → cognitive-core → macro-agent → sdr", () => {
73
+ it("PROJECT_INIT_ORDER includes all project-level packages", () => {
73
74
  expect(PROJECT_INIT_ORDER).toEqual([
74
75
  "opentasks",
75
76
  "minimem",
76
77
  "cognitive-core",
77
- "macro-agent",
78
+ "skill-tree",
78
79
  "self-driving-repo",
80
+ "openteams",
81
+ "sessionlog",
82
+ "claude-code-swarm",
79
83
  ]);
80
84
  });
81
85
  it("every ordered package has a config dir mapping", () => {
@@ -84,7 +88,7 @@ describe("constants", () => {
84
88
  }
85
89
  });
86
90
  it("GLOBAL_CONFIG_DIRS maps all global packages", () => {
87
- expect(Object.keys(GLOBAL_CONFIG_DIRS).sort()).toEqual(["agent-iam", "openhive", "openswarm", "skill-tree"]);
91
+ expect(Object.keys(GLOBAL_CONFIG_DIRS).sort()).toEqual(["openhive", "skill-tree"]);
88
92
  });
89
93
  it("no overlap between project and global config dirs", () => {
90
94
  const projectDirs = new Set(Object.values(PROJECT_CONFIG_DIRS));
@@ -101,7 +105,11 @@ describe("isProjectInit", () => {
101
105
  expect(isProjectInit(testDir, pkg)).toBe(false);
102
106
  }
103
107
  });
104
- it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists", (pkg, configDir) => {
108
+ it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (prefixed)", (pkg, configDir) => {
109
+ mkdirSync(join(testDir, configDir), { recursive: true });
110
+ expect(isProjectInit(testDir, pkg)).toBe(true);
111
+ });
112
+ it.each(Object.entries(FLAT_PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (flat)", (pkg, configDir) => {
105
113
  mkdirSync(join(testDir, configDir), { recursive: true });
106
114
  expect(isProjectInit(testDir, pkg)).toBe(true);
107
115
  });
@@ -130,18 +138,24 @@ describe("isGlobalInit", () => {
130
138
  // ─── Project: opentasks (real CLI) ───────────────────────────────────────────
131
139
  describe("initProjectPackage — opentasks (real CLI)", async () => {
132
140
  const installed = await hasCliInstalled("opentasks");
133
- it.skipIf(!installed)("runs opentasks init and creates .opentasks/", async () => {
141
+ it.skipIf(!installed)("runs opentasks init, relocates to .swarm/, and creates symlink", async () => {
134
142
  createProject("test-opentasks");
135
143
  const result = await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
136
144
  expect(result.success).toBe(true);
137
145
  expect(result.package).toBe("opentasks");
138
- // Verify real filesystem artifacts
139
- expect(existsSync(join(testDir, ".opentasks"))).toBe(true);
146
+ // Verify real filesystem artifacts in .swarm/
147
+ expect(existsSync(join(testDir, ".swarm", "opentasks"))).toBe(true);
148
+ expect(existsSync(join(testDir, ".swarm", "opentasks", "config.json"))).toBe(true);
149
+ expect(existsSync(join(testDir, ".swarm", "opentasks", "graph.jsonl"))).toBe(true);
150
+ expect(existsSync(join(testDir, ".swarm", "opentasks", ".gitignore"))).toBe(true);
151
+ // Verify symlink at legacy location
152
+ const link = join(testDir, ".opentasks");
153
+ expect(lstatSync(link).isSymbolicLink()).toBe(true);
154
+ expect(readlinkSync(link)).toBe(".swarm/opentasks");
155
+ // Accessible via symlink
140
156
  expect(existsSync(join(testDir, ".opentasks", "config.json"))).toBe(true);
141
- expect(existsSync(join(testDir, ".opentasks", "graph.jsonl"))).toBe(true);
142
- expect(existsSync(join(testDir, ".opentasks", ".gitignore"))).toBe(true);
143
157
  // Verify config content
144
- const config = JSON.parse(readFileSync(join(testDir, ".opentasks", "config.json"), "utf-8"));
158
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
145
159
  expect(config.version).toBe("1.0");
146
160
  expect(config.location.name).toBe("test-opentasks");
147
161
  expect(config.location.hash).toBeDefined();
@@ -150,14 +164,14 @@ describe("initProjectPackage — opentasks (real CLI)", async () => {
150
164
  it.skipIf(!installed)("uses project name from package.json", async () => {
151
165
  createProject("my-custom-name");
152
166
  await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
153
- const config = JSON.parse(readFileSync(join(testDir, ".opentasks", "config.json"), "utf-8"));
167
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
154
168
  expect(config.location.name).toBe("my-custom-name");
155
169
  });
156
170
  it.skipIf(!installed)("falls back to directory name without package.json", async () => {
157
171
  // No package.json, just a real git repo
158
172
  execSync("git init -q", { cwd: testDir });
159
173
  await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
160
- const config = JSON.parse(readFileSync(join(testDir, ".opentasks", "config.json"), "utf-8"));
174
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
161
175
  expect(config.location.name).toBe(basename(testDir));
162
176
  });
163
177
  });
@@ -169,12 +183,12 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
169
183
  const result = await initProjectPackage("minimem", projectCtx({ cwd: testDir, packages: ["minimem"] }));
170
184
  expect(result.success).toBe(true);
171
185
  // Verify real filesystem artifacts
172
- expect(existsSync(join(testDir, ".minimem"))).toBe(true);
173
- expect(existsSync(join(testDir, ".minimem", "config.json"))).toBe(true);
174
- expect(existsSync(join(testDir, ".minimem", ".gitignore"))).toBe(true);
186
+ expect(existsSync(join(testDir, ".swarm", "minimem"))).toBe(true);
187
+ expect(existsSync(join(testDir, ".swarm", "minimem", "config.json"))).toBe(true);
188
+ expect(existsSync(join(testDir, ".swarm", "minimem", ".gitignore"))).toBe(true);
175
189
  expect(existsSync(join(testDir, "MEMORY.md"))).toBe(true);
176
190
  // Verify default config
177
- const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
191
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
178
192
  expect(config.embedding.provider).toBe("auto");
179
193
  expect(config.hybrid.enabled).toBe(true);
180
194
  });
@@ -185,7 +199,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
185
199
  packages: ["minimem"],
186
200
  embeddingProvider: "openai",
187
201
  }));
188
- const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
202
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
189
203
  expect(config.embedding.provider).toBe("openai");
190
204
  // Other fields preserved
191
205
  expect(config.hybrid.enabled).toBe(true);
@@ -198,7 +212,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
198
212
  packages: ["minimem"],
199
213
  embeddingProvider: "gemini",
200
214
  }));
201
- const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
215
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
202
216
  expect(config.embedding.provider).toBe("gemini");
203
217
  });
204
218
  it.skipIf(!installed)("does NOT patch when provider is local", async () => {
@@ -208,7 +222,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
208
222
  packages: ["minimem"],
209
223
  embeddingProvider: "local",
210
224
  }));
211
- const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
225
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
212
226
  expect(config.embedding.provider).toBe("auto");
213
227
  });
214
228
  it.skipIf(!installed)("does NOT patch when provider is null", async () => {
@@ -218,7 +232,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
218
232
  packages: ["minimem"],
219
233
  embeddingProvider: null,
220
234
  }));
221
- const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
235
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
222
236
  expect(config.embedding.provider).toBe("auto");
223
237
  });
224
238
  });
@@ -230,53 +244,11 @@ describe("initProjectPackage — cognitive-core (real CLI)", async () => {
230
244
  const result = await initProjectPackage("cognitive-core", projectCtx({ cwd: testDir, packages: ["cognitive-core"] }));
231
245
  expect(result.success).toBe(true);
232
246
  // Verify directory structure
233
- expect(existsSync(join(testDir, ".cognitive-core"))).toBe(true);
234
- expect(existsSync(join(testDir, ".cognitive-core", "experiences"))).toBe(true);
235
- expect(existsSync(join(testDir, ".cognitive-core", "playbooks"))).toBe(true);
236
- expect(existsSync(join(testDir, ".cognitive-core", "meta-strategies"))).toBe(true);
237
- expect(existsSync(join(testDir, ".cognitive-core", "meta-observations"))).toBe(true);
238
- });
239
- });
240
- // ─── Project: macro-agent (direct write — no CLI) ───────────────────────────
241
- describe("initProjectPackage — macro-agent (direct config write)", () => {
242
- it("creates .multiagent/config.json", async () => {
243
- const result = await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent"] }));
244
- expect(result.success).toBe(true);
245
- expect(existsSync(join(testDir, ".multiagent", "config.json"))).toBe(true);
246
- });
247
- it("config has correct defaults", async () => {
248
- await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent"] }));
249
- const config = JSON.parse(readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8"));
250
- expect(config).toEqual({
251
- team: "default",
252
- port: 3001,
253
- host: "localhost",
254
- });
255
- });
256
- it("wires opentasks as task backend when opentasks is selected", async () => {
257
- await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent", "opentasks"] }));
258
- const config = JSON.parse(readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8"));
259
- expect(config.task).toEqual({
260
- backend: "opentasks",
261
- opentasks: { auto_start: true },
262
- });
263
- });
264
- it("omits task backend when opentasks not selected", async () => {
265
- await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent"] }));
266
- const config = JSON.parse(readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8"));
267
- expect(config.task).toBeUndefined();
268
- });
269
- it("is idempotent — second run overwrites cleanly", async () => {
270
- await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent", "opentasks"] }));
271
- await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent"] }));
272
- const config = JSON.parse(readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8"));
273
- expect(config.task).toBeUndefined();
274
- });
275
- it("produces valid JSON with trailing newline", async () => {
276
- await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent", "opentasks"] }));
277
- const raw = readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8");
278
- expect(() => JSON.parse(raw)).not.toThrow();
279
- expect(raw.endsWith("\n")).toBe(true);
247
+ expect(existsSync(join(testDir, ".swarm", "cognitive-core"))).toBe(true);
248
+ expect(existsSync(join(testDir, ".swarm", "cognitive-core", "experiences"))).toBe(true);
249
+ expect(existsSync(join(testDir, ".swarm", "cognitive-core", "playbooks"))).toBe(true);
250
+ expect(existsSync(join(testDir, ".swarm", "cognitive-core", "meta-strategies"))).toBe(true);
251
+ expect(existsSync(join(testDir, ".swarm", "cognitive-core", "meta-observations"))).toBe(true);
280
252
  });
281
253
  });
282
254
  // ─── Project: self-driving-repo ──────────────────────────────────────────────
@@ -287,7 +259,7 @@ describe("initProjectPackage — self-driving-repo", async () => {
287
259
  createProject("test-sdr");
288
260
  const result = await initProjectPackage("self-driving-repo", projectCtx({ cwd: testDir, packages: ["self-driving-repo"] }));
289
261
  expect(result.success).toBe(true);
290
- expect(existsSync(join(testDir, ".self-driving"))).toBe(true);
262
+ expect(existsSync(join(testDir, ".swarm", "self-driving"))).toBe(true);
291
263
  });
292
264
  it.skipIf(installed)("returns failure when sdr CLI is not installed", async () => {
293
265
  const result = await initProjectPackage("self-driving-repo", projectCtx({ cwd: testDir, packages: ["self-driving-repo"] }));
@@ -296,6 +268,134 @@ describe("initProjectPackage — self-driving-repo", async () => {
296
268
  expect(result.message).toBeDefined();
297
269
  });
298
270
  });
271
+ // ─── Project: skill-tree (direct directory creation — no CLI) ────────────────
272
+ describe("initProjectPackage — skill-tree", () => {
273
+ it("creates .swarm/skilltree/ with skills/ subdirectory (prefixed)", async () => {
274
+ const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
275
+ expect(result.success).toBe(true);
276
+ expect(result.package).toBe("skill-tree");
277
+ expect(existsSync(join(testDir, ".swarm", "skilltree"))).toBe(true);
278
+ expect(existsSync(join(testDir, ".swarm", "skilltree", "skills"))).toBe(true);
279
+ });
280
+ it("creates .skilltree/ with skills/ subdirectory (flat)", async () => {
281
+ const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"], usePrefix: false }));
282
+ expect(result.success).toBe(true);
283
+ expect(existsSync(join(testDir, ".skilltree"))).toBe(true);
284
+ expect(existsSync(join(testDir, ".skilltree", "skills"))).toBe(true);
285
+ });
286
+ it("is idempotent", async () => {
287
+ await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
288
+ const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
289
+ expect(result.success).toBe(true);
290
+ });
291
+ });
292
+ // ─── Project: openteams (direct directory creation — no CLI) ─────────────────
293
+ describe("initProjectPackage — openteams", () => {
294
+ it("creates .swarm/openteams/ with config.json (prefixed)", async () => {
295
+ const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
296
+ expect(result.success).toBe(true);
297
+ expect(result.package).toBe("openteams");
298
+ expect(existsSync(join(testDir, ".swarm", "openteams"))).toBe(true);
299
+ expect(existsSync(join(testDir, ".swarm", "openteams", "config.json"))).toBe(true);
300
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "openteams", "config.json"), "utf-8"));
301
+ expect(config).toEqual({});
302
+ });
303
+ it("creates .openteams/ with config.json (flat)", async () => {
304
+ const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"], usePrefix: false }));
305
+ expect(result.success).toBe(true);
306
+ expect(existsSync(join(testDir, ".openteams"))).toBe(true);
307
+ expect(existsSync(join(testDir, ".openteams", "config.json"))).toBe(true);
308
+ });
309
+ it("does not overwrite existing config.json", async () => {
310
+ mkdirSync(join(testDir, ".swarm", "openteams"), { recursive: true });
311
+ writeFileSync(join(testDir, ".swarm", "openteams", "config.json"), JSON.stringify({ defaults: { include: ["gsd"] } }));
312
+ const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
313
+ expect(result.success).toBe(true);
314
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "openteams", "config.json"), "utf-8"));
315
+ expect(config.defaults.include).toEqual(["gsd"]);
316
+ });
317
+ it("is idempotent", async () => {
318
+ await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
319
+ const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
320
+ expect(result.success).toBe(true);
321
+ });
322
+ });
323
+ // ─── Project: sessionlog (direct directory creation — no CLI) ────────────────
324
+ describe("initProjectPackage — sessionlog", () => {
325
+ it("creates .swarm/sessionlog/ with settings.json (prefixed)", async () => {
326
+ const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
327
+ expect(result.success).toBe(true);
328
+ expect(result.package).toBe("sessionlog");
329
+ expect(existsSync(join(testDir, ".swarm", "sessionlog"))).toBe(true);
330
+ expect(existsSync(join(testDir, ".swarm", "sessionlog", "settings.json"))).toBe(true);
331
+ const settings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
332
+ expect(settings.enabled).toBe(false);
333
+ expect(settings.strategy).toBe("manual-commit");
334
+ });
335
+ it("creates .sessionlog/ with settings.json (flat)", async () => {
336
+ const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"], usePrefix: false }));
337
+ expect(result.success).toBe(true);
338
+ expect(existsSync(join(testDir, ".sessionlog"))).toBe(true);
339
+ expect(existsSync(join(testDir, ".sessionlog", "settings.json"))).toBe(true);
340
+ });
341
+ it("does not overwrite existing settings.json", async () => {
342
+ mkdirSync(join(testDir, ".swarm", "sessionlog"), { recursive: true });
343
+ writeFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), JSON.stringify({ enabled: true, strategy: "auto" }));
344
+ const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
345
+ expect(result.success).toBe(true);
346
+ const settings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
347
+ expect(settings.enabled).toBe(true);
348
+ expect(settings.strategy).toBe("auto");
349
+ });
350
+ it("is idempotent", async () => {
351
+ await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
352
+ const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
353
+ expect(result.success).toBe(true);
354
+ });
355
+ });
356
+ // ─── Project: claude-code-swarm (direct directory creation — no CLI) ──────────
357
+ describe("initProjectPackage — claude-code-swarm", () => {
358
+ it("creates .swarm/claude-swarm/ with config.json and .gitignore (prefixed)", async () => {
359
+ const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
360
+ expect(result.success).toBe(true);
361
+ expect(result.package).toBe("claude-code-swarm");
362
+ expect(existsSync(join(testDir, ".swarm", "claude-swarm"))).toBe(true);
363
+ expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
364
+ expect(existsSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
365
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
366
+ expect(config).toEqual({});
367
+ const gitignore = readFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
368
+ expect(gitignore).toBe("tmp/\n");
369
+ });
370
+ it("creates .claude-swarm/ with config.json and .gitignore (flat)", async () => {
371
+ const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"], usePrefix: false }));
372
+ expect(result.success).toBe(true);
373
+ expect(existsSync(join(testDir, ".claude-swarm"))).toBe(true);
374
+ expect(existsSync(join(testDir, ".claude-swarm", "config.json"))).toBe(true);
375
+ expect(existsSync(join(testDir, ".claude-swarm", ".gitignore"))).toBe(true);
376
+ });
377
+ it("does not overwrite existing config.json", async () => {
378
+ mkdirSync(join(testDir, ".swarm", "claude-swarm"), { recursive: true });
379
+ writeFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), JSON.stringify({ template: "gsd" }));
380
+ const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
381
+ expect(result.success).toBe(true);
382
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
383
+ expect(config.template).toBe("gsd");
384
+ });
385
+ it("does not overwrite existing .gitignore", async () => {
386
+ mkdirSync(join(testDir, ".swarm", "claude-swarm"), { recursive: true });
387
+ writeFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "tmp/\ncustom/\n");
388
+ const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
389
+ expect(result.success).toBe(true);
390
+ const gitignore = readFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
391
+ expect(gitignore).toBe("tmp/\ncustom/\n");
392
+ });
393
+ it("is idempotent", async () => {
394
+ await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
395
+ const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
396
+ expect(result.success).toBe(true);
397
+ });
398
+ });
299
399
  // ─── Project: unknown ────────────────────────────────────────────────────────
300
400
  describe("initProjectPackage — unknown", () => {
301
401
  it("returns failure with descriptive message", async () => {
@@ -307,61 +407,6 @@ describe("initProjectPackage — unknown", () => {
307
407
  });
308
408
  });
309
409
  });
310
- // ─── Global: agent-iam (real CLI) ────────────────────────────────────────────
311
- describe("initGlobalPackage — agent-iam (real CLI)", async () => {
312
- const installed = await hasCliInstalled("agent-iam");
313
- it.skipIf(!installed)("creates ~/.agent-credentials/ with 0700 permissions", async () => {
314
- const result = await initGlobalPackage("agent-iam", globalCtx({ packages: ["agent-iam"] }));
315
- expect(result.success).toBe(true);
316
- expect(existsSync(join(testHome, ".agent-credentials"))).toBe(true);
317
- const stats = statSync(join(testHome, ".agent-credentials"));
318
- expect(stats.mode & 0o777).toBe(0o700);
319
- });
320
- it.skipIf(!installed)("registers openai API key via real CLI", async () => {
321
- const result = await initGlobalPackage("agent-iam", globalCtx({
322
- packages: ["agent-iam"],
323
- apiKeys: { openai: "sk-test-integration-key" },
324
- }));
325
- expect(result.success).toBe(true);
326
- // Verify the key was actually stored by agent-iam
327
- const config = JSON.parse(readFileSync(join(testHome, ".agent-credentials", "config.json"), "utf-8"));
328
- expect(config.providers.apikeys.openai.apiKey).toBe("sk-test-integration-key");
329
- expect(config.providers.apikeys.openai.providerName).toBe("openai");
330
- });
331
- it.skipIf(!installed)("registers multiple API keys", async () => {
332
- await initGlobalPackage("agent-iam", globalCtx({
333
- packages: ["agent-iam"],
334
- apiKeys: {
335
- openai: "sk-o",
336
- anthropic: "sk-a",
337
- gemini: "g-1",
338
- },
339
- }));
340
- const config = JSON.parse(readFileSync(join(testHome, ".agent-credentials", "config.json"), "utf-8"));
341
- expect(config.providers.apikeys.openai).toBeDefined();
342
- expect(config.providers.apikeys.anthropic).toBeDefined();
343
- expect(config.providers.apikeys.gemini).toBeDefined();
344
- });
345
- it.skipIf(!installed)("ignores non-standard provider keys", async () => {
346
- await initGlobalPackage("agent-iam", globalCtx({
347
- packages: ["agent-iam"],
348
- apiKeys: { custom: "xxx" },
349
- }));
350
- // Only credentials dir exists, no apikeys config
351
- const configPath = join(testHome, ".agent-credentials", "config.json");
352
- if (existsSync(configPath)) {
353
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
354
- expect(config.providers?.apikeys?.custom).toBeUndefined();
355
- }
356
- });
357
- it.skipIf(!installed)("does not recreate dir if it already exists", async () => {
358
- mkdirSync(join(testHome, ".agent-credentials"), { mode: 0o700 });
359
- writeFileSync(join(testHome, ".agent-credentials", "token_secret"), "existing-secret");
360
- await initGlobalPackage("agent-iam", globalCtx({ packages: ["agent-iam"] }));
361
- // Existing file preserved
362
- expect(readFileSync(join(testHome, ".agent-credentials", "token_secret"), "utf-8")).toBe("existing-secret");
363
- });
364
- });
365
410
  // ─── Global: skill-tree (real CLI) ───────────────────────────────────────────
366
411
  describe("initGlobalPackage — skill-tree (real CLI)", async () => {
367
412
  const installed = await hasCliInstalled("skill-tree");
@@ -393,52 +438,6 @@ describe("initGlobalPackage — skill-tree (real CLI)", async () => {
393
438
  expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
394
439
  });
395
440
  });
396
- // ─── Global: openswarm (direct config write — no CLI) ────────────────────────
397
- describe("initGlobalPackage — openswarm (direct config write)", () => {
398
- it("creates ~/.openswarm/server.json", async () => {
399
- const result = await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm"] }));
400
- expect(result.success).toBe(true);
401
- expect(existsSync(join(testHome, ".openswarm", "server.json"))).toBe(true);
402
- });
403
- it("config defaults are correct", async () => {
404
- await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm"] }));
405
- const config = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
406
- expect(config).toEqual({
407
- host: "localhost",
408
- port: 3000,
409
- auth: { mode: "none" },
410
- storage: { type: "memory" },
411
- logging: { level: "info" },
412
- });
413
- });
414
- it("wires macro-agent adapter when macro-agent is selected", async () => {
415
- await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm", "macro-agent"] }));
416
- const config = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
417
- expect(config.adapter).toEqual({ id: "macro-agent" });
418
- });
419
- it("omits adapter when macro-agent not selected", async () => {
420
- await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm"] }));
421
- const config = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
422
- expect(config.adapter).toBeUndefined();
423
- });
424
- it("skips when server.json already exists (preserves existing)", async () => {
425
- mkdirSync(join(testHome, ".openswarm"), { recursive: true });
426
- writeFileSync(join(testHome, ".openswarm", "server.json"), JSON.stringify({ custom: true }));
427
- const result = await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm", "macro-agent"] }));
428
- expect(result.success).toBe(true);
429
- expect(result.message).toBe("already configured");
430
- // Original config NOT overwritten
431
- const config = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
432
- expect(config.custom).toBe(true);
433
- expect(config.adapter).toBeUndefined();
434
- });
435
- it("produces valid JSON with trailing newline", async () => {
436
- await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm"] }));
437
- const raw = readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8");
438
- expect(() => JSON.parse(raw)).not.toThrow();
439
- expect(raw.endsWith("\n")).toBe(true);
440
- });
441
- });
442
441
  // ─── Global: openhive ────────────────────────────────────────────────────────
443
442
  // openhive 0.0.1 is a stub publish without a bin entry
444
443
  describe("initGlobalPackage — openhive", async () => {
@@ -468,7 +467,7 @@ describe("initGlobalPackage — openhive", async () => {
468
467
  expect(result.message).toBeDefined();
469
468
  });
470
469
  });
471
- // ─── Global: openteams (install-onlyno config) ────────────────────────────
470
+ // ─── Global: openteams (no global config project config handled separately)
472
471
  describe("initGlobalPackage — openteams", () => {
473
472
  it("returns success with no-setup message", async () => {
474
473
  const result = await initGlobalPackage("openteams", globalCtx({ packages: ["openteams"] }));
@@ -479,7 +478,7 @@ describe("initGlobalPackage — openteams", () => {
479
478
  });
480
479
  });
481
480
  });
482
- // ─── Global: sessionlog (install-onlyno config) ───────────────────────────
481
+ // ─── Global: sessionlog (no global config project config handled separately)
483
482
  describe("initGlobalPackage — sessionlog", () => {
484
483
  it("returns success with no-setup message", async () => {
485
484
  const result = await initGlobalPackage("sessionlog", globalCtx({ packages: ["sessionlog"] }));
@@ -490,6 +489,17 @@ describe("initGlobalPackage — sessionlog", () => {
490
489
  });
491
490
  });
492
491
  });
492
+ // ─── Global: claude-code-swarm (no global config) ────────────────────────────
493
+ describe("initGlobalPackage — claude-code-swarm", () => {
494
+ it("returns success with no-setup message", async () => {
495
+ const result = await initGlobalPackage("claude-code-swarm", globalCtx({ packages: ["claude-code-swarm"] }));
496
+ expect(result).toEqual({
497
+ package: "claude-code-swarm",
498
+ success: true,
499
+ message: "no setup required",
500
+ });
501
+ });
502
+ });
493
503
  // ─── Global: unknown ─────────────────────────────────────────────────────────
494
504
  describe("initGlobalPackage — unknown", () => {
495
505
  it("returns failure", async () => {
@@ -506,11 +516,11 @@ describe("e2e: project init — solo bundle", async () => {
506
516
  const opentasksOk = await hasCliInstalled("opentasks");
507
517
  const minimemOk = await hasCliInstalled("minimem");
508
518
  const allOk = opentasksOk && minimemOk;
509
- it.skipIf(!allOk)("initializes opentasks → minimem → macro-agent with cross-wiring", async () => {
519
+ it.skipIf(!allOk)("initializes opentasks → minimem with cross-wiring", async () => {
510
520
  createProject("solo-e2e");
511
521
  const ctx = projectCtx({
512
522
  cwd: testDir,
513
- packages: ["opentasks", "minimem", "macro-agent"],
523
+ packages: ["opentasks", "minimem"],
514
524
  embeddingProvider: "openai",
515
525
  });
516
526
  const results = [];
@@ -526,19 +536,14 @@ describe("e2e: project init — solo bundle", async () => {
526
536
  expect(results.map((r) => r.package)).toEqual([
527
537
  "opentasks",
528
538
  "minimem",
529
- "macro-agent",
530
539
  ]);
531
540
  // opentasks: real config with project name
532
- const otConfig = JSON.parse(readFileSync(join(testDir, ".opentasks", "config.json"), "utf-8"));
541
+ const otConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
533
542
  expect(otConfig.location.name).toBe("solo-e2e");
534
543
  // minimem: real config patched with openai
535
- const mmConfig = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
544
+ const mmConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
536
545
  expect(mmConfig.embedding.provider).toBe("openai");
537
546
  expect(mmConfig.hybrid.enabled).toBe(true);
538
- // macro-agent: config wires opentasks
539
- const maConfig = JSON.parse(readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8"));
540
- expect(maConfig.task.backend).toBe("opentasks");
541
- expect(maConfig.task.opentasks.auto_start).toBe(true);
542
547
  });
543
548
  it.skipIf(!allOk)("skips already-initialized packages", async () => {
544
549
  createProject("skip-e2e");
@@ -552,7 +557,7 @@ describe("e2e: project init — solo bundle", async () => {
552
557
  // Now run full init — opentasks should be skipped
553
558
  const ctx = projectCtx({
554
559
  cwd: testDir,
555
- packages: ["opentasks", "minimem", "macro-agent"],
560
+ packages: ["opentasks", "minimem"],
556
561
  });
557
562
  const results = [];
558
563
  for (const pkg of PROJECT_INIT_ORDER) {
@@ -565,7 +570,6 @@ describe("e2e: project init — solo bundle", async () => {
565
570
  // opentasks was skipped
566
571
  expect(results.map((r) => r.package)).toEqual([
567
572
  "minimem",
568
- "macro-agent",
569
573
  ]);
570
574
  });
571
575
  });
@@ -583,7 +587,6 @@ describe("e2e: project init — team bundle (init order)", async () => {
583
587
  "opentasks",
584
588
  "minimem",
585
589
  "cognitive-core",
586
- "macro-agent",
587
590
  ],
588
591
  embeddingProvider: "gemini",
589
592
  });
@@ -600,34 +603,24 @@ describe("e2e: project init — team bundle (init order)", async () => {
600
603
  "opentasks",
601
604
  "minimem",
602
605
  "cognitive-core",
603
- "macro-agent",
604
606
  ]);
605
607
  // All directories exist
606
- expect(existsSync(join(testDir, ".opentasks"))).toBe(true);
607
- expect(existsSync(join(testDir, ".minimem"))).toBe(true);
608
- expect(existsSync(join(testDir, ".cognitive-core"))).toBe(true);
609
- expect(existsSync(join(testDir, ".multiagent"))).toBe(true);
608
+ expect(existsSync(join(testDir, ".swarm", "opentasks"))).toBe(true);
609
+ expect(existsSync(join(testDir, ".swarm", "minimem"))).toBe(true);
610
+ expect(existsSync(join(testDir, ".swarm", "cognitive-core"))).toBe(true);
610
611
  // minimem was patched to gemini
611
- const mmConfig = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
612
+ const mmConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
612
613
  expect(mmConfig.embedding.provider).toBe("gemini");
613
614
  });
614
615
  });
615
- // ─── E2E: full global init flow (platform bundle) ───────────────────────────
616
- describe("e2e: global init — platform bundle", async () => {
617
- const iamOk = await hasCliInstalled("agent-iam");
616
+ // ─── E2E: full global init flow ──────────────────────────────────────────────
617
+ describe("e2e: global init — skill-tree", async () => {
618
618
  const stOk = await hasCliInstalled("skill-tree");
619
- const allOk = iamOk && stOk;
620
- it.skipIf(!allOk)("initializes agent-iam, skill-tree, and openswarm", async () => {
619
+ it.skipIf(!stOk)("initializes skill-tree", async () => {
621
620
  const ctx = globalCtx({
622
- packages: [
623
- "agent-iam",
624
- "skill-tree",
625
- "openswarm",
626
- "macro-agent",
627
- ],
628
- apiKeys: { anthropic: "sk-ant-e2e" },
621
+ packages: ["skill-tree"],
629
622
  });
630
- const globalOrder = ["agent-iam", "skill-tree", "openswarm"];
623
+ const globalOrder = ["skill-tree"];
631
624
  const results = [];
632
625
  for (const pkg of globalOrder) {
633
626
  if (!ctx.packages.includes(pkg))
@@ -637,32 +630,18 @@ describe("e2e: global init — platform bundle", async () => {
637
630
  results.push(await initGlobalPackage(pkg, ctx));
638
631
  }
639
632
  expect(results.every((r) => r.success)).toBe(true);
640
- expect(results.map((r) => r.package)).toEqual([
641
- "agent-iam",
642
- "skill-tree",
643
- "openswarm",
644
- ]);
645
- // agent-iam: credentials dir exists, anthropic key registered
646
- expect(existsSync(join(testHome, ".agent-credentials"))).toBe(true);
647
- const iamConfig = JSON.parse(readFileSync(join(testHome, ".agent-credentials", "config.json"), "utf-8"));
648
- expect(iamConfig.providers.apikeys.anthropic.apiKey).toBe("sk-ant-e2e");
633
+ expect(results.map((r) => r.package)).toEqual(["skill-tree"]);
649
634
  // skill-tree: config.yaml exists
650
635
  expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
651
- // openswarm: server.json wires macro-agent adapter
652
- const swarmConfig = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
653
- expect(swarmConfig.adapter).toEqual({ id: "macro-agent" });
654
636
  });
655
- it.skipIf(!allOk)("skips already-configured global packages", async () => {
637
+ it.skipIf(!stOk)("skips already-configured global packages", async () => {
656
638
  // Pre-create skill-tree config
657
639
  mkdirSync(join(testHome, ".skill-tree"), { recursive: true });
658
640
  writeFileSync(join(testHome, ".skill-tree", "config.yaml"), "existing: true\n");
659
- // Pre-create openswarm config
660
- mkdirSync(join(testHome, ".openswarm"), { recursive: true });
661
- writeFileSync(join(testHome, ".openswarm", "server.json"), JSON.stringify({ existing: true }));
662
641
  const ctx = globalCtx({
663
- packages: ["agent-iam", "skill-tree", "openswarm"],
642
+ packages: ["skill-tree"],
664
643
  });
665
- const globalOrder = ["agent-iam", "skill-tree", "openswarm"];
644
+ const globalOrder = ["skill-tree"];
666
645
  const results = [];
667
646
  for (const pkg of globalOrder) {
668
647
  if (!ctx.packages.includes(pkg))
@@ -671,12 +650,142 @@ describe("e2e: global init — platform bundle", async () => {
671
650
  continue;
672
651
  results.push(await initGlobalPackage(pkg, ctx));
673
652
  }
674
- // Only agent-iam was initialized
675
- expect(results.map((r) => r.package)).toEqual(["agent-iam"]);
676
- // Existing configs preserved
653
+ // Nothing was initialized (already configured)
654
+ expect(results).toEqual([]);
655
+ // Existing config preserved
677
656
  expect(readFileSync(join(testHome, ".skill-tree", "config.yaml"), "utf-8")).toBe("existing: true\n");
678
- const swarmConfig = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
679
- expect(swarmConfig.existing).toBe(true);
657
+ });
658
+ });
659
+ // ─── Flow 2: plugin bootstrap → swarmkit project init ────────────────────────
660
+ //
661
+ // Simulates what claude-code-swarm's bootstrap.mjs does when it calls swarmkit
662
+ // programmatically: check isProjectInit for each dep, then init those missing.
663
+ // This is the "plugin installs first, swarmkit comes bundled" path.
664
+ // ─────────────────────────────────────────────────────────────────────────────
665
+ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
666
+ it("initializes openteams + claude-code-swarm in correct order", async () => {
667
+ // Mirrors bootstrap.mjs's initSwarmProject() + ensureSwarmDir() flow
668
+ const cwd = testDir;
669
+ // Bootstrap determines required packages from config
670
+ const requiredPackages = ["openteams", "claude-code-swarm"];
671
+ const ctx = projectCtx({
672
+ cwd,
673
+ packages: requiredPackages,
674
+ });
675
+ // Bootstrap checks which packages are already initialized
676
+ for (const pkg of requiredPackages) {
677
+ expect(isProjectInit(cwd, pkg)).toBe(false);
678
+ }
679
+ // Bootstrap initializes missing packages using PROJECT_INIT_ORDER
680
+ const results = [];
681
+ for (const pkg of PROJECT_INIT_ORDER) {
682
+ if (!requiredPackages.includes(pkg))
683
+ continue;
684
+ if (isProjectInit(cwd, pkg))
685
+ continue;
686
+ results.push(await initProjectPackage(pkg, ctx));
687
+ }
688
+ // All succeeded
689
+ expect(results.every((r) => r.success)).toBe(true);
690
+ expect(results.map((r) => r.package)).toEqual([
691
+ "openteams",
692
+ "claude-code-swarm",
693
+ ]);
694
+ // openteams: .swarm/openteams/config.json exists
695
+ expect(existsSync(join(cwd, ".swarm", "openteams"))).toBe(true);
696
+ expect(existsSync(join(cwd, ".swarm", "openteams", "config.json"))).toBe(true);
697
+ // claude-code-swarm: .swarm/claude-swarm/ exists with config + .gitignore
698
+ expect(existsSync(join(cwd, ".swarm", "claude-swarm"))).toBe(true);
699
+ expect(existsSync(join(cwd, ".swarm", "claude-swarm", "config.json"))).toBe(true);
700
+ expect(existsSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
701
+ const config = JSON.parse(readFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), "utf-8"));
702
+ expect(config).toEqual({});
703
+ const gitignore = readFileSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
704
+ expect(gitignore).toBe("tmp/\n");
705
+ // isProjectInit now returns true for both
706
+ expect(isProjectInit(cwd, "openteams")).toBe(true);
707
+ expect(isProjectInit(cwd, "claude-code-swarm")).toBe(true);
708
+ });
709
+ it("initializes openteams + sessionlog + claude-code-swarm when sessionlog enabled", async () => {
710
+ const cwd = testDir;
711
+ // Config has sessionlog enabled → bootstrap adds it to required
712
+ const requiredPackages = ["openteams", "sessionlog", "claude-code-swarm"];
713
+ const ctx = projectCtx({
714
+ cwd,
715
+ packages: requiredPackages,
716
+ });
717
+ const results = [];
718
+ for (const pkg of PROJECT_INIT_ORDER) {
719
+ if (!requiredPackages.includes(pkg))
720
+ continue;
721
+ if (isProjectInit(cwd, pkg))
722
+ continue;
723
+ results.push(await initProjectPackage(pkg, ctx));
724
+ }
725
+ expect(results.every((r) => r.success)).toBe(true);
726
+ expect(results.map((r) => r.package)).toEqual([
727
+ "openteams",
728
+ "sessionlog",
729
+ "claude-code-swarm",
730
+ ]);
731
+ // All three dirs exist
732
+ expect(existsSync(join(cwd, ".swarm", "openteams"))).toBe(true);
733
+ expect(existsSync(join(cwd, ".swarm", "sessionlog"))).toBe(true);
734
+ expect(existsSync(join(cwd, ".swarm", "claude-swarm"))).toBe(true);
735
+ // sessionlog has default settings
736
+ const settings = JSON.parse(readFileSync(join(cwd, ".swarm", "sessionlog", "settings.json"), "utf-8"));
737
+ expect(settings.enabled).toBe(false);
738
+ expect(settings.strategy).toBe("manual-commit");
739
+ });
740
+ it("skips already-initialized packages on re-run (idempotent bootstrap)", async () => {
741
+ const cwd = testDir;
742
+ const requiredPackages = ["openteams", "claude-code-swarm"];
743
+ const ctx = projectCtx({ cwd, packages: requiredPackages });
744
+ // First bootstrap run
745
+ for (const pkg of PROJECT_INIT_ORDER) {
746
+ if (!requiredPackages.includes(pkg))
747
+ continue;
748
+ if (isProjectInit(cwd, pkg))
749
+ continue;
750
+ await initProjectPackage(pkg, ctx);
751
+ }
752
+ // Write custom config to .swarm/claude-swarm/config.json
753
+ writeFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), JSON.stringify({ template: "gsd" }));
754
+ // Second bootstrap run — should skip both (already initialized)
755
+ const results = [];
756
+ for (const pkg of PROJECT_INIT_ORDER) {
757
+ if (!requiredPackages.includes(pkg))
758
+ continue;
759
+ if (isProjectInit(cwd, pkg))
760
+ continue;
761
+ results.push(await initProjectPackage(pkg, ctx));
762
+ }
763
+ // Nothing was initialized (all already exist)
764
+ expect(results).toEqual([]);
765
+ // Custom config preserved
766
+ const config = JSON.parse(readFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), "utf-8"));
767
+ expect(config.template).toBe("gsd");
768
+ });
769
+ it("handles pre-existing openteams, only initializes claude-code-swarm", async () => {
770
+ const cwd = testDir;
771
+ // Simulate swarmkit init already ran (openteams exists)
772
+ mkdirSync(join(cwd, ".swarm", "openteams"), { recursive: true });
773
+ writeFileSync(join(cwd, ".swarm", "openteams", "config.json"), JSON.stringify({ existing: true }));
774
+ const requiredPackages = ["openteams", "claude-code-swarm"];
775
+ const ctx = projectCtx({ cwd, packages: requiredPackages });
776
+ const results = [];
777
+ for (const pkg of PROJECT_INIT_ORDER) {
778
+ if (!requiredPackages.includes(pkg))
779
+ continue;
780
+ if (isProjectInit(cwd, pkg))
781
+ continue;
782
+ results.push(await initProjectPackage(pkg, ctx));
783
+ }
784
+ // Only claude-code-swarm was initialized
785
+ expect(results.map((r) => r.package)).toEqual(["claude-code-swarm"]);
786
+ // openteams config preserved
787
+ const otConfig = JSON.parse(readFileSync(join(cwd, ".swarm", "openteams", "config.json"), "utf-8"));
788
+ expect(otConfig.existing).toBe(true);
680
789
  });
681
790
  });
682
791
  // ─── E2E: embedding provider propagation ─────────────────────────────────────
@@ -690,28 +799,10 @@ describe("e2e: embedding provider propagation", async () => {
690
799
  embeddingProvider: "openai",
691
800
  apiKeys: { openai: "sk-embed" },
692
801
  }));
693
- const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
802
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
694
803
  expect(config.embedding.provider).toBe("openai");
695
804
  // Real minimem config has these fields too
696
805
  expect(config.hybrid).toBeDefined();
697
806
  expect(config.query).toBeDefined();
698
807
  });
699
808
  });
700
- // ─── E2E: API key propagation to agent-iam ───────────────────────────────────
701
- describe("e2e: API key propagation to agent-iam", async () => {
702
- const iamOk = await hasCliInstalled("agent-iam");
703
- it.skipIf(!iamOk)("all three standard keys end up in agent-iam config", async () => {
704
- await initGlobalPackage("agent-iam", globalCtx({
705
- packages: ["agent-iam"],
706
- apiKeys: {
707
- openai: "sk-openai-e2e",
708
- anthropic: "sk-anthropic-e2e",
709
- gemini: "gemini-e2e",
710
- },
711
- }));
712
- const config = JSON.parse(readFileSync(join(testHome, ".agent-credentials", "config.json"), "utf-8"));
713
- expect(config.providers.apikeys.openai.apiKey).toBe("sk-openai-e2e");
714
- expect(config.providers.apikeys.anthropic.apiKey).toBe("sk-anthropic-e2e");
715
- expect(config.providers.apikeys.gemini.apiKey).toBe("gemini-e2e");
716
- });
717
- });