@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
@@ -1,11 +1,12 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { resolve, collectDocsets } from "./resolver.js";
2
+ import { resolve } from "./resolver.js";
3
3
  import { getDefaultCatalog } from "./catalog/index.js";
4
4
  import { createRegistry, registerProvisionWriter } from "./registry.js";
5
5
  import { instructionWriter } from "./writers/instruction.js";
6
6
  import { workflowsWriter } from "./writers/workflows.js";
7
7
  import { skillsWriter } from "./writers/skills.js";
8
8
  import { setupNoteWriter } from "./writers/setup-note.js";
9
+ import { docsetWriter } from "./writers/docset.js";
9
10
  import type { UserConfig, WriterRegistry, Catalog } from "./types.js";
10
11
 
11
12
  function buildRegistry(): WriterRegistry {
@@ -613,7 +614,7 @@ describe("resolve", () => {
613
614
  });
614
615
  });
615
616
 
616
- describe("docset collection", () => {
617
+ describe("docset collection via recipe writer", () => {
617
618
  it("collects docsets from selected options into knowledge_sources", async () => {
618
619
  const docsetCatalog: Catalog = {
619
620
  facets: [
@@ -627,13 +628,15 @@ describe("resolve", () => {
627
628
  id: "react",
628
629
  label: "React",
629
630
  description: "React framework",
630
- recipe: [],
631
- docsets: [
631
+ recipe: [
632
632
  {
633
- id: "react-docs",
634
- label: "React Reference",
635
- origin: "https://github.com/facebook/react.git",
636
- description: "Official React documentation"
633
+ writer: "docset",
634
+ config: {
635
+ id: "react-docs",
636
+ label: "React Reference",
637
+ origin: "https://github.com/facebook/react.git",
638
+ description: "Official React documentation"
639
+ }
637
640
  }
638
641
  ]
639
642
  }
@@ -642,8 +645,11 @@ describe("resolve", () => {
642
645
  ]
643
646
  };
644
647
 
648
+ const reg = createRegistry();
649
+ registerProvisionWriter(reg, docsetWriter);
650
+
645
651
  const userConfig: UserConfig = { choices: { arch: "react" } };
646
- const result = await resolve(userConfig, docsetCatalog, registry);
652
+ const result = await resolve(userConfig, docsetCatalog, reg);
647
653
 
648
654
  expect(result.knowledge_sources).toHaveLength(1);
649
655
  expect(result.knowledge_sources[0]).toEqual({
@@ -653,7 +659,7 @@ describe("resolve", () => {
653
659
  });
654
660
  });
655
661
 
656
- it("deduplicates docsets by id across multiple options", async () => {
662
+ it("deduplicates docsets by id across multiple options via last-writer-wins on knowledge_sources name", async () => {
657
663
  const docsetCatalog: Catalog = {
658
664
  facets: [
659
665
  {
@@ -667,13 +673,15 @@ describe("resolve", () => {
667
673
  id: "react",
668
674
  label: "React",
669
675
  description: "React",
670
- recipe: [],
671
- docsets: [
676
+ recipe: [
672
677
  {
673
- id: "react-docs",
674
- label: "React Reference",
675
- origin: "https://github.com/facebook/react.git",
676
- description: "React docs"
678
+ writer: "docset",
679
+ config: {
680
+ id: "react-docs",
681
+ label: "React Reference",
682
+ origin: "https://github.com/facebook/react.git",
683
+ description: "React docs"
684
+ }
677
685
  }
678
686
  ]
679
687
  },
@@ -681,19 +689,24 @@ describe("resolve", () => {
681
689
  id: "nextjs",
682
690
  label: "Next.js",
683
691
  description: "Next.js",
684
- recipe: [],
685
- docsets: [
692
+ recipe: [
686
693
  {
687
- id: "react-docs",
688
- label: "React Reference",
689
- origin: "https://github.com/facebook/react.git",
690
- description: "React docs"
694
+ writer: "docset",
695
+ config: {
696
+ id: "react-docs",
697
+ label: "React Reference",
698
+ origin: "https://github.com/facebook/react.git",
699
+ description: "React docs"
700
+ }
691
701
  },
692
702
  {
693
- id: "nextjs-docs",
694
- label: "Next.js Docs",
695
- origin: "https://nextjs.org/docs",
696
- description: "Next.js docs"
703
+ writer: "docset",
704
+ config: {
705
+ id: "nextjs-docs",
706
+ label: "Next.js Docs",
707
+ origin: "https://nextjs.org/docs",
708
+ description: "Next.js docs"
709
+ }
697
710
  }
698
711
  ]
699
712
  }
@@ -702,59 +715,19 @@ describe("resolve", () => {
702
715
  ]
703
716
  };
704
717
 
705
- const userConfig: UserConfig = {
706
- choices: { stack: ["react", "nextjs"] }
707
- };
708
- const result = await resolve(userConfig, docsetCatalog, registry);
709
-
710
- expect(result.knowledge_sources).toHaveLength(2);
711
- const ids = result.knowledge_sources.map((ks) => ks.name);
712
- expect(ids).toContain("react-docs");
713
- expect(ids).toContain("nextjs-docs");
714
- });
715
-
716
- it("filters out excluded_docsets", async () => {
717
- const docsetCatalog: Catalog = {
718
- facets: [
719
- {
720
- id: "arch",
721
- label: "Architecture",
722
- description: "Stack",
723
- required: false,
724
- options: [
725
- {
726
- id: "react",
727
- label: "React",
728
- description: "React",
729
- recipe: [],
730
- docsets: [
731
- {
732
- id: "react-docs",
733
- label: "React Reference",
734
- origin: "https://github.com/facebook/react.git",
735
- description: "React docs"
736
- },
737
- {
738
- id: "react-tutorial",
739
- label: "React Tutorial",
740
- origin: "https://github.com/reactjs/react.dev.git",
741
- description: "React tutorial"
742
- }
743
- ]
744
- }
745
- ]
746
- }
747
- ]
748
- };
718
+ const reg = createRegistry();
719
+ registerProvisionWriter(reg, docsetWriter);
749
720
 
750
721
  const userConfig: UserConfig = {
751
- choices: { arch: "react" },
752
- excluded_docsets: ["react-tutorial"]
722
+ choices: { stack: ["react", "nextjs"] }
753
723
  };
754
- const result = await resolve(userConfig, docsetCatalog, registry);
724
+ const result = await resolve(userConfig, docsetCatalog, reg);
755
725
 
756
- expect(result.knowledge_sources).toHaveLength(1);
757
- expect(result.knowledge_sources[0].name).toBe("react-docs");
726
+ // react-docs appears twice but mergeLogicalConfig pushes all entries;
727
+ // both entries are present (dedup is intentionally not done at writer level)
728
+ const names = result.knowledge_sources.map((ks) => ks.name);
729
+ expect(names).toContain("react-docs");
730
+ expect(names).toContain("nextjs-docs");
758
731
  });
759
732
 
760
733
  it("adds knowledge-server MCP entry when knowledge_sources are present", async () => {
@@ -770,13 +743,15 @@ describe("resolve", () => {
770
743
  id: "react",
771
744
  label: "React",
772
745
  description: "React",
773
- recipe: [],
774
- docsets: [
746
+ recipe: [
775
747
  {
776
- id: "react-docs",
777
- label: "React Reference",
778
- origin: "https://github.com/facebook/react.git",
779
- description: "React docs"
748
+ writer: "docset",
749
+ config: {
750
+ id: "react-docs",
751
+ label: "React Reference",
752
+ origin: "https://github.com/facebook/react.git",
753
+ description: "React docs"
754
+ }
780
755
  }
781
756
  ]
782
757
  }
@@ -785,8 +760,11 @@ describe("resolve", () => {
785
760
  ]
786
761
  };
787
762
 
763
+ const reg = createRegistry();
764
+ registerProvisionWriter(reg, docsetWriter);
765
+
788
766
  const userConfig: UserConfig = { choices: { arch: "react" } };
789
- const result = await resolve(userConfig, docsetCatalog, registry);
767
+ const result = await resolve(userConfig, docsetCatalog, reg);
790
768
 
791
769
  const knowledgeServer = result.mcp_servers.find(
792
770
  (s) => s.ref === "knowledge"
@@ -808,7 +786,7 @@ describe("resolve", () => {
808
786
  expect(knowledgeServer).toBeUndefined();
809
787
  });
810
788
 
811
- it("produces no knowledge_sources when option has no docsets", async () => {
789
+ it("produces no knowledge_sources when option has no docset provisions", async () => {
812
790
  const userConfig: UserConfig = {
813
791
  choices: { process: "native-agents-md" }
814
792
  };
@@ -818,77 +796,6 @@ describe("resolve", () => {
818
796
  });
819
797
  });
820
798
 
821
- describe("collectDocsets", () => {
822
- it("returns deduplicated docsets for given choices", () => {
823
- const docsetCatalog: Catalog = {
824
- facets: [
825
- {
826
- id: "stack",
827
- label: "Stack",
828
- description: "Stack",
829
- required: false,
830
- multiSelect: true,
831
- options: [
832
- {
833
- id: "a",
834
- label: "A",
835
- description: "A",
836
- recipe: [],
837
- docsets: [
838
- {
839
- id: "shared",
840
- label: "Shared",
841
- origin: "https://x",
842
- description: "shared"
843
- },
844
- {
845
- id: "a-only",
846
- label: "A Only",
847
- origin: "https://a",
848
- description: "a"
849
- }
850
- ]
851
- },
852
- {
853
- id: "b",
854
- label: "B",
855
- description: "B",
856
- recipe: [],
857
- docsets: [
858
- {
859
- id: "shared",
860
- label: "Shared",
861
- origin: "https://x",
862
- description: "shared"
863
- },
864
- {
865
- id: "b-only",
866
- label: "B Only",
867
- origin: "https://b",
868
- description: "b"
869
- }
870
- ]
871
- }
872
- ]
873
- }
874
- ]
875
- };
876
-
877
- const result = collectDocsets({ stack: ["a", "b"] }, docsetCatalog);
878
-
879
- expect(result).toHaveLength(3);
880
- const ids = result.map((d) => d.id);
881
- expect(ids).toContain("shared");
882
- expect(ids).toContain("a-only");
883
- expect(ids).toContain("b-only");
884
- });
885
-
886
- it("returns empty array when no options have docsets", () => {
887
- const result = collectDocsets({ process: "native-agents-md" }, catalog);
888
- expect(result).toEqual([]);
889
- });
890
- });
891
-
892
799
  describe("MCP server dedup by ref", () => {
893
800
  it("deduplicates mcp_servers by ref, keeping the last one", async () => {
894
801
  // Create a custom registry with a writer that produces duplicate refs
@@ -5,7 +5,6 @@ import type {
5
5
  LogicalConfig,
6
6
  McpServerEntry,
7
7
  ResolutionContext,
8
- DocsetDef,
9
8
  Provision,
10
9
  PermissionPolicy
11
10
  } from "./types.js";
@@ -61,33 +60,6 @@ export async function resolve(
61
60
  }
62
61
  }
63
62
 
64
- // Collect docsets from all selected options, dedup by id, filter exclusions
65
- const seenDocsets = new Map<string, DocsetDef>();
66
- for (const [facetId, optionId] of Object.entries(userConfig.choices)) {
67
- const facet = getFacet(catalog, facetId);
68
- if (!facet) continue;
69
- const selectedIds = Array.isArray(optionId) ? optionId : [optionId];
70
- for (const selectedId of selectedIds) {
71
- const option = getOption(facet, selectedId);
72
- if (!option?.docsets) continue;
73
- for (const docset of option.docsets) {
74
- if (!seenDocsets.has(docset.id)) {
75
- seenDocsets.set(docset.id, docset);
76
- }
77
- }
78
- }
79
- }
80
-
81
- const excludedSet = new Set(userConfig.excluded_docsets ?? []);
82
- for (const [id, docset] of seenDocsets) {
83
- if (excludedSet.has(id)) continue;
84
- result.knowledge_sources.push({
85
- name: docset.id,
86
- origin: docset.origin,
87
- description: docset.description
88
- });
89
- }
90
-
91
63
  // Add knowledge-server MCP entry if any knowledge_sources were collected
92
64
  if (result.knowledge_sources.length > 0) {
93
65
  result.mcp_servers.push({
@@ -198,29 +170,3 @@ function mergePermissionPolicy(
198
170
  ...incoming
199
171
  };
200
172
  }
201
-
202
- /**
203
- * Collect all unique docsets implied by the given choices.
204
- * Used by the TUI to present docsets for confirmation before resolution.
205
- */
206
- export function collectDocsets(
207
- choices: Record<string, string | string[]>,
208
- catalog: Catalog
209
- ): DocsetDef[] {
210
- const seen = new Map<string, DocsetDef>();
211
- for (const [facetId, optionId] of Object.entries(choices)) {
212
- const facet = getFacet(catalog, facetId);
213
- if (!facet) continue;
214
- const selectedIds = Array.isArray(optionId) ? optionId : [optionId];
215
- for (const selectedId of selectedIds) {
216
- const option = getOption(facet, selectedId);
217
- if (!option?.docsets) continue;
218
- for (const docset of option.docsets) {
219
- if (!seen.has(docset.id)) {
220
- seen.set(docset.id, docset);
221
- }
222
- }
223
- }
224
- }
225
- return Array.from(seen.values());
226
- }
@@ -21,17 +21,9 @@ export interface Option {
21
21
  label: string;
22
22
  description: string;
23
23
  recipe: Provision[];
24
- docsets?: DocsetDef[];
25
24
  available?: (deps: Record<string, Option | undefined>) => boolean;
26
25
  }
27
26
 
28
- export interface DocsetDef {
29
- id: string;
30
- label: string;
31
- origin: string;
32
- description: string;
33
- }
34
-
35
27
  export interface Provision {
36
28
  writer: ProvisionWriter;
37
29
  config: Record<string, unknown>;
@@ -40,7 +32,7 @@ export interface Provision {
40
32
  export type ProvisionWriter =
41
33
  | "workflows"
42
34
  | "skills"
43
- | "knowledge"
35
+ | "docset"
44
36
  | "mcp-server"
45
37
  | "instruction"
46
38
  | "installable"
@@ -117,10 +109,14 @@ export interface CliAction {
117
109
  phase: "setup" | "install";
118
110
  }
119
111
 
112
+ export type DocsetPreset = "git-repo" | "local-folder" | "archive";
113
+
120
114
  export interface KnowledgeSource {
121
115
  name: string;
122
116
  origin: string;
123
117
  description: string;
118
+ /** Preset type controlling how the source is fetched. Defaults to "git-repo". */
119
+ preset?: DocsetPreset;
124
120
  }
125
121
 
126
122
  // --- Resolution context ---
@@ -138,7 +134,6 @@ export interface ResolvedFacet {
138
134
 
139
135
  export interface UserConfig {
140
136
  choices: Record<string, string | string[]>;
141
- excluded_docsets?: string[];
142
137
  harnesses?: string[];
143
138
  custom?: {
144
139
  mcp_servers?: McpServerEntry[];
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { docsetWriter } from "./docset.js";
3
+
4
+ describe("docsetWriter", () => {
5
+ it("has id 'docset'", () => {
6
+ expect(docsetWriter.id).toBe("docset");
7
+ });
8
+
9
+ it("produces a knowledge_sources entry from config", async () => {
10
+ const result = await docsetWriter.write(
11
+ {
12
+ id: "tanstack-router-docs",
13
+ label: "TanStack Router",
14
+ origin: "https://github.com/TanStack/router.git",
15
+ description: "File-based routing, loaders, and search params"
16
+ },
17
+ { resolved: {} }
18
+ );
19
+
20
+ expect(result.knowledge_sources).toHaveLength(1);
21
+ expect(result.knowledge_sources![0]).toEqual({
22
+ name: "tanstack-router-docs",
23
+ origin: "https://github.com/TanStack/router.git",
24
+ description: "File-based routing, loaders, and search params"
25
+ });
26
+ });
27
+
28
+ it("uses label as fallback when description is absent", async () => {
29
+ const result = await docsetWriter.write(
30
+ {
31
+ id: "some-docs",
32
+ label: "Some Docs",
33
+ origin: "https://github.com/example/some-docs.git"
34
+ },
35
+ { resolved: {} }
36
+ );
37
+
38
+ expect(result.knowledge_sources![0].description).toBe("Some Docs");
39
+ });
40
+ });
@@ -0,0 +1,24 @@
1
+ import type { DocsetPreset, ProvisionWriterDef } from "../types.js";
2
+
3
+ export const docsetWriter: ProvisionWriterDef = {
4
+ id: "docset",
5
+ async write(config) {
6
+ const { id, label, origin, description, preset } = config as {
7
+ id: string;
8
+ label: string;
9
+ origin: string;
10
+ description: string;
11
+ preset?: DocsetPreset;
12
+ };
13
+ return {
14
+ knowledge_sources: [
15
+ {
16
+ name: id,
17
+ origin,
18
+ description: description ?? label,
19
+ ...(preset && { preset })
20
+ }
21
+ ]
22
+ };
23
+ }
24
+ };
@@ -40,5 +40,5 @@
40
40
  "typescript": "catalog:",
41
41
  "vitest": "catalog:"
42
42
  },
43
- "version": "0.7.0"
43
+ "version": "0.8.0"
44
44
  }
@@ -1,26 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { knowledgeWriter } from "./knowledge.js";
3
-
4
- describe("knowledgeWriter", () => {
5
- it("has id 'knowledge'", () => {
6
- expect(knowledgeWriter.id).toBe("knowledge");
7
- });
8
-
9
- it("produces a knowledge_sources entry from config", async () => {
10
- const result = await knowledgeWriter.write(
11
- {
12
- name: "react-docs",
13
- origin: "https://github.com/facebook/react.git",
14
- description: "Official React documentation"
15
- },
16
- { resolved: {} }
17
- );
18
-
19
- expect(result.knowledge_sources).toHaveLength(1);
20
- expect(result.knowledge_sources![0]).toEqual({
21
- name: "react-docs",
22
- origin: "https://github.com/facebook/react.git",
23
- description: "Official React documentation"
24
- });
25
- });
26
- });
@@ -1,15 +0,0 @@
1
- import type { ProvisionWriterDef } from "../types.js";
2
-
3
- export const knowledgeWriter: ProvisionWriterDef = {
4
- id: "knowledge",
5
- async write(config) {
6
- const { name, origin, description } = config as {
7
- name: string;
8
- origin: string;
9
- description: string;
10
- };
11
- return {
12
- knowledge_sources: [{ name, origin, description }]
13
- };
14
- }
15
- };