@codemcp/ade 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.beads/issues.jsonl +26 -0
  2. package/.beads/last-touched +1 -1
  3. package/.vibe/beads-state-ade-fix-docset-writing-37fuoj.json +29 -0
  4. package/.vibe/development-plan-fix-docset-writing.md +77 -0
  5. package/.vibe/docs/architecture.md +201 -0
  6. package/.vibe/docs/design.md +179 -0
  7. package/.vibe/docs/requirements.md +17 -0
  8. package/ade.extensions.mjs +13 -15
  9. package/docs/CLI-PRD.md +38 -40
  10. package/docs/CLI-design.md +47 -57
  11. package/docs/guide/extensions.md +6 -14
  12. package/package.json +1 -1
  13. package/packages/cli/dist/index.js +15202 -5579
  14. package/packages/cli/package.json +1 -1
  15. package/packages/cli/src/commands/conventions.integration.spec.ts +29 -4
  16. package/packages/cli/src/commands/extensions.integration.spec.ts +26 -4
  17. package/packages/cli/src/commands/install.ts +2 -0
  18. package/packages/cli/src/commands/knowledge-docset.integration.spec.ts +179 -0
  19. package/packages/cli/src/commands/knowledge.integration.spec.ts +24 -36
  20. package/packages/cli/src/commands/setup.spec.ts +1 -101
  21. package/packages/cli/src/commands/setup.ts +23 -36
  22. package/packages/cli/src/knowledge-installer.spec.ts +43 -3
  23. package/packages/cli/src/knowledge-installer.ts +12 -9
  24. package/packages/core/package.json +1 -1
  25. package/packages/core/src/catalog/catalog.spec.ts +75 -43
  26. package/packages/core/src/catalog/facets/architecture.ts +89 -58
  27. package/packages/core/src/catalog/facets/practices.ts +9 -8
  28. package/packages/core/src/index.ts +4 -4
  29. package/packages/core/src/registry.spec.ts +1 -1
  30. package/packages/core/src/registry.ts +2 -2
  31. package/packages/core/src/resolver.spec.ts +61 -154
  32. package/packages/core/src/resolver.ts +0 -54
  33. package/packages/core/src/types.ts +5 -10
  34. package/packages/core/src/writers/docset.spec.ts +40 -0
  35. package/packages/core/src/writers/docset.ts +24 -0
  36. package/packages/harnesses/package.json +1 -1
  37. package/packages/core/src/writers/knowledge.spec.ts +0 -26
  38. package/packages/core/src/writers/knowledge.ts +0 -15
@@ -41,5 +41,5 @@
41
41
  "typescript": "catalog:",
42
42
  "vitest": "catalog:"
43
43
  },
44
- "version": "0.7.0"
44
+ "version": "0.8.0"
45
45
  }
@@ -1,5 +1,12 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdtemp, rm, readFile, access } from "node:fs/promises";
2
+ import {
3
+ mkdtemp,
4
+ rm,
5
+ readFile,
6
+ access,
7
+ writeFile,
8
+ mkdir
9
+ } from "node:fs/promises";
3
10
  import { tmpdir } from "node:os";
4
11
  import { join } from "node:path";
5
12
 
@@ -21,6 +28,27 @@ vi.mock("@clack/prompts", () => ({
21
28
  spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
22
29
  }));
23
30
 
31
+ // Mock the knowledge package to avoid real network I/O
32
+ vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({
33
+ createDocset: vi.fn(
34
+ async (
35
+ params: { id: string; name: string; url?: string },
36
+ options: { cwd?: string }
37
+ ) => {
38
+ const dir = join(options?.cwd ?? process.cwd(), ".knowledge");
39
+ await mkdir(dir, { recursive: true });
40
+ const configPath = join(dir, "config.yaml");
41
+ await writeFile(
42
+ configPath,
43
+ `version: "1.0"\ndocsets:\n - id: ${params.id}\n`,
44
+ { flag: "w" }
45
+ );
46
+ return { docset: {}, configPath, configCreated: true };
47
+ }
48
+ ),
49
+ initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false })
50
+ }));
51
+
24
52
  import * as clack from "@clack/prompts";
25
53
  import { runSetup } from "./setup.js";
