@codemcp/ade 0.6.1 → 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 (47) hide show
  1. package/.beads/issues.jsonl +37 -0
  2. package/.beads/last-touched +1 -1
  3. package/.kiro/agents/ade.json +10 -2
  4. package/.kiro/settings/mcp.json +6 -1
  5. package/.opencode/agents/ade.md +7 -2
  6. package/.vibe/beads-state-ade-extension-override-skills-d44z9p.json +29 -0
  7. package/.vibe/beads-state-ade-fix-docset-writing-37fuoj.json +29 -0
  8. package/.vibe/development-plan-extension-override-skills.md +110 -0
  9. package/.vibe/development-plan-fix-docset-writing.md +77 -0
  10. package/.vibe/docs/architecture.md +201 -0
  11. package/.vibe/docs/design.md +179 -0
  12. package/.vibe/docs/requirements.md +17 -0
  13. package/ade.extensions.mjs +13 -15
  14. package/config.lock.yaml +6 -1
  15. package/docs/CLI-PRD.md +38 -40
  16. package/docs/CLI-design.md +47 -57
  17. package/docs/guide/extensions.md +6 -14
  18. package/package.json +1 -1
  19. package/packages/cli/dist/index.js +15213 -5579
  20. package/packages/cli/package.json +1 -1
  21. package/packages/cli/src/commands/conventions.integration.spec.ts +29 -4
  22. package/packages/cli/src/commands/extensions.integration.spec.ts +26 -4
  23. package/packages/cli/src/commands/install.ts +2 -0
  24. package/packages/cli/src/commands/knowledge-docset.integration.spec.ts +179 -0
  25. package/packages/cli/src/commands/knowledge.integration.spec.ts +24 -36
  26. package/packages/cli/src/commands/setup.spec.ts +1 -101
  27. package/packages/cli/src/commands/setup.ts +23 -36
  28. package/packages/cli/src/knowledge-installer.spec.ts +43 -3
  29. package/packages/cli/src/knowledge-installer.ts +12 -9
  30. package/packages/core/package.json +1 -1
  31. package/packages/core/src/catalog/catalog.spec.ts +75 -43
  32. package/packages/core/src/catalog/facets/architecture.ts +89 -58
  33. package/packages/core/src/catalog/facets/practices.ts +9 -8
  34. package/packages/core/src/index.ts +4 -4
  35. package/packages/core/src/registry.spec.ts +1 -1
  36. package/packages/core/src/registry.ts +2 -2
  37. package/packages/core/src/resolver.spec.ts +391 -154
  38. package/packages/core/src/resolver.ts +16 -54
  39. package/packages/core/src/types.ts +19 -10
  40. package/packages/core/src/writers/docset.spec.ts +40 -0
  41. package/packages/core/src/writers/docset.ts +24 -0
  42. package/packages/core/src/writers/skills.spec.ts +46 -0
  43. package/packages/harnesses/package.json +1 -1
  44. package/packages/harnesses/src/writers/opencode.spec.ts +3 -5
  45. package/packages/harnesses/src/writers/opencode.ts +3 -4
  46. package/packages/core/src/writers/knowledge.spec.ts +0 -26
  47. 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 {
@@ -246,6 +247,336 @@ describe("resolve", () => {
246
247
  );
247
248
  expect(agentskills).toBeUndefined();
248
249
  });
250
+
251
+ it("removes a skill when another skill declares it in replaces", async () => {
252
+ const skillsCatalog: Catalog = {
253
+ facets: [
254
+ {
255
+ id: "process",
256
+ label: "Process",
257
+ description: "Process",
258
+ required: true,
259
+ options: [
260
+ {
261
+ id: "base",
262
+ label: "Base",
263
+ description: "Base process",
264
+ recipe: [
265
+ {
266
+ writer: "skills",
267
+ config: {
268
+ skills: [
269
+ {
270
+ name: "architecture",
271
+ description: "Generic architecture skill",
272
+ body: "Generic architecture content."
273
+ }
274
+ ]
275
+ }
276
+ }
277
+ ]
278
+ }
279
+ ]
280
+ },
281
+ {
282
+ id: "architecture",
283
+ label: "Architecture",
284
+ description: "Stack",
285
+ required: false,
286
+ options: [
287
+ {
288
+ id: "sabdx",
289
+ label: "SABDX",
290
+ description: "SABDX frontend",
291
+ recipe: [
292
+ {
293
+ writer: "skills",
294
+ config: {
295
+ skills: [
296
+ {
297
+ name: "sabdx-architecture",
298
+ description: "SABDX architecture skill",
299
+ body: "SABDX architecture content.",
300
+ replaces: ["architecture"]
301
+ }
302
+ ]
303
+ }
304
+ }
305
+ ]
306
+ }
307
+ ]
308
+ }
309
+ ]
310
+ };
311
+
312
+ const userConfig: UserConfig = {
313
+ choices: { process: "base", architecture: "sabdx" }
314
+ };
315
+
316
+ const result = await resolve(userConfig, skillsCatalog, registry);
317
+
318
+ expect(result.skills).toHaveLength(1);
319
+ expect(result.skills[0].name).toBe("sabdx-architecture");
320
+ expect(
321
+ result.skills.find((s) => s.name === "architecture")
322
+ ).toBeUndefined();
323
+ });
324
+
325
+ it("removes multiple skills when a single skill declares several replaces entries", async () => {
326
+ const skillsCatalog: Catalog = {
327
+ facets: [
328
+ {
329
+ id: "process",
330
+ label: "Process",
331
+ description: "Process",
332
+ required: true,
333
+ options: [
334
+ {
335
+ id: "base",
336
+ label: "Base",
337
+ description: "Base",
338
+ recipe: [
339
+ {
340
+ writer: "skills",
341
+ config: {
342
+ skills: [
343
+ {
344
+ name: "coding",
345
+ description: "Generic coding",
346
+ body: "Generic coding."
347
+ },
348
+ {
349
+ name: "testing",
350
+ description: "Generic testing",
351
+ body: "Generic testing."
352
+ }
353
+ ]
354
+ }
355
+ }
356
+ ]
357
+ }
358
+ ]
359
+ },
360
+ {
361
+ id: "architecture",
362
+ label: "Architecture",
363
+ description: "Stack",
364
+ required: false,
365
+ options: [
366
+ {
367
+ id: "ext",
368
+ label: "Extension",
369
+ description: "Extension",
370
+ recipe: [
371
+ {
372
+ writer: "skills",
373
+ config: {
374
+ skills: [
375
+ {
376
+ name: "ext-all",
377
+ description: "Replaces both",
378
+ body: "Extension content.",
379
+ replaces: ["coding", "testing"]
380
+ }
381
+ ]
382
+ }
383
+ }
384
+ ]
385
+ }
386
+ ]
387
+ }
388
+ ]
389
+ };
390
+
391
+ const userConfig: UserConfig = {
392
+ choices: { process: "base", architecture: "ext" }
393
+ };
394
+
395
+ const result = await resolve(userConfig, skillsCatalog, registry);
396
+
397
+ expect(result.skills).toHaveLength(1);
398
+ expect(result.skills[0].name).toBe("ext-all");
399
+ });
400
+
401
+ it("keeps all skills when no replaces are declared", async () => {
402
+ const skillsCatalog: Catalog = {
403
+ facets: [
404
+ {
405
+ id: "process",
406
+ label: "Process",
407
+ description: "Process",
408
+ required: true,
409
+ options: [
410
+ {
411
+ id: "base",
412
+ label: "Base",
413
+ description: "Base",
414
+ recipe: [
415
+ {
416
+ writer: "skills",
417
+ config: {
418
+ skills: [
419
+ { name: "skill-a", description: "A", body: "Body A." },
420
+ { name: "skill-b", description: "B", body: "Body B." }
421
+ ]
422
+ }
423
+ }
424
+ ]
425
+ }
426
+ ]
427
+ }
428
+ ]
429
+ };
430
+
431
+ const userConfig: UserConfig = { choices: { process: "base" } };
432
+ const result = await resolve(userConfig, skillsCatalog, registry);
433
+
434
+ expect(result.skills).toHaveLength(2);
435
+ expect(result.skills.map((s) => s.name)).toEqual(["skill-a", "skill-b"]);
436
+ });
437
+
438
+ it("deduplicates skills by name — last writer wins", async () => {
439
+ const skillsCatalog: Catalog = {
440
+ facets: [
441
+ {
442
+ id: "process",
443
+ label: "Process",
444
+ description: "Process",
445
+ required: true,
446
+ options: [
447
+ {
448
+ id: "base",
449
+ label: "Base",
450
+ description: "Base",
451
+ recipe: [
452
+ {
453
+ writer: "skills",
454
+ config: {
455
+ skills: [
456
+ {
457
+ name: "shared-skill",
458
+ description: "First",
459
+ body: "First body."
460
+ }
461
+ ]
462
+ }
463
+ }
464
+ ]
465
+ }
466
+ ]
467
+ },
468
+ {
469
+ id: "architecture",
470
+ label: "Architecture",
471
+ description: "Stack",
472
+ required: false,
473
+ options: [
474
+ {
475
+ id: "ext",
476
+ label: "Extension",
477
+ description: "Extension",
478
+ recipe: [
479
+ {
480
+ writer: "skills",
481
+ config: {
482
+ skills: [
483
+ {
484
+ name: "shared-skill",
485
+ description: "Second",
486
+ body: "Second body."
487
+ }
488
+ ]
489
+ }
490
+ }
491
+ ]
492
+ }
493
+ ]
494
+ }
495
+ ]
496
+ };
497
+
498
+ const userConfig: UserConfig = {
499
+ choices: { process: "base", architecture: "ext" }
500
+ };
501
+
502
+ const result = await resolve(userConfig, skillsCatalog, registry);
503
+
504
+ expect(result.skills).toHaveLength(1);
505
+ expect(result.skills[0]).toMatchObject({
506
+ name: "shared-skill",
507
+ body: "Second body."
508
+ });
509
+ });
510
+
511
+ it("works with external skills that declare replaces", async () => {
512
+ const skillsCatalog: Catalog = {
513
+ facets: [
514
+ {
515
+ id: "process",
516
+ label: "Process",
517
+ description: "Process",
518
+ required: true,
519
+ options: [
520
+ {
521
+ id: "base",
522
+ label: "Base",
523
+ description: "Base",
524
+ recipe: [
525
+ {
526
+ writer: "skills",
527
+ config: {
528
+ skills: [
529
+ {
530
+ name: "tdd",
531
+ description: "Generic TDD",
532
+ body: "Generic TDD."
533
+ }
534
+ ]
535
+ }
536
+ }
537
+ ]
538
+ }
539
+ ]
540
+ },
541
+ {
542
+ id: "architecture",
543
+ label: "Architecture",
544
+ description: "Stack",
545
+ required: false,
546
+ options: [
547
+ {
548
+ id: "ext",
549
+ label: "Extension",
550
+ description: "Extension",
551
+ recipe: [
552
+ {
553
+ writer: "skills",
554
+ config: {
555
+ skills: [
556
+ {
557
+ name: "ext-tdd",
558
+ source: "org/repo/skills/ext-tdd",
559
+ replaces: ["tdd"]
560
+ }
561
+ ]
562
+ }
563
+ }
564
+ ]
565
+ }
566
+ ]
567
+ }
568
+ ]
569
+ };
570
+
571
+ const userConfig: UserConfig = {
572
+ choices: { process: "base", architecture: "ext" }
573
+ };
574
+
575
+ const result = await resolve(userConfig, skillsCatalog, registry);
576
+
577
+ expect(result.skills).toHaveLength(1);
578
+ expect(result.skills[0].name).toBe("ext-tdd");
579
+ });
249
580
  });
250
581
 
251
582
  describe("setup_notes merging", () => {
@@ -283,7 +614,7 @@ describe("resolve", () => {
283
614
  });
284
615
  });
285
616
 
286
- describe("docset collection", () => {
617
+ describe("docset collection via recipe writer", () => {
287
618
  it("collects docsets from selected options into knowledge_sources", async () => {
288
619
  const docsetCatalog: Catalog = {
289
620
  facets: [
@@ -297,13 +628,15 @@ describe("resolve", () => {
297
628
  id: "react",
298
629
  label: "React",
299
630
  description: "React framework",
300
- recipe: [],
301
- docsets: [
631
+ recipe: [
302
632
  {
303
- id: "react-docs",
304
- label: "React Reference",
305
- origin: "https://github.com/facebook/react.git",
306
- 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
+ }
307
640
  }
308
641
  ]
309
642
  }
@@ -312,8 +645,11 @@ describe("resolve", () => {
312
645
  ]
313
646
  };
314
647
 
648
+ const reg = createRegistry();
649
+ registerProvisionWriter(reg, docsetWriter);
650
+
315
651
  const userConfig: UserConfig = { choices: { arch: "react" } };
316
- const result = await resolve(userConfig, docsetCatalog, registry);
652
+ const result = await resolve(userConfig, docsetCatalog, reg);
317
653
 
318
654
  expect(result.knowledge_sources).toHaveLength(1);
319
655
  expect(result.knowledge_sources[0]).toEqual({
@@ -323,7 +659,7 @@ describe("resolve", () => {
323
659
  });
324
660
  });
325
661
 
326
- 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 () => {
327
663
  const docsetCatalog: Catalog = {
328
664
  facets: [
329
665
  {
@@ -337,13 +673,15 @@ describe("resolve", () => {
337
673
  id: "react",
338
674
  label: "React",
339
675
  description: "React",
340
- recipe: [],
341
- docsets: [
676
+ recipe: [
342
677
  {
343
- id: "react-docs",
344
- label: "React Reference",
345
- origin: "https://github.com/facebook/react.git",
346
- 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
+ }
347
685
  }
348
686
  ]
349
687
  },
@@ -351,19 +689,24 @@ describe("resolve", () => {
351
689
  id: "nextjs",
352
690
  label: "Next.js",
353
691
  description: "Next.js",
354
- recipe: [],
355
- docsets: [
692
+ recipe: [
356
693
  {
357
- id: "react-docs",
358
- label: "React Reference",
359
- origin: "https://github.com/facebook/react.git",
360
- 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
+ }
361
701
  },
362
702
  {
363
- id: "nextjs-docs",
364
- label: "Next.js Docs",
365
- origin: "https://nextjs.org/docs",
366
- 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
+ }
367
710
  }
368
711
  ]
369
712
  }
@@ -372,59 +715,19 @@ describe("resolve", () => {
372
715
  ]
373
716
  };
374
717
 
375
- const userConfig: UserConfig = {
376
- choices: { stack: ["react", "nextjs"] }
377
- };
378
- const result = await resolve(userConfig, docsetCatalog, registry);
379
-
380
- expect(result.knowledge_sources).toHaveLength(2);
381
- const ids = result.knowledge_sources.map((ks) => ks.name);
382
- expect(ids).toContain("react-docs");
383
- expect(ids).toContain("nextjs-docs");
384
- });
385
-
386
- it("filters out excluded_docsets", async () => {
387
- const docsetCatalog: Catalog = {
388
- facets: [
389
- {
390
- id: "arch",
391
- label: "Architecture",
392
- description: "Stack",
393
- required: false,
394
- options: [
395
- {
396
- id: "react",
397
- label: "React",
398
- description: "React",
399
- recipe: [],
400
- docsets: [
401
- {
402
- id: "react-docs",
403
- label: "React Reference",
404
- origin: "https://github.com/facebook/react.git",
405
- description: "React docs"
406
- },
407
- {
408
- id: "react-tutorial",
409
- label: "React Tutorial",
410
- origin: "https://github.com/reactjs/react.dev.git",
411
- description: "React tutorial"
412
- }
413
- ]
414
- }
415
- ]
416
- }
417
- ]
418
- };
718
+ const reg = createRegistry();
719
+ registerProvisionWriter(reg, docsetWriter);
419
720
 
420
721
  const userConfig: UserConfig = {
421
- choices: { arch: "react" },
422
- excluded_docsets: ["react-tutorial"]
722
+ choices: { stack: ["react", "nextjs"] }
423
723
  };
424
- const result = await resolve(userConfig, docsetCatalog, registry);
724
+ const result = await resolve(userConfig, docsetCatalog, reg);
425
725
 
426
- expect(result.knowledge_sources).toHaveLength(1);
427
- 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");
428
731
  });
429
732
 
430
733
  it("adds knowledge-server MCP entry when knowledge_sources are present", async () => {
@@ -440,13 +743,15 @@ describe("resolve", () => {
440
743
  id: "react",
441
744
  label: "React",
442
745
  description: "React",
443
- recipe: [],
444
- docsets: [
746
+ recipe: [
445
747
  {
446
- id: "react-docs",
447
- label: "React Reference",
448
- origin: "https://github.com/facebook/react.git",
449
- 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
+ }
450
755
  }
451
756
  ]
452
757
  }
@@ -455,8 +760,11 @@ describe("resolve", () => {
455
760
  ]
456
761
  };
457
762
 
763
+ const reg = createRegistry();
764
+ registerProvisionWriter(reg, docsetWriter);
765
+
458
766
  const userConfig: UserConfig = { choices: { arch: "react" } };
459
- const result = await resolve(userConfig, docsetCatalog, registry);
767
+ const result = await resolve(userConfig, docsetCatalog, reg);
460
768
 
461
769
  const knowledgeServer = result.mcp_servers.find(
462
770
  (s) => s.ref === "knowledge"
@@ -478,7 +786,7 @@ describe("resolve", () => {
478
786
  expect(knowledgeServer).toBeUndefined();
479
787
  });
480
788
 
481
- it("produces no knowledge_sources when option has no docsets", async () => {
789
+ it("produces no knowledge_sources when option has no docset provisions", async () => {
482
790
  const userConfig: UserConfig = {
483
791
  choices: { process: "native-agents-md" }
484
792
  };
@@ -488,77 +796,6 @@ describe("resolve", () => {
488
796
  });
489
797
  });
490
798
 
491
- describe("collectDocsets", () => {
492
- it("returns deduplicated docsets for given choices", () => {
493
- const docsetCatalog: Catalog = {
494
- facets: [
495
- {
496
- id: "stack",
497
- label: "Stack",
498
- description: "Stack",
499
- required: false,
500
- multiSelect: true,
501
- options: [
502
- {
503
- id: "a",
504
- label: "A",
505
- description: "A",
506
- recipe: [],
507
- docsets: [
508
- {
509
- id: "shared",
510
- label: "Shared",
511
- origin: "https://x",
512
- description: "shared"
513
- },
514
- {
515
- id: "a-only",
516
- label: "A Only",
517
- origin: "https://a",
518
- description: "a"
519
- }
520
- ]
521
- },
522
- {
523
- id: "b",
524
- label: "B",
525
- description: "B",
526
- recipe: [],
527
- docsets: [
528
- {
529
- id: "shared",
530
- label: "Shared",
531
- origin: "https://x",
532
- description: "shared"
533
- },
534
- {
535
- id: "b-only",
536
- label: "B Only",
537
- origin: "https://b",
538
- description: "b"
539
- }
540
- ]
541
- }
542
- ]
543
- }
544
- ]
545
- };
546
-
547
- const result = collectDocsets({ stack: ["a", "b"] }, docsetCatalog);
548
-
549
- expect(result).toHaveLength(3);
550
- const ids = result.map((d) => d.id);
551
- expect(ids).toContain("shared");
552
- expect(ids).toContain("a-only");
553
- expect(ids).toContain("b-only");
554
- });
555
-
556
- it("returns empty array when no options have docsets", () => {
557
- const result = collectDocsets({ process: "native-agents-md" }, catalog);
558
- expect(result).toEqual([]);
559
- });
560
- });
561
-
562
799
  describe("MCP server dedup by ref", () => {
563
800
  it("deduplicates mcp_servers by ref, keeping the last one", async () => {
564
801
  // Create a custom registry with a writer that produces duplicate refs