@codemcp/ade-core 0.0.2

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 (71) hide show
  1. package/.prettierignore +1 -0
  2. package/.turbo/turbo-build.log +4 -0
  3. package/.turbo/turbo-format.log +6 -0
  4. package/.turbo/turbo-lint.log +4 -0
  5. package/.turbo/turbo-test.log +21 -0
  6. package/.turbo/turbo-typecheck.log +4 -0
  7. package/LICENSE +21 -0
  8. package/dist/catalog/facets/architecture.d.ts +2 -0
  9. package/dist/catalog/facets/architecture.js +424 -0
  10. package/dist/catalog/facets/backpressure.d.ts +2 -0
  11. package/dist/catalog/facets/backpressure.js +123 -0
  12. package/dist/catalog/facets/practices.d.ts +2 -0
  13. package/dist/catalog/facets/practices.js +163 -0
  14. package/dist/catalog/facets/process.d.ts +2 -0
  15. package/dist/catalog/facets/process.js +47 -0
  16. package/dist/catalog/index.d.ts +14 -0
  17. package/dist/catalog/index.js +71 -0
  18. package/dist/config.d.ts +5 -0
  19. package/dist/config.js +29 -0
  20. package/dist/index.d.ts +12 -0
  21. package/dist/index.js +6 -0
  22. package/dist/registry.d.ts +7 -0
  23. package/dist/registry.js +41 -0
  24. package/dist/resolver.d.ts +7 -0
  25. package/dist/resolver.js +142 -0
  26. package/dist/types.d.ts +110 -0
  27. package/dist/types.js +2 -0
  28. package/dist/writers/git-hooks.d.ts +2 -0
  29. package/dist/writers/git-hooks.js +7 -0
  30. package/dist/writers/instruction.d.ts +2 -0
  31. package/dist/writers/instruction.js +6 -0
  32. package/dist/writers/knowledge.d.ts +2 -0
  33. package/dist/writers/knowledge.js +9 -0
  34. package/dist/writers/setup-note.d.ts +2 -0
  35. package/dist/writers/setup-note.js +7 -0
  36. package/dist/writers/skills.d.ts +2 -0
  37. package/dist/writers/skills.js +7 -0
  38. package/dist/writers/workflows.d.ts +2 -0
  39. package/dist/writers/workflows.js +16 -0
  40. package/eslint.config.mjs +40 -0
  41. package/nodemon.json +7 -0
  42. package/package.json +34 -0
  43. package/src/catalog/catalog.spec.ts +531 -0
  44. package/src/catalog/facets/architecture.ts +438 -0
  45. package/src/catalog/facets/backpressure.ts +143 -0
  46. package/src/catalog/facets/practices.ts +173 -0
  47. package/src/catalog/facets/process.ts +50 -0
  48. package/src/catalog/index.ts +86 -0
  49. package/src/config.spec.ts +165 -0
  50. package/src/config.ts +39 -0
  51. package/src/index.ts +49 -0
  52. package/src/registry.spec.ts +144 -0
  53. package/src/registry.ts +68 -0
  54. package/src/resolver.spec.ts +581 -0
  55. package/src/resolver.ts +170 -0
  56. package/src/types.ts +151 -0
  57. package/src/writers/git-hooks.ts +9 -0
  58. package/src/writers/instruction.spec.ts +42 -0
  59. package/src/writers/instruction.ts +8 -0
  60. package/src/writers/knowledge.spec.ts +26 -0
  61. package/src/writers/knowledge.ts +15 -0
  62. package/src/writers/setup-note.ts +9 -0
  63. package/src/writers/skills.spec.ts +109 -0
  64. package/src/writers/skills.ts +9 -0
  65. package/src/writers/workflows.spec.ts +72 -0
  66. package/src/writers/workflows.ts +26 -0
  67. package/tsconfig.build.json +8 -0
  68. package/tsconfig.json +7 -0
  69. package/tsconfig.tsbuildinfo +1 -0
  70. package/tsconfig.vitest.json +7 -0
  71. package/vitest.config.ts +5 -0
@@ -0,0 +1,173 @@
1
+ import type { Facet } from "../../types.js";
2
+
3
+ export const practicesFacet: Facet = {
4
+ id: "practices",
5
+ label: "Practices",
6
+ description:
7
+ "Composable development practices — mix and match regardless of stack",
8
+ required: false,
9
+ multiSelect: true,
10
+ options: [
11
+ {
12
+ id: "conventional-commits",
13
+ label: "Conventional Commits",
14
+ description:
15
+ "Structured commit messages following the Conventional Commits specification",
16
+ recipe: [
17
+ {
18
+ writer: "skills",
19
+ config: {
20
+ skills: [
21
+ {
22
+ name: "conventional-commits",
23
+ description:
24
+ "Conventional Commits specification for structured commit messages",
25
+ body: [
26
+ "# Conventional Commits",
27
+ "",
28
+ "## Format",
29
+ "```",
30
+ "<type>[optional scope]: <description>",
31
+ "",
32
+ "[optional body]",
33
+ "",
34
+ "[optional footer(s)]",
35
+ "```",
36
+ "",
37
+ "## Types",
38
+ "- `feat`: A new feature (correlates with MINOR in SemVer)",
39
+ "- `fix`: A bug fix (correlates with PATCH in SemVer)",
40
+ "- `docs`: Documentation only changes",
41
+ "- `style`: Changes that do not affect the meaning of the code",
42
+ "- `refactor`: A code change that neither fixes a bug nor adds a feature",
43
+ "- `perf`: A code change that improves performance",
44
+ "- `test`: Adding missing tests or correcting existing tests",
45
+ "- `chore`: Changes to the build process or auxiliary tools",
46
+ "",
47
+ "## Rules",
48
+ "- Subject line must not exceed 72 characters",
49
+ '- Use imperative mood in the subject line ("add" not "added")',
50
+ "- Do not end the subject line with a period",
51
+ "- Separate subject from body with a blank line",
52
+ "- Use the body to explain what and why, not how",
53
+ "- `BREAKING CHANGE:` footer or `!` after type/scope for breaking changes"
54
+ ].join("\n")
55
+ }
56
+ ]
57
+ }
58
+ }
59
+ ],
60
+ docsets: [
61
+ {
62
+ id: "conventional-commits-spec",
63
+ label: "Conventional Commits Spec",
64
+ origin:
65
+ "https://github.com/conventional-commits/conventionalcommits.org.git",
66
+ description: "The Conventional Commits specification"
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ id: "tdd-london",
72
+ label: "TDD (London Style)",
73
+ description:
74
+ "Test-Driven Development using the London school (mockist) approach",
75
+ recipe: [
76
+ {
77
+ writer: "skills",
78
+ config: {
79
+ skills: [
80
+ {
81
+ name: "tdd-london",
82
+ description:
83
+ "London-school TDD methodology with outside-in design",
84
+ body: [
85
+ "# TDD — London Style (Mockist)",
86
+ "",
87
+ "## Core Cycle",
88
+ "1. **Red** — Write a failing test for the next behavior",
89
+ "2. **Green** — Write the minimum code to make the test pass",
90
+ "3. **Refactor** — Improve the code while keeping tests green",
91
+ "",
92
+ "## London School Principles",
93
+ "- Work **outside-in**: start from the outermost layer (API / UI) and drive inward",
94
+ "- **Mock collaborators**: each unit test isolates the unit under test by mocking its direct dependencies",
95
+ "- Discover interfaces through tests — let the test define the collaborator contract before implementing it",
96
+ "- Prefer **role-based interfaces** over concrete classes",
97
+ "",
98
+ "## Test Structure",
99
+ "- **Arrange**: Set up mocks and the unit under test",
100
+ "- **Act**: Call the method being tested",
101
+ "- **Assert**: Verify the unit's output and interactions with mocks",
102
+ "",
103
+ "## Guidelines",
104
+ "- One logical assertion per test",
105
+ '- Test names describe behavior, not methods (e.g. "notifies user when order is placed")',
106
+ "- Only mock types you own — wrap third-party APIs in adapters and mock those",
107
+ "- Use the test doubles: stubs for queries, mocks for commands",
108
+ "- Do not test implementation details — test observable behavior",
109
+ "- Refactor step is mandatory, not optional"
110
+ ].join("\n")
111
+ }
112
+ ]
113
+ }
114
+ }
115
+ ]
116
+ },
117
+ {
118
+ id: "adr-nygard",
119
+ label: "ADR (Nygard)",
120
+ description:
121
+ "Architecture Decision Records following Michael Nygard's template",
122
+ recipe: [
123
+ {
124
+ writer: "skills",
125
+ config: {
126
+ skills: [
127
+ {
128
+ name: "adr-nygard",
129
+ description:
130
+ "Architecture Decision Records following Nygard's lightweight template",
131
+ body: [
132
+ "# Architecture Decision Records (Nygard)",
133
+ "",
134
+ "## When to Write an ADR",
135
+ "- When making a significant architectural decision",
136
+ "- When choosing between multiple viable options",
137
+ "- When the decision will be hard to reverse",
138
+ '- When future developers will ask "why did we do this?"',
139
+ "",
140
+ "## Template",
141
+ "Store ADRs in `docs/adr/` as numbered markdown files: `NNNN-title-with-dashes.md`",
142
+ "",
143
+ "```markdown",
144
+ "# N. Title",
145
+ "",
146
+ "## Status",
147
+ "Proposed | Accepted | Deprecated | Superseded by [ADR-NNNN]",
148
+ "",
149
+ "## Context",
150
+ "What is the issue that we're seeing that is motivating this decision or change?",
151
+ "",
152
+ "## Decision",
153
+ "What is the change that we're proposing and/or doing?",
154
+ "",
155
+ "## Consequences",
156
+ "What becomes easier or more difficult to do because of this change?",
157
+ "```",
158
+ "",
159
+ "## Rules",
160
+ "- ADRs are immutable once accepted — supersede, don't edit",
161
+ "- Keep context focused on forces at play at the time of the decision",
162
+ "- Write consequences as both positive and negative impacts",
163
+ "- Number sequentially, never reuse numbers",
164
+ '- Title should be a short noun phrase (e.g. "Use PostgreSQL for persistence")'
165
+ ].join("\n")
166
+ }
167
+ ]
168
+ }
169
+ }
170
+ ]
171
+ }
172
+ ]
173
+ };
@@ -0,0 +1,50 @@
1
+ import type { Facet } from "../../types.js";
2
+
3
+ export const processFacet: Facet = {
4
+ id: "process",
5
+ label: "Process",
6
+ description: "How your AI agent receives and executes tasks",
7
+ required: true,
8
+ options: [
9
+ {
10
+ id: "codemcp-workflows",
11
+ label: "CodeMCP Workflows",
12
+ description:
13
+ "Use @codemcp/workflows to drive agent tasks with structured engineering workflows",
14
+ recipe: [
15
+ {
16
+ writer: "workflows",
17
+ config: {
18
+ package: "@codemcp/workflows-server@latest",
19
+ ref: "workflows"
20
+ }
21
+ },
22
+ {
23
+ writer: "instruction",
24
+ config: {
25
+ text: [
26
+ "You are an AI assistant that helps users develop software features using the workflows server.",
27
+ "IMPORTANT: Call whats_next() after each user message to get phase-specific instructions and maintain the development workflow.",
28
+ 'Each tool call returns a JSON response with an "instructions" field. Follow these instructions immediately after you receive them.',
29
+ "Use the development plan which you will retrieve via whats_next() to record important insights and decisions as per the structure of the plan.",
30
+ "Do not use your own task management tools."
31
+ ].join("\n")
32
+ }
33
+ }
34
+ ]
35
+ },
36
+ {
37
+ id: "native-agents-md",
38
+ label: "Native agents.md",
39
+ description: "Use a plain agents.md instruction file",
40
+ recipe: [
41
+ {
42
+ writer: "instruction",
43
+ config: {
44
+ text: "Read AGENTS.md for project conventions and task instructions."
45
+ }
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ };
@@ -0,0 +1,86 @@
1
+ import type { Catalog, Facet, Option } from "../types.js";
2
+ import { processFacet } from "./facets/process.js";
3
+ import { architectureFacet } from "./facets/architecture.js";
4
+ import { practicesFacet } from "./facets/practices.js";
5
+ import { backpressureFacet } from "./facets/backpressure.js";
6
+
7
+ export function getDefaultCatalog(): Catalog {
8
+ return {
9
+ facets: [processFacet, architectureFacet, practicesFacet, backpressureFacet]
10
+ };
11
+ }
12
+
13
+ export function getFacet(catalog: Catalog, id: string): Facet | undefined {
14
+ return catalog.facets.find((f) => f.id === id);
15
+ }
16
+
17
+ export function getOption(facet: Facet, id: string): Option | undefined {
18
+ return facet.options.find((o) => o.id === id);
19
+ }
20
+
21
+ /**
22
+ * Topologically sort facets so that dependency facets come before dependent ones.
23
+ * Uses Kahn's algorithm. Throws if a cycle is detected.
24
+ */
25
+ export function sortFacets(catalog: Catalog): Facet[] {
26
+ const facets = catalog.facets;
27
+ const idToFacet = new Map(facets.map((f) => [f.id, f]));
28
+
29
+ // Build in-degree and adjacency (dependsOn edge: dep → dependent)
30
+ const inDegree = new Map<string, number>(facets.map((f) => [f.id, 0]));
31
+ const dependents = new Map<string, string[]>(facets.map((f) => [f.id, []]));
32
+
33
+ for (const facet of facets) {
34
+ for (const dep of facet.dependsOn ?? []) {
35
+ if (idToFacet.has(dep)) {
36
+ inDegree.set(facet.id, (inDegree.get(facet.id) ?? 0) + 1);
37
+ dependents.get(dep)!.push(facet.id);
38
+ }
39
+ }
40
+ }
41
+
42
+ const queue: Facet[] = facets.filter((f) => (inDegree.get(f.id) ?? 0) === 0);
43
+ const result: Facet[] = [];
44
+
45
+ while (queue.length > 0) {
46
+ const facet = queue.shift()!;
47
+ result.push(facet);
48
+ for (const depId of dependents.get(facet.id) ?? []) {
49
+ const newDegree = (inDegree.get(depId) ?? 0) - 1;
50
+ inDegree.set(depId, newDegree);
51
+ if (newDegree === 0) {
52
+ queue.push(idToFacet.get(depId)!);
53
+ }
54
+ }
55
+ }
56
+
57
+ if (result.length !== facets.length) {
58
+ throw new Error("Cycle detected in facet dependsOn graph");
59
+ }
60
+
61
+ return result;
62
+ }
63
+
64
+ /**
65
+ * Returns only the options of a facet that are visible given the current choices.
66
+ * Options without an `available()` function are always visible.
67
+ */
68
+ export function getVisibleOptions(
69
+ facet: Facet,
70
+ choices: Record<string, string | string[]>,
71
+ catalog: Catalog
72
+ ): Option[] {
73
+ return facet.options.filter((option) => {
74
+ if (!option.available) return true;
75
+ const deps: Record<string, Option | undefined> = {};
76
+ for (const depFacetId of facet.dependsOn ?? []) {
77
+ const depFacet = getFacet(catalog, depFacetId);
78
+ const choiceVal = choices[depFacetId];
79
+ deps[depFacetId] =
80
+ depFacet && typeof choiceVal === "string"
81
+ ? getOption(depFacet, choiceVal)
82
+ : undefined;
83
+ }
84
+ return option.available(deps);
85
+ });
86
+ }
@@ -0,0 +1,165 @@
1
+ import { describe, it, expect, 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
+ import { parse as parseYaml } from "yaml";
6
+
7
+ import {
8
+ readUserConfig,
9
+ writeUserConfig,
10
+ readLockFile,
11
+ writeLockFile
12
+ } from "./config.js";
13
+
14
+ import type { UserConfig, LockFile } from "./types.js";
15
+
16
+ describe("config", () => {
17
+ let tempDir: string;
18
+
19
+ beforeEach(async () => {
20
+ tempDir = await mkdtemp(join(tmpdir(), "ade-config-test-"));
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await rm(tempDir, { recursive: true, force: true });
25
+ });
26
+
27
+ describe("UserConfig roundtrip", () => {
28
+ it("write then read produces identical data", async () => {
29
+ const config: UserConfig = {
30
+ choices: {
31
+ language: "typescript",
32
+ framework: "react"
33
+ }
34
+ };
35
+
36
+ await writeUserConfig(tempDir, config);
37
+ const result = await readUserConfig(tempDir);
38
+
39
+ expect(result).toEqual(config);
40
+ });
41
+
42
+ it("returns null when config.yaml does not exist", async () => {
43
+ const result = await readUserConfig(tempDir);
44
+ expect(result).toBeNull();
45
+ });
46
+
47
+ it("multi-select choices (string[]) survive roundtrip", async () => {
48
+ const config: UserConfig = {
49
+ choices: {
50
+ language: "typescript",
51
+ plugins: ["eslint", "prettier", "vitest"]
52
+ }
53
+ };
54
+
55
+ await writeUserConfig(tempDir, config);
56
+ const result = await readUserConfig(tempDir);
57
+
58
+ expect(result).toEqual(config);
59
+ expect(Array.isArray(result!.choices.plugins)).toBe(true);
60
+ expect(result!.choices.plugins).toEqual(["eslint", "prettier", "vitest"]);
61
+ });
62
+
63
+ it("custom section with mcp_servers and instructions survives roundtrip", async () => {
64
+ const config: UserConfig = {
65
+ choices: {
66
+ language: "python"
67
+ },
68
+ custom: {
69
+ mcp_servers: [
70
+ {
71
+ ref: "my-server",
72
+ command: "npx",
73
+ args: ["-y", "my-mcp-server"],
74
+ env: { API_KEY: "test-key" }
75
+ }
76
+ ],
77
+ instructions: ["Always use type hints", "Follow PEP 8 style guide"]
78
+ }
79
+ };
80
+
81
+ await writeUserConfig(tempDir, config);
82
+ const result = await readUserConfig(tempDir);
83
+
84
+ expect(result).toEqual(config);
85
+ expect(result!.custom!.mcp_servers).toHaveLength(1);
86
+ expect(result!.custom!.mcp_servers![0].ref).toBe("my-server");
87
+ expect(result!.custom!.instructions).toEqual([
88
+ "Always use type hints",
89
+ "Follow PEP 8 style guide"
90
+ ]);
91
+ });
92
+ });
93
+
94
+ describe("LockFile roundtrip", () => {
95
+ it("write then read produces identical data", async () => {
96
+ const lock: LockFile = {
97
+ version: 1,
98
+ generated_at: "2026-03-14T00:00:00.000Z",
99
+ choices: {
100
+ language: "typescript",
101
+ framework: "react"
102
+ },
103
+ logical_config: {
104
+ mcp_servers: [
105
+ {
106
+ ref: "typescript-server",
107
+ command: "npx",
108
+ args: ["-y", "ts-server"],
109
+ env: {}
110
+ }
111
+ ],
112
+ instructions: ["Use strict TypeScript"],
113
+ cli_actions: [
114
+ {
115
+ command: "npm",
116
+ args: ["install"],
117
+ phase: "install"
118
+ }
119
+ ],
120
+ knowledge_sources: [
121
+ {
122
+ name: "ts-docs",
123
+ origin: "https://typescriptlang.org",
124
+ description: "TypeScript documentation"
125
+ }
126
+ ],
127
+ skills: [],
128
+ git_hooks: [],
129
+ setup_notes: []
130
+ }
131
+ };
132
+
133
+ await writeLockFile(tempDir, lock);
134
+ const result = await readLockFile(tempDir);
135
+
136
+ expect(result).toEqual(lock);
137
+ });
138
+
139
+ it("returns null when config.lock.yaml does not exist", async () => {
140
+ const result = await readLockFile(tempDir);
141
+ expect(result).toBeNull();
142
+ });
143
+ });
144
+
145
+ describe("YAML validity", () => {
146
+ it("config file written is valid YAML", async () => {
147
+ const config: UserConfig = {
148
+ choices: {
149
+ language: "typescript",
150
+ tools: ["eslint", "prettier"]
151
+ },
152
+ custom: {
153
+ instructions: ["Be concise"]
154
+ }
155
+ };
156
+
157
+ await writeUserConfig(tempDir, config);
158
+
159
+ const raw = await readFile(join(tempDir, "config.yaml"), "utf-8");
160
+ const parsed = parseYaml(raw);
161
+
162
+ expect(parsed).toEqual(config);
163
+ });
164
+ });
165
+ });
package/src/config.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { parse, stringify } from "yaml";
4
+ import type { UserConfig, LockFile } from "./types.js";
5
+
6
+ const CONFIG_FILE = "config.yaml";
7
+ const LOCK_FILE = "config.lock.yaml";
8
+
9
+ export async function readUserConfig(dir: string): Promise<UserConfig | null> {
10
+ try {
11
+ const raw = await readFile(join(dir, CONFIG_FILE), "utf-8");
12
+ return parse(raw) as UserConfig;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ export async function writeUserConfig(
19
+ dir: string,
20
+ config: UserConfig
21
+ ): Promise<void> {
22
+ await writeFile(join(dir, CONFIG_FILE), stringify(config), "utf-8");
23
+ }
24
+
25
+ export async function readLockFile(dir: string): Promise<LockFile | null> {
26
+ try {
27
+ const raw = await readFile(join(dir, LOCK_FILE), "utf-8");
28
+ return parse(raw) as LockFile;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export async function writeLockFile(
35
+ dir: string,
36
+ lock: LockFile
37
+ ): Promise<void> {
38
+ await writeFile(join(dir, LOCK_FILE), stringify(lock), "utf-8");
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ export {
2
+ type Catalog,
3
+ type Facet,
4
+ type Option,
5
+ type Provision,
6
+ type DocsetDef
7
+ } from "./types.js";
8
+ export {
9
+ type LogicalConfig,
10
+ type McpServerEntry,
11
+ type CliAction,
12
+ type KnowledgeSource,
13
+ type SkillDefinition,
14
+ type InlineSkill,
15
+ type ExternalSkill,
16
+ type GitHook
17
+ } from "./types.js";
18
+ export { type ResolutionContext, type ResolvedFacet } from "./types.js";
19
+ export { type UserConfig, type LockFile } from "./types.js";
20
+ export { type ProvisionWriter } from "./types.js";
21
+ export {
22
+ readUserConfig,
23
+ writeUserConfig,
24
+ readLockFile,
25
+ writeLockFile
26
+ } from "./config.js";
27
+ export {
28
+ type ProvisionWriterDef,
29
+ type AgentWriterDef,
30
+ type WriterRegistry
31
+ } from "./types.js";
32
+ export {
33
+ createRegistry,
34
+ registerProvisionWriter,
35
+ getProvisionWriter,
36
+ registerAgentWriter,
37
+ getAgentWriter,
38
+ createDefaultRegistry
39
+ } from "./registry.js";
40
+ export { resolve, collectDocsets } from "./resolver.js";
41
+ export {
42
+ getDefaultCatalog,
43
+ getFacet,
44
+ getOption,
45
+ sortFacets,
46
+ getVisibleOptions
47
+ } from "./catalog/index.js";
48
+ export { skillsWriter } from "./writers/skills.js";
49
+ export { knowledgeWriter } from "./writers/knowledge.js";