26
54
  import { readUserConfig, readLockFile } from "@codemcp/ade-core";
@@ -51,7 +79,6 @@ describe("architecture and practices facets integration", () => {
51
79
  vi.mocked(clack.multiselect)
52
80
  .mockResolvedValueOnce([]) // practices: none
53
81
  .mockResolvedValueOnce([]) // backpressure: none
54
- .mockResolvedValueOnce([]) // docsets: deselect all
55
82
  .mockResolvedValueOnce(["claude-code"]); // harnesses
56
83
 
57
84
  await runSetup(dir, catalog);
@@ -110,7 +137,6 @@ describe("architecture and practices facets integration", () => {
110
137
  .mockResolvedValueOnce("__skip__"); // architecture: skip
111
138
  vi.mocked(clack.multiselect)
112
139
  .mockResolvedValueOnce(["conventional-commits", "tdd-london"]) // practices
113
- .mockResolvedValueOnce([]) // docsets: deselect all (conventional-commits has docset)
114
140
  .mockResolvedValueOnce(["claude-code"]); // harnesses
115
141
 
116
142
  await runSetup(dir, catalog);
@@ -237,7 +263,6 @@ describe("architecture and practices facets integration", () => {
237
263
  vi.mocked(clack.multiselect)
238
264
  .mockResolvedValueOnce(["tdd-london", "conventional-commits"]) // practices
239
265
  .mockResolvedValueOnce([]) // backpressure: none
240
- .mockResolvedValueOnce([]) // docsets: deselect all
241
266
  .mockResolvedValueOnce(["claude-code"]); // harnesses
242
267
 
243
268
  await runSetup(dir, catalog);
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdtemp, rm, readFile } from "node:fs/promises";
2
+ import { mkdtemp, rm, readFile, writeFile, mkdir } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
 
@@ -17,6 +17,27 @@ vi.mock("@clack/prompts", () => ({
17
17
  spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
18
18
  }));
19
19
 
20
+ // Mock the knowledge package to avoid real network I/O
21
+ vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({
22
+ createDocset: vi.fn(
23
+ async (
24
+ params: { id: string; name: string; url?: string },
25
+ options: { cwd?: string }
26
+ ) => {
27
+ const dir = join(options?.cwd ?? process.cwd(), ".knowledge");
28
+ await mkdir(dir, { recursive: true });
29
+ const configPath = join(dir, "config.yaml");
30
+ await writeFile(
31
+ configPath,
32
+ `version: "1.0"\ndocsets:\n - id: ${params.id}\n`,
33
+ { flag: "w" }
34
+ );
35
+ return { docset: {}, configPath, configCreated: true };
36
+ }
37
+ ),
38
+ initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false })
39
+ }));
40
+
20
41
  import * as clack from "@clack/prompts";
21
42
  import { runSetup } from "./setup.js";
22
43
  import {
@@ -43,7 +64,7 @@ describe("extension e2e — option contributes skills and knowledge to setup out
43
64
  "extension-contributed architecture option writes inline skill and knowledge source",
44
65
  { timeout: 60_000 },
45
66
  async () => {
46
- // Build an extension with a SAP option that has an inline skill + knowledge
67
+ // Build an extension with a SAP option that has an inline skill + a docset
47
68
  const extensions: AdeExtensions = {
48
69
  facetContributions: {
49
70
  architecture: [
@@ -65,9 +86,10 @@ describe("extension e2e — option contributes skills and knowledge to setup out
65
86
  }
66
87
  },
67
88
  {
68
- writer: "knowledge",
89
+ writer: "docset",
69
90
  config: {
70
- name: "sap-abap-docs",
91
+ id: "sap-abap-docs",
92
+ label: "SAP ABAP Cloud",
71
93
  origin: "https://help.sap.com/docs/abap-cloud",
72
94
  description: "SAP ABAP Cloud documentation"
73
95
  }
@@ -8,6 +8,7 @@ import {
8
8
  installSkills,
9
9
  writeInlineSkills
10
10
  } from "@codemcp/ade-harnesses";
11
+ import { installKnowledge } from "../knowledge-installer.js";
11
12
 
12
13
  export async function runInstall(
13
14
  projectRoot: string,
@@ -77,6 +78,7 @@ export async function runInstall(
77
78
  }
78
79
 
79
80
  if (logicalConfig.knowledge_sources.length > 0) {
81
+ await installKnowledge(logicalConfig.knowledge_sources, projectRoot);
80
82
  clack.log.info(
81
83
  "Knowledge sources configured. Initialize them separately:\n npx @codemcp/knowledge init"
82
84
  );
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { mkdtemp, rm, readFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ /**
7
+ * E2E regression tests for two issues with docset / knowledge setup:
8
+ *
9
+ * Issue 1 — Design inconsistency (now fixed):
10
+ * The `knowledge` provision writer was a redundant second way to add
11
+ * knowledge sources alongside `option.docsets[]`. It has been removed.
12
+ * Docsets are now declared via `{ writer: "docset", config: {...} }` recipe
13
+ * entries (the `docset` provision writer), consistent with how skills work.
14
+ *
15
+ * Issue 2 — Missing .knowledge/config.yaml (now fixed):
16
+ * `installKnowledge` was never called from `setup` or `install`, so
17
+ * `createDocset` was never invoked and `.knowledge/config.yaml` was
18
+ * never written. Both commands now call `installKnowledge`.
19
+ */
20
+
21
+ // Mock the TUI
22
+ vi.mock("@clack/prompts", () => ({
23
+ intro: vi.fn(),
24
+ outro: vi.fn(),
25
+ note: vi.fn(),
26
+ select: vi.fn(),
27
+ multiselect: vi.fn(),
28
+ confirm: vi.fn().mockResolvedValue(false),
29
+ isCancel: vi.fn().mockReturnValue(false),
30
+ cancel: vi.fn(),
31
+ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn() },
32
+ spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
33
+ }));
34
+
35
+ // Mock the knowledge package to avoid real network I/O while still letting us
36
+ // assert that createDocset is called with the correct arguments.
37
+ // The mock writes a real .knowledge/config.yaml so file-existence assertions work.
38
+ import { writeFile, mkdir } from "node:fs/promises";
39
+ vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({
40
+ createDocset: vi.fn(
41
+ async (
42
+ params: { id: string; name: string; url?: string },
43
+ options: { cwd?: string }
44
+ ) => {
45
+ const dir = join(options?.cwd ?? process.cwd(), ".knowledge");
46
+ await mkdir(dir, { recursive: true });
47
+ const configPath = join(dir, "config.yaml");
48
+ // Append a minimal docset entry so the file is created/updated
49
+ await writeFile(
50
+ configPath,
51
+ `version: "1.0"\ndocsets:\n - id: ${params.id}\n`,
52
+ { flag: "w" }
53
+ );
54
+ return { docset: {}, configPath, configCreated: true };
55
+ }
56
+ ),
57
+ initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false })
58
+ }));
59
+
60
+ import * as clack from "@clack/prompts";
61
+ import { createDocset } from "@codemcp/knowledge/packages/cli/dist/exports.js";
62
+ import { runSetup } from "./setup.js";
63
+ import { runInstall } from "./install.js";
64
+ import { readLockFile, getDefaultCatalog } from "@codemcp/ade-core";
65
+
66
+ describe("knowledge docset regression tests", () => {
67
+ let dir: string;
68
+
69
+ beforeEach(async () => {
70
+ vi.clearAllMocks();
71
+ vi.mocked(clack.confirm).mockResolvedValue(false);
72
+ dir = await mkdtemp(join(tmpdir(), "ade-knowledge-bug-"));
73
+ });
74
+
75
+ afterEach(async () => {
76
+ await rm(dir, { recursive: true, force: true });
77
+ });
78
+
79
+ // -------------------------------------------------------------------------
80
+ // Issue 2 fix: setup writes .knowledge/config.yaml
81
+ // -------------------------------------------------------------------------
82
+
83
+ it(
84
+ "setup writes .knowledge/config.yaml when knowledge_sources are configured",
85
+ { timeout: 30_000 },
86
+ async () => {
87
+ const catalog = getDefaultCatalog();
88
+
89
+ vi.mocked(clack.select)
90
+ .mockResolvedValueOnce("codemcp-workflows") // process
91
+ .mockResolvedValueOnce("tanstack"); // architecture — has 4 docsets
92
+ vi.mocked(clack.multiselect)
93
+ .mockResolvedValueOnce([]) // practices: none
94
+ .mockResolvedValueOnce([]) // backpressure: none
95
+ .mockResolvedValueOnce(["claude-code"]); // harnesses
96
+ vi.mocked(clack.confirm)
97
+ .mockResolvedValueOnce(false) // skills: skip
98
+ .mockResolvedValueOnce(true); // knowledge: initialize now
99
+
100
+ await runSetup(dir, catalog);
101
+
102
+ // Sanity: knowledge_sources in lock file
103
+ const lock = await readLockFile(dir);
104
+ expect(lock!.logical_config.knowledge_sources).toHaveLength(4);
105
+
106
+ // createDocset must have been called once per source
107
+ expect(createDocset).toHaveBeenCalledTimes(4);
108
+ expect(createDocset).toHaveBeenCalledWith(
109
+ expect.objectContaining({ id: "tanstack-router-docs" }),
110
+ expect.objectContaining({ cwd: dir })
111
+ );
112
+
113
+ // .knowledge/config.yaml must exist
114
+ const configYaml = await readFile(
115
+ join(dir, ".knowledge", "config.yaml"),
116
+ "utf-8"
117
+ );
118
+ expect(configYaml).toBeTruthy();
119
+ }
120
+ );
121
+
122
+ it(
123
+ "install writes .knowledge/config.yaml when knowledge_sources exist in lock file",
124
+ { timeout: 30_000 },
125
+ async () => {
126
+ const catalog = getDefaultCatalog();
127
+
128
+ // First setup to produce a lock file with knowledge_sources
129
+ vi.mocked(clack.select)
130
+ .mockResolvedValueOnce("codemcp-workflows") // process
131
+ .mockResolvedValueOnce("tanstack"); // architecture
132
+ vi.mocked(clack.multiselect)
133
+ .mockResolvedValueOnce([]) // practices: none
134
+ .mockResolvedValueOnce([]) // backpressure: none
135
+ .mockResolvedValueOnce(["claude-code"]); // harnesses
136
+ // skills: skip, knowledge: skip (install command will handle it)
137
+ vi.mocked(clack.confirm)
138
+ .mockResolvedValueOnce(false)
139
+ .mockResolvedValueOnce(false);
140
+
141
+ await runSetup(dir, catalog);
142
+ vi.clearAllMocks();
143
+
144
+ // Wipe agent files so install has something to regenerate
145
+ await rm(join(dir, ".mcp.json"), { force: true });
146
+ await rm(join(dir, ".knowledge"), { recursive: true, force: true });
147
+
148
+ // Now run install — should also write .knowledge/config.yaml
149
+ await runInstall(dir, ["claude-code"]);
150
+
151
+ // All 4 tanstack docsets are configured via the docset writer (no per-item selection)
152
+ expect(createDocset).toHaveBeenCalledTimes(4);
153
+ expect(createDocset).toHaveBeenCalledWith(
154
+ expect.objectContaining({ id: "tanstack-router-docs" }),
155
+ expect.objectContaining({ cwd: dir })
156
+ );
157
+
158
+ const configYaml = await readFile(
159
+ join(dir, ".knowledge", "config.yaml"),
160
+ "utf-8"
161
+ );
162
+ expect(configYaml).toBeTruthy();
163
+ }
164
+ );
165
+
166
+ // -------------------------------------------------------------------------
167
+ // Issue 1 fix: knowledge writer removed — docset provision writer is canonical
168
+ // -------------------------------------------------------------------------
169
+
170
+ it("ProvisionWriter type no longer includes 'knowledge'", async () => {
171
+ // Import the type-level check: if 'knowledge' were still in ProvisionWriter,
172
+ // this runtime check would catch the registry accepting it silently.
173
+ const { createDefaultRegistry } = await import("@codemcp/ade-core");
174
+ const registry = createDefaultRegistry();
175
+ // The knowledge writer must not be registered
176
+ const { getProvisionWriter } = await import("@codemcp/ade-core");
177
+ expect(getProvisionWriter(registry, "knowledge")).toBeUndefined();
178
+ });
179
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdtemp, rm, readFile } from "node:fs/promises";
2
+ import { mkdtemp, rm, readFile, writeFile, mkdir } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
 
@@ -16,6 +16,27 @@ vi.mock("@clack/prompts", () => ({
16
16
  spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
17
17
  }));
18
18
 
19
+ // Mock the knowledge package to avoid real network I/O
20
+ vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({
21
+ createDocset: vi.fn(
22
+ async (
23
+ params: { id: string; name: string; url?: string },
24
+ options: { cwd?: string }
25
+ ) => {
26
+ const dir = join(options?.cwd ?? process.cwd(), ".knowledge");
27
+ await mkdir(dir, { recursive: true });
28
+ const configPath = join(dir, "config.yaml");
29
+ await writeFile(
30
+ configPath,
31
+ `version: "1.0"\ndocsets:\n - id: ${params.id}\n`,
32
+ { flag: "w" }
33
+ );
34
+ return { docset: {}, configPath, configCreated: true };
35
+ }
36
+ ),
37
+ initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false })
38
+ }));
39
+
19
40
  import * as clack from "@clack/prompts";
20
41
  import { runSetup } from "./setup.js";
21
42
  import { readLockFile } from "@codemcp/ade-core";
@@ -46,17 +67,11 @@ describe("knowledge integration", () => {
46
67
  vi.mocked(clack.multiselect)
47
68
  .mockResolvedValueOnce([]) // practices: none
48
69
  .mockResolvedValueOnce([]) // backpressure: none
49
- .mockResolvedValueOnce([
50
- "tanstack-router-docs",
51
- "tanstack-query-docs",
52
- "tanstack-form-docs",
53
- "tanstack-table-docs"
54
- ])
55
70
  .mockResolvedValueOnce(["claude-code"]); // harnesses
56
71
 
57
72
  await runSetup(dir, catalog);
58
73
 
59
- // Lock file should contain knowledge_sources
74
+ // Lock file should contain all 4 knowledge_sources from tanstack docset entries
60
75
  const lock = await readLockFile(dir);
61
76
  expect(lock!.logical_config.knowledge_sources).toHaveLength(4);
62
77
  expect(lock!.logical_config.knowledge_sources.map((s) => s.name)).toEqual(
@@ -84,33 +99,6 @@ describe("knowledge integration", () => {
84
99
  }
85
100
  );
86
101
 
87
- it(
88
- "excludes deselected docsets from lock file",
89
- { timeout: 60_000 },
90
- async () => {
91
- const catalog = getDefaultCatalog();
92
-
93
- vi.mocked(clack.select)
94
- .mockResolvedValueOnce("codemcp-workflows") // process
95
- .mockResolvedValueOnce("tanstack"); // architecture
96
-
97
- vi.mocked(clack.multiselect)
98
- .mockResolvedValueOnce([]) // practices: none
99
- .mockResolvedValueOnce([]) // backpressure: none
100
- .mockResolvedValueOnce(["tanstack-router-docs", "tanstack-query-docs"])
101
- .mockResolvedValueOnce(["claude-code"]); // harnesses
102
-
103
- await runSetup(dir, catalog);
104
-
105
- // Lock file should only have the 2 selected sources
106
- const lock = await readLockFile(dir);
107
- expect(lock!.logical_config.knowledge_sources).toHaveLength(2);
108
- expect(lock!.logical_config.knowledge_sources.map((s) => s.name)).toEqual(
109
- expect.arrayContaining(["tanstack-router-docs", "tanstack-query-docs"])
110
- );
111
- }
112
- );
113
-
114
102
  it("does not show knowledge hint when no docsets are implied", async () => {
115
103
  const catalog = getDefaultCatalog();
116
104
 
@@ -118,7 +106,7 @@ describe("knowledge integration", () => {
118
106
  .mockResolvedValueOnce("native-agents-md") // process
119
107
  .mockResolvedValueOnce("__skip__"); // architecture: skip
120
108
  vi.mocked(clack.multiselect)
121
- .mockResolvedValueOnce(["tdd-london"]) // practices: no docsets
109
+ .mockResolvedValueOnce(["tdd-london"]) // practices: tdd-london has no docsets
122
110
  .mockResolvedValueOnce(["claude-code"]); // harnesses
123
111
 
124
112
  await runSetup(dir, catalog);
@@ -31,8 +31,7 @@ vi.mock("@codemcp/ade-core", async (importOriginal) => {
31
31
  skills: [],
32
32
  git_hooks: [],
33
33
  setup_notes: []
34
- } satisfies LogicalConfig),
35
- collectDocsets: actual.collectDocsets
34
+ } satisfies LogicalConfig)
36
35
  };
37
36
  });
38
37
 
@@ -112,39 +111,6 @@ const testCatalog: Catalog = {
112
111
  ]
113
112
  };
114
113
 
115
- const docsetCatalog: Catalog = {
116
- facets: [
117
- {
118
- id: "arch",
119
- label: "Architecture",
120
- description: "Stack",
121
- required: true,
122
- options: [
123
- {
124
- id: "react",
125
- label: "React",
126
- description: "React framework",
127
- recipe: [],
128
- docsets: [
129
- {
130
- id: "react-docs",
131
- label: "React Reference",
132
- origin: "https://github.com/facebook/react.git",
133
- description: "Official React docs"
134
- },
135
- {
136
- id: "react-tutorial",
137
- label: "React Tutorial",
138
- origin: "https://github.com/reactjs/react.dev.git",
139
- description: "React learn guide"
140
- }
141
- ]
142
- }
143
- ]
144
- }
145
- ]
146
- };
147
-
148
114
  // ── Tests ────────────────────────────────────────────────────────────────────
149
115
 
150
116
  describe("runSetup", () => {
@@ -239,72 +205,6 @@ describe("runSetup", () => {
239
205
  expect(clack.cancel).toHaveBeenCalled();
240
206
  });
241
207
 
242
- describe("docset confirmation step", () => {
243
- it("presents implied docsets as a multiselect after facet selection", async () => {
244
- vi.mocked(clack.select).mockResolvedValueOnce("react");
245
- // User accepts all docsets (returns all ids), then harness selection
246
- vi.mocked(clack.multiselect)
247
- .mockResolvedValueOnce(["react-docs", "react-tutorial"])
248
- .mockResolvedValueOnce(["claude-code"]);
249
-
250
- await runSetup("/tmp/test-project", docsetCatalog);
251
-
252
- // multiselect should have been called for docsets
253
- expect(clack.multiselect).toHaveBeenCalledWith(
254
- expect.objectContaining({
255
- message: expect.stringContaining("Documentation")
256
- })
257
- );
258
- });
259
-
260
- it("stores deselected docsets as excluded_docsets in user config", async () => {
261
- vi.mocked(clack.select).mockResolvedValueOnce("react");
262
- // User deselects react-tutorial, keeps only react-docs; then harness
263
- vi.mocked(clack.multiselect)
264
- .mockResolvedValueOnce(["react-docs"])
265
- .mockResolvedValueOnce(["claude-code"]);
266
-
267
- await runSetup("/tmp/test-project", docsetCatalog);
268
-
269
- expect(writeUserConfig).toHaveBeenCalledWith(
270
- "/tmp/test-project",
271
- expect.objectContaining({
272
- excluded_docsets: ["react-tutorial"]
273
- })
274
- );
275
- });
276
-
277
- it("does not set excluded_docsets when all docsets are accepted", async () => {
278
- vi.mocked(clack.select).mockResolvedValueOnce("react");
279
- vi.mocked(clack.multiselect)
280
- .mockResolvedValueOnce(["react-docs", "react-tutorial"])
281
- .mockResolvedValueOnce(["claude-code"]);
282
-
283
- await runSetup("/tmp/test-project", docsetCatalog);
284
-
285
- const configArg = vi.mocked(writeUserConfig).mock.calls[0][1];
286
- expect(configArg.excluded_docsets).toBeUndefined();
287
- });
288
-
289
- it("skips docset prompt when no options have docsets", async () => {
290
- vi.mocked(clack.select)
291
- .mockResolvedValueOnce("workflow-a")
292
- .mockResolvedValueOnce("vitest");
293
- // Only the harness multiselect should be called (no docsets in testCatalog)
294
- vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
295
-
296
- await runSetup("/tmp/test-project", testCatalog);
297
-
298
- // multiselect should have been called exactly once (for harnesses only)
299
- expect(clack.multiselect).toHaveBeenCalledTimes(1);
300
- expect(clack.multiselect).toHaveBeenCalledWith(
301
- expect.objectContaining({
302
- message: expect.stringContaining("coding agents")
303
- })
304
- );
305
- });
306
- });
307
-
308
208
  it("calls intro and outro from @clack/prompts", async () => {
309
209
  vi.mocked(clack.select)
310
210
  .mockResolvedValueOnce("workflow-a")
@@ -8,7 +8,6 @@ import {
8
8
  writeUserConfig,
9
9
  writeLockFile,
10
10
  resolve,
11
- collectDocsets,
12
11
  createDefaultRegistry,
13
12
  getFacet,
14
13
  getOption,
@@ -22,6 +21,7 @@ import {
22
21
  installSkills,
23
22
  writeInlineSkills
24
23
  } from "@codemcp/ade-harnesses";
24
+ import { installKnowledge } from "../knowledge-installer.js";
25
25
 
26
26
  export async function runSetup(
27
27
  projectRoot: string,
@@ -107,37 +107,6 @@ export async function runSetup(
107
107
  }
108
108
  }
109
109
 
110
- // Docset confirmation step: collect implied docsets, let user deselect
111
- const impliedDocsets = collectDocsets(choices, catalog);
112
- let excludedDocsets: string[] | undefined;
113
-
114
- if (impliedDocsets.length > 0) {
115
- const selected = await clack.multiselect({
116
- message:
117
- "Documentation sources — Those will be pulled to your local disk for browsing on demand",
118
- options: impliedDocsets.map((d) => ({
119
- value: d.id,
120
- label: d.label,
121
- hint: d.description
122
- })),
123
- initialValues: impliedDocsets.map((d) => d.id),
124
- required: false
125
- });
126
-
127
- if (typeof selected === "symbol") {
128
- clack.cancel("Setup cancelled.");
129
- return;
130
- }
131
-
132
- const selectedSet = new Set(selected as string[]);
133
- const excluded = impliedDocsets
134
- .filter((d) => !selectedSet.has(d.id))
135
- .map((d) => d.id);
136
- if (excluded.length > 0) {
137
- excludedDocsets = excluded;
138
- }
139
- }
140
-
141
110
  // Harness selection — multi-select from all available harnesses
142
111
  const existingHarnesses = existingConfig?.harnesses;
143
112
  const harnessOptions = harnessWriters.map((w) => ({
@@ -171,7 +140,6 @@ export async function runSetup(
171
140
 
172
141
  const userConfig: UserConfig = {
173
142
  choices,
174
- ...(excludedDocsets && { excluded_docsets: excludedDocsets }),
175
143
  ...(harnesses.length > 0 && { harnesses })
176
144
  };
177
145
  const registry = createDefaultRegistry();
@@ -234,9 +202,28 @@ export async function runSetup(
234
202
  }
235
203
 
236
204
  if (logicalConfig.knowledge_sources.length > 0) {
237
- clack.log.info(
238
- "Knowledge sources selected. Initialize them separately:\n npx @codemcp/knowledge init"
239
- );
205
+ const initCommands = logicalConfig.knowledge_sources
206
+ .map((s) => ` npx @codemcp/knowledge init ${s.name}`)
207
+ .join("\n");
208
+ const confirmInit = await clack.confirm({
209
+ message: `Initialize ${logicalConfig.knowledge_sources.length} knowledge source(s) now?`,
210
+ initialValue: false
211
+ });
212
+
213
+ if (typeof confirmInit === "symbol") {
214
+ clack.cancel("Setup cancelled.");
215
+ return;
216
+ }
217
+
218
+ if (confirmInit) {
219
+ await installKnowledge(logicalConfig.knowledge_sources, projectRoot, {
220
+ force: true
221
+ });
222
+ } else {
223
+ clack.log.info(
224
+ `Knowledge sources configured. Initialize them when ready:\n${initCommands}`
225
+ );
226
+ }
240
227
  }
241
228
 
242
229
  for (const note of logicalConfig.setup_notes) {