@codemcp/ade 0.5.0 → 0.6.1
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.
- package/.beads/issues.jsonl +24 -0
- package/.beads/last-touched +1 -1
- package/.vibe/beads-state-ade-fix-zod-7eypxn.json +34 -0
- package/.vibe/beads-state-ade-main-iazal7.json +29 -0
- package/.vibe/beads-state-ade-partially-skilled-ywlqhb.json +24 -0
- package/.vibe/development-plan-extensibility.md +169 -0
- package/.vibe/development-plan-fix-zod.md +72 -0
- package/.vibe/development-plan-partially-skilled.md +44 -0
- package/ade.extensions.mjs +66 -0
- package/docs/adr/0002-extension-file-type-safety.md +97 -0
- package/docs/guide/extensions.md +187 -0
- package/package.json +3 -2
- package/packages/cli/dist/index.js +33333 -12021
- package/packages/cli/package.json +4 -2
- package/packages/cli/src/commands/extensions.integration.spec.ts +122 -0
- package/packages/cli/src/commands/install.spec.ts +21 -1
- package/packages/cli/src/commands/install.ts +10 -5
- package/packages/cli/src/commands/setup.ts +8 -4
- package/packages/cli/src/extensions.spec.ts +128 -0
- package/packages/cli/src/extensions.ts +71 -0
- package/packages/cli/src/index.ts +10 -5
- package/packages/cli/tsup.config.ts +7 -1
- package/packages/core/package.json +3 -2
- package/packages/core/src/catalog/facets/process.ts +174 -0
- package/packages/core/src/catalog/index.ts +38 -1
- package/packages/core/src/extensions.spec.ts +169 -0
- package/packages/core/src/index.ts +3 -1
- package/packages/core/src/types.ts +71 -0
- package/packages/harnesses/package.json +1 -1
- package/packages/harnesses/src/index.spec.ts +48 -1
- package/packages/harnesses/src/index.ts +10 -0
- package/pnpm-workspace.yaml +2 -0
- /package/docs/{adrs → adr}/0001-tui-framework-selection.md +0 -0
|
@@ -11,6 +11,42 @@ export const processFacet: Facet = {
|
|
|
11
11
|
label: "CodeMCP Workflows",
|
|
12
12
|
description:
|
|
13
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
|
+
// env: {
|
|
21
|
+
// VIBE_WORKFLOW_DOMAINS: "skilled"
|
|
22
|
+
// },
|
|
23
|
+
allowedTools: [
|
|
24
|
+
"whats_next",
|
|
25
|
+
"conduct_review",
|
|
26
|
+
"list_workflows",
|
|
27
|
+
"get_tool_info"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
writer: "instruction",
|
|
33
|
+
config: {
|
|
34
|
+
text: [
|
|
35
|
+
"You are an AI assistant that helps users develop software features using the workflows server.",
|
|
36
|
+
"IMPORTANT: Call whats_next() after each user message to get phase-specific instructions and maintain the development workflow.",
|
|
37
|
+
'Each tool call returns a JSON response with an "instructions" field. Follow these instructions immediately after you receive them.',
|
|
38
|
+
"Use the development plan which you will retrieve via whats_next() to record important insights and decisions as per the structure of the plan.",
|
|
39
|
+
"Do not use your own task management tools."
|
|
40
|
+
].join("\n")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "codemcp-workflows-skilled",
|
|
47
|
+
label: "CodeMCP Workflows (Skilled)",
|
|
48
|
+
description:
|
|
49
|
+
"Use @codemcp/workflows with domain-specific skills for starting projects, architecture, design, coding and testing",
|
|
14
50
|
recipe: [
|
|
15
51
|
{
|
|
16
52
|
writer: "workflows",
|
|
@@ -28,6 +64,144 @@ export const processFacet: Facet = {
|
|
|
28
64
|
]
|
|
29
65
|
}
|
|
30
66
|
},
|
|
67
|
+
{
|
|
68
|
+
writer: "skills",
|
|
69
|
+
config: {
|
|
70
|
+
skills: [
|
|
71
|
+
{
|
|
72
|
+
name: "starting-project",
|
|
73
|
+
description:
|
|
74
|
+
"Conventions and tooling to expect when starting a new project",
|
|
75
|
+
body: [
|
|
76
|
+
"# Starting a New Project",
|
|
77
|
+
"",
|
|
78
|
+
"## Project Setup",
|
|
79
|
+
"- Check for an existing README, architecture doc, or requirements doc before doing anything else",
|
|
80
|
+
"- Prefer monorepo tooling (pnpm workspaces, nx, turborepo) for multi-package projects",
|
|
81
|
+
"- Use a `.editorconfig` and a linter/formatter config (ESLint + Prettier, Biome, etc.) from day one",
|
|
82
|
+
"- Store secrets in environment variables — never commit them; provide a `.env.example`",
|
|
83
|
+
"",
|
|
84
|
+
"## Conventions",
|
|
85
|
+
"- Follow the language/framework conventions already present in the project",
|
|
86
|
+
"- If no conventions exist yet, propose them and document them before writing code",
|
|
87
|
+
"- Prefer explicit over implicit: clear naming, documented interfaces, typed APIs",
|
|
88
|
+
"",
|
|
89
|
+
"## First Steps Checklist",
|
|
90
|
+
"1. Read all existing documentation",
|
|
91
|
+
"2. Understand the intended architecture (ask if unclear)",
|
|
92
|
+
"3. Confirm the tech stack and tooling",
|
|
93
|
+
"4. Set up the development environment and verify it works",
|
|
94
|
+
"5. Identify and create the initial project skeleton if needed"
|
|
95
|
+
].join("\n")
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "architecture",
|
|
99
|
+
description:
|
|
100
|
+
"Architectural conventions and decision-making guidelines",
|
|
101
|
+
body: [
|
|
102
|
+
"# Architecture",
|
|
103
|
+
"",
|
|
104
|
+
"## Principles",
|
|
105
|
+
"- Prefer simple, proven architectural patterns over novel ones",
|
|
106
|
+
"- Separate concerns: domain logic, infrastructure, and presentation must not be mixed",
|
|
107
|
+
"- Design for testability: business logic must be testable without I/O",
|
|
108
|
+
"- Apply the dependency rule: inner layers must not depend on outer layers",
|
|
109
|
+
"",
|
|
110
|
+
"## Decision Making",
|
|
111
|
+
"- Document significant architecture decisions as ADRs (Architecture Decision Records)",
|
|
112
|
+
"- Evaluate alternatives before committing to an approach",
|
|
113
|
+
"- Consider non-functional requirements: scalability, maintainability, operability",
|
|
114
|
+
"",
|
|
115
|
+
"## Boundaries",
|
|
116
|
+
"- Define clear module/package boundaries with explicit public APIs",
|
|
117
|
+
"- Avoid circular dependencies between modules",
|
|
118
|
+
"- Keep infrastructure concerns (DB, HTTP, queues) behind interfaces"
|
|
119
|
+
].join("\n")
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "application-design",
|
|
123
|
+
description:
|
|
124
|
+
"Design patterns for authentication, routing, error handling, and forms",
|
|
125
|
+
body: [
|
|
126
|
+
"# Application Design",
|
|
127
|
+
"",
|
|
128
|
+
"## Authentication & Authorization",
|
|
129
|
+
"- Authenticate at the edge (middleware/guard) — never inside business logic",
|
|
130
|
+
"- Use short-lived tokens (JWT or session) with refresh strategies",
|
|
131
|
+
"- Apply the principle of least privilege for all role/permission checks",
|
|
132
|
+
"",
|
|
133
|
+
"## Routing",
|
|
134
|
+
"- Use declarative, file-based or configuration-driven routing where available",
|
|
135
|
+
"- Protect routes with auth guards rather than ad-hoc checks",
|
|
136
|
+
"- Keep route handlers thin — delegate to services immediately",
|
|
137
|
+
"",
|
|
138
|
+
"## Error Handling",
|
|
139
|
+
"- Distinguish between operational errors (expected) and programmer errors (bugs)",
|
|
140
|
+
"- Return structured error responses with consistent shape (code, message, details)",
|
|
141
|
+
"- Log errors with context (request id, user id, stack trace) for observability",
|
|
142
|
+
"- Never expose internal stack traces or sensitive data to clients",
|
|
143
|
+
"",
|
|
144
|
+
"## Forms & Validation",
|
|
145
|
+
"- Validate input at the boundary (schema-first with Zod, Yup, Joi, etc.)",
|
|
146
|
+
"- Show inline, field-level validation errors in the UI",
|
|
147
|
+
"- Prevent double-submission by disabling submit controls during in-flight requests"
|
|
148
|
+
].join("\n")
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "coding",
|
|
152
|
+
description:
|
|
153
|
+
"Code style, patterns, and implementation conventions",
|
|
154
|
+
body: [
|
|
155
|
+
"# Coding",
|
|
156
|
+
"",
|
|
157
|
+
"## Style",
|
|
158
|
+
"- Follow the project's existing code style and linter rules unconditionally",
|
|
159
|
+
"- Write self-documenting code: prefer expressive names over comments that explain *what*",
|
|
160
|
+
"- Use comments only to explain *why* when the reason is non-obvious",
|
|
161
|
+
"",
|
|
162
|
+
"## Patterns",
|
|
163
|
+
"- Prefer pure functions and immutable data structures",
|
|
164
|
+
"- Keep functions small and focused on a single responsibility",
|
|
165
|
+
"- Avoid deep nesting — use early returns, guard clauses, and extraction",
|
|
166
|
+
"- Prefer composition over inheritance",
|
|
167
|
+
"",
|
|
168
|
+
"## Quality Gates",
|
|
169
|
+
"- Run the linter and type-checker before declaring a task done",
|
|
170
|
+
"- Fix all warnings, not just errors",
|
|
171
|
+
"- Ensure the build passes end-to-end before moving on"
|
|
172
|
+
].join("\n")
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "testing",
|
|
176
|
+
description:
|
|
177
|
+
"Testing strategy, patterns, and execution conventions",
|
|
178
|
+
body: [
|
|
179
|
+
"# Testing",
|
|
180
|
+
"",
|
|
181
|
+
"## Strategy",
|
|
182
|
+
"- Follow the test pyramid: many unit tests, fewer integration tests, fewest E2E tests",
|
|
183
|
+
"- Write tests alongside the code — not as an afterthought",
|
|
184
|
+
"- Each test must be independent: no shared mutable state between tests",
|
|
185
|
+
"",
|
|
186
|
+
"## Unit Tests",
|
|
187
|
+
"- Test one unit of behavior per test case",
|
|
188
|
+
"- Use descriptive test names that read as specifications",
|
|
189
|
+
"- Mock only direct dependencies, not transitive ones",
|
|
190
|
+
"",
|
|
191
|
+
"## Integration & E2E Tests",
|
|
192
|
+
"- Test real interactions between components (DB, HTTP, queue) in integration tests",
|
|
193
|
+
"- Use realistic data and environments — avoid fake setups that hide real issues",
|
|
194
|
+
"- Clean up test data after each test to keep tests isolated",
|
|
195
|
+
"",
|
|
196
|
+
"## Execution",
|
|
197
|
+
"- All tests must pass before committing",
|
|
198
|
+
"- Run the full test suite after refactoring, even if only small changes were made",
|
|
199
|
+
"- Treat a flaky test as a bug — fix or delete it, never ignore it"
|
|
200
|
+
].join("\n")
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
},
|
|
31
205
|
{
|
|
32
206
|
writer: "instruction",
|
|
33
207
|
config: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Catalog, Facet, Option } from "../types.js";
|
|
1
|
+
import type { Catalog, Facet, Option, AdeExtensions } from "../types.js";
|
|
2
2
|
import { processFacet } from "./facets/process.js";
|
|
3
3
|
import { architectureFacet } from "./facets/architecture.js";
|
|
4
4
|
import { practicesFacet } from "./facets/practices.js";
|
|
@@ -91,3 +91,40 @@ export function getVisibleOptions(
|
|
|
91
91
|
return option.available(deps);
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Merges extension contributions into a catalog, returning a new catalog
|
|
97
|
+
* without mutating the original.
|
|
98
|
+
*
|
|
99
|
+
* - `extensions.facetContributions`: appends new options to existing facets
|
|
100
|
+
* (silently ignores contributions for unknown facet ids)
|
|
101
|
+
* - `extensions.facets`: appends entirely new facets
|
|
102
|
+
*/
|
|
103
|
+
export function mergeExtensions(
|
|
104
|
+
catalog: Catalog,
|
|
105
|
+
extensions: AdeExtensions
|
|
106
|
+
): Catalog {
|
|
107
|
+
// Deep-clone the facets array (shallow-clone each facet with a new options array)
|
|
108
|
+
let facets: Facet[] = catalog.facets.map((f) => ({
|
|
109
|
+
...f,
|
|
110
|
+
options: [...f.options]
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
// Append contributed options to existing facets
|
|
114
|
+
for (const [facetId, newOptions] of Object.entries(
|
|
115
|
+
extensions.facetContributions ?? {}
|
|
116
|
+
)) {
|
|
117
|
+
const facet = facets.find((f) => f.id === facetId);
|
|
118
|
+
if (facet) {
|
|
119
|
+
facet.options = [...facet.options, ...newOptions];
|
|
120
|
+
}
|
|
121
|
+
// Unknown facet ids are silently ignored
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Append entirely new facets
|
|
125
|
+
if (extensions.facets && extensions.facets.length > 0) {
|
|
126
|
+
facets = [...facets, ...extensions.facets];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { facets };
|
|
130
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import type { AdeExtensions } from "./types.js";
|
|
3
|
+
import { AdeExtensionsSchema } from "./types.js";
|
|
4
|
+
import { mergeExtensions } from "./catalog/index.js";
|
|
5
|
+
import { getDefaultCatalog, getFacet, getOption } from "./catalog/index.js";
|
|
6
|
+
|
|
7
|
+
// ─── AdeExtensionsSchema (Zod validation) ──────────────────────────────────
|
|
8
|
+
|
|
9
|
+
describe("AdeExtensionsSchema", () => {
|
|
10
|
+
it("accepts an empty object (all fields optional)", () => {
|
|
11
|
+
const result = AdeExtensionsSchema.safeParse({});
|
|
12
|
+
expect(result.success).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("accepts a valid facetContributions map", () => {
|
|
16
|
+
const ext: AdeExtensions = {
|
|
17
|
+
facetContributions: {
|
|
18
|
+
architecture: [
|
|
19
|
+
{
|
|
20
|
+
id: "sap",
|
|
21
|
+
label: "SAP",
|
|
22
|
+
description: "SAP BTP ABAP development",
|
|
23
|
+
recipe: [{ writer: "skills", config: { skills: [] } }]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const result = AdeExtensionsSchema.safeParse(ext);
|
|
29
|
+
expect(result.success).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("accepts a valid facets array (new facets)", () => {
|
|
33
|
+
const ext: AdeExtensions = {
|
|
34
|
+
facets: [
|
|
35
|
+
{
|
|
36
|
+
id: "custom-facet",
|
|
37
|
+
label: "Custom",
|
|
38
|
+
description: "A custom facet",
|
|
39
|
+
required: false,
|
|
40
|
+
options: []
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
const result = AdeExtensionsSchema.safeParse(ext);
|
|
45
|
+
expect(result.success).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("accepts harnessWriters", () => {
|
|
49
|
+
const ext: AdeExtensions = {
|
|
50
|
+
harnessWriters: [
|
|
51
|
+
{
|
|
52
|
+
id: "my-harness",
|
|
53
|
+
label: "My Harness",
|
|
54
|
+
description: "Custom harness",
|
|
55
|
+
install: async () => {}
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
const result = AdeExtensionsSchema.safeParse(ext);
|
|
60
|
+
expect(result.success).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rejects an invalid facetContributions value (wrong type)", () => {
|
|
64
|
+
const result = AdeExtensionsSchema.safeParse({
|
|
65
|
+
facetContributions: "not-an-object"
|
|
66
|
+
});
|
|
67
|
+
expect(result.success).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("rejects a facetContributions option missing required fields", () => {
|
|
71
|
+
const result = AdeExtensionsSchema.safeParse({
|
|
72
|
+
facetContributions: {
|
|
73
|
+
architecture: [
|
|
74
|
+
{ id: "sap" } // missing label, description, recipe
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
expect(result.success).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─── mergeExtensions ────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
describe("mergeExtensions", () => {
|
|
85
|
+
it("returns the original catalog unchanged when extensions is empty", () => {
|
|
86
|
+
const original = getDefaultCatalog();
|
|
87
|
+
const merged = mergeExtensions(original, {});
|
|
88
|
+
expect(merged.facets).toHaveLength(original.facets.length);
|
|
89
|
+
expect(merged.facets.map((f) => f.id)).toEqual(
|
|
90
|
+
original.facets.map((f) => f.id)
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("adds new options to an existing facet via facetContributions", () => {
|
|
95
|
+
const catalog = getDefaultCatalog();
|
|
96
|
+
const sapOption = {
|
|
97
|
+
id: "sap",
|
|
98
|
+
label: "SAP BTP / ABAP",
|
|
99
|
+
description: "SAP BTP ABAP development",
|
|
100
|
+
recipe: [{ writer: "skills" as const, config: { skills: [] } }]
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const merged = mergeExtensions(catalog, {
|
|
104
|
+
facetContributions: { architecture: [sapOption] }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const arch = getFacet(merged, "architecture")!;
|
|
108
|
+
expect(arch).toBeDefined();
|
|
109
|
+
const sap = getOption(arch, "sap");
|
|
110
|
+
expect(sap).toBeDefined();
|
|
111
|
+
expect(sap!.label).toBe("SAP BTP / ABAP");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("does not mutate the original catalog", () => {
|
|
115
|
+
const catalog = getDefaultCatalog();
|
|
116
|
+
const originalArchOptionCount = getFacet(catalog, "architecture")!.options
|
|
117
|
+
.length;
|
|
118
|
+
|
|
119
|
+
mergeExtensions(catalog, {
|
|
120
|
+
facetContributions: {
|
|
121
|
+
architecture: [
|
|
122
|
+
{
|
|
123
|
+
id: "sap",
|
|
124
|
+
label: "SAP",
|
|
125
|
+
description: "SAP",
|
|
126
|
+
recipe: [{ writer: "skills" as const, config: { skills: [] } }]
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(getFacet(catalog, "architecture")!.options).toHaveLength(
|
|
133
|
+
originalArchOptionCount
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("appends entirely new facets from extensions.facets", () => {
|
|
138
|
+
const catalog = getDefaultCatalog();
|
|
139
|
+
const newFacet = {
|
|
140
|
+
id: "sap-specific",
|
|
141
|
+
label: "SAP Specific",
|
|
142
|
+
description: "SAP-specific choices",
|
|
143
|
+
required: false,
|
|
144
|
+
options: []
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const merged = mergeExtensions(catalog, { facets: [newFacet] });
|
|
148
|
+
expect(merged.facets.map((f) => f.id)).toContain("sap-specific");
|
|
149
|
+
expect(merged.facets).toHaveLength(catalog.facets.length + 1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("ignores facetContributions for unknown facet ids (no crash)", () => {
|
|
153
|
+
const catalog = getDefaultCatalog();
|
|
154
|
+
const merged = mergeExtensions(catalog, {
|
|
155
|
+
facetContributions: {
|
|
156
|
+
"totally-unknown-facet": [
|
|
157
|
+
{
|
|
158
|
+
id: "x",
|
|
159
|
+
label: "X",
|
|
160
|
+
description: "X",
|
|
161
|
+
recipe: [{ writer: "skills" as const, config: { skills: [] } }]
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// Should not throw; catalog unchanged
|
|
167
|
+
expect(merged.facets).toHaveLength(catalog.facets.length);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -45,8 +45,10 @@ export {
|
|
|
45
45
|
getFacet,
|
|
46
46
|
getOption,
|
|
47
47
|
sortFacets,
|
|
48
|
-
getVisibleOptions
|
|
48
|
+
getVisibleOptions,
|
|
49
|
+
mergeExtensions
|
|
49
50
|
} from "./catalog/index.js";
|
|
51
|
+
export { type AdeExtensions, AdeExtensionsSchema } from "./types.js";
|
|
50
52
|
export { skillsWriter } from "./writers/skills.js";
|
|
51
53
|
export { knowledgeWriter } from "./writers/knowledge.js";
|
|
52
54
|
export { permissionPolicyWriter } from "./writers/permission-policy.js";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
1
3
|
// --- Catalog types ---
|
|
2
4
|
|
|
3
5
|
export interface Catalog {
|
|
@@ -157,3 +159,72 @@ export interface WriterRegistry {
|
|
|
157
159
|
provisions: Map<string, ProvisionWriterDef>;
|
|
158
160
|
agents: Map<string, AgentWriterDef>;
|
|
159
161
|
}
|
|
162
|
+
|
|
163
|
+
// --- Extension types ---
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Runtime validation helpers for extension file loading.
|
|
167
|
+
*
|
|
168
|
+
* We use z.custom<T>() for Option, Facet, HarnessWriter and ProvisionWriterDef
|
|
169
|
+
* because their TypeScript interfaces contain function types that Zod cannot
|
|
170
|
+
* faithfully represent without losing the concrete signature. z.custom<T>
|
|
171
|
+
* gives us the correct TS type while still letting us write a runtime check.
|
|
172
|
+
*/
|
|
173
|
+
const OptionSchema = z.custom<Option>(
|
|
174
|
+
(val) =>
|
|
175
|
+
typeof val === "object" &&
|
|
176
|
+
val !== null &&
|
|
177
|
+
typeof (val as Record<string, unknown>).id === "string" &&
|
|
178
|
+
typeof (val as Record<string, unknown>).label === "string" &&
|
|
179
|
+
typeof (val as Record<string, unknown>).description === "string" &&
|
|
180
|
+
Array.isArray((val as Record<string, unknown>).recipe),
|
|
181
|
+
{ message: "Option must have id, label, description and recipe fields" }
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const FacetSchema = z.custom<Facet>(
|
|
185
|
+
(val) =>
|
|
186
|
+
typeof val === "object" &&
|
|
187
|
+
val !== null &&
|
|
188
|
+
typeof (val as Record<string, unknown>).id === "string" &&
|
|
189
|
+
typeof (val as Record<string, unknown>).label === "string" &&
|
|
190
|
+
typeof (val as Record<string, unknown>).description === "string" &&
|
|
191
|
+
typeof (val as Record<string, unknown>).required === "boolean" &&
|
|
192
|
+
Array.isArray((val as Record<string, unknown>).options),
|
|
193
|
+
{ message: "Facet must have id, label, description, required and options" }
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const HarnessWriterSchema = z.custom<
|
|
197
|
+
AgentWriterDef & { label: string; description: string }
|
|
198
|
+
>(
|
|
199
|
+
(val) =>
|
|
200
|
+
typeof val === "object" &&
|
|
201
|
+
val !== null &&
|
|
202
|
+
typeof (val as Record<string, unknown>).id === "string" &&
|
|
203
|
+
typeof (val as Record<string, unknown>).label === "string" &&
|
|
204
|
+
typeof (val as Record<string, unknown>).description === "string" &&
|
|
205
|
+
typeof (val as Record<string, unknown>).install === "function",
|
|
206
|
+
{ message: "HarnessWriter must have id, label, description and install()" }
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const ProvisionWriterDefSchema = z.custom<ProvisionWriterDef>(
|
|
210
|
+
(val) =>
|
|
211
|
+
typeof val === "object" &&
|
|
212
|
+
val !== null &&
|
|
213
|
+
typeof (val as Record<string, unknown>).id === "string" &&
|
|
214
|
+
typeof (val as Record<string, unknown>).write === "function",
|
|
215
|
+
{ message: "ProvisionWriterDef must have id and write()" }
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
export const AdeExtensionsSchema = z.object({
|
|
219
|
+
/** Add new options to existing facets, keyed by facet id */
|
|
220
|
+
facetContributions: z.record(z.string(), z.array(OptionSchema)).optional(),
|
|
221
|
+
/** Add entirely new facets */
|
|
222
|
+
facets: z.array(FacetSchema).optional(),
|
|
223
|
+
/** Add new provision writers */
|
|
224
|
+
provisionWriters: z.array(ProvisionWriterDefSchema).optional(),
|
|
225
|
+
/** Add new harness writers */
|
|
226
|
+
harnessWriters: z.array(HarnessWriterSchema).optional()
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/** The shape of a consumer's `ade.extensions.mjs` default export. */
|
|
230
|
+
export type AdeExtensions = z.infer<typeof AdeExtensionsSchema>;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
allHarnessWriters,
|
|
4
|
+
getHarnessWriter,
|
|
5
|
+
getHarnessIds,
|
|
6
|
+
buildHarnessWriters
|
|
7
|
+
} from "./index.js";
|
|
8
|
+
import type { HarnessWriter } from "./types.js";
|
|
3
9
|
|
|
4
10
|
describe("harness registry", () => {
|
|
5
11
|
it("exports all harness writers", () => {
|
|
@@ -43,3 +49,44 @@ describe("harness registry", () => {
|
|
|
43
49
|
}
|
|
44
50
|
});
|
|
45
51
|
});
|
|
52
|
+
|
|
53
|
+
describe("buildHarnessWriters", () => {
|
|
54
|
+
it("returns all built-in writers when no extensions provided", () => {
|
|
55
|
+
const writers = buildHarnessWriters({});
|
|
56
|
+
expect(writers).toHaveLength(allHarnessWriters.length);
|
|
57
|
+
expect(writers.map((w) => w.id)).toEqual(
|
|
58
|
+
allHarnessWriters.map((w) => w.id)
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("appends extension harness writers after built-ins", () => {
|
|
63
|
+
const customWriter: HarnessWriter = {
|
|
64
|
+
id: "sap-copilot",
|
|
65
|
+
label: "SAP Copilot",
|
|
66
|
+
description: "SAP internal Copilot harness",
|
|
67
|
+
install: async () => {}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const writers = buildHarnessWriters({ harnessWriters: [customWriter] });
|
|
71
|
+
expect(writers).toHaveLength(allHarnessWriters.length + 1);
|
|
72
|
+
expect(writers.map((w) => w.id)).toContain("sap-copilot");
|
|
73
|
+
// built-ins come first
|
|
74
|
+
expect(writers[0].id).toBe("universal");
|
|
75
|
+
expect(writers[writers.length - 1].id).toBe("sap-copilot");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("does not mutate allHarnessWriters", () => {
|
|
79
|
+
const originalLength = allHarnessWriters.length;
|
|
80
|
+
buildHarnessWriters({
|
|
81
|
+
harnessWriters: [
|
|
82
|
+
{
|
|
83
|
+
id: "ephemeral",
|
|
84
|
+
label: "Ephemeral",
|
|
85
|
+
description: "Should not persist",
|
|
86
|
+
install: async () => {}
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
expect(allHarnessWriters).toHaveLength(originalLength);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -45,3 +45,13 @@ export function getHarnessWriter(id: string): HarnessWriter | undefined {
|
|
|
45
45
|
export function getHarnessIds(): string[] {
|
|
46
46
|
return allHarnessWriters.map((w) => w.id);
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the full list of harness writers: built-ins first, then any
|
|
51
|
+
* additional writers contributed via extensions. Does not mutate allHarnessWriters.
|
|
52
|
+
*/
|
|
53
|
+
export function buildHarnessWriters(extensions: {
|
|
54
|
+
harnessWriters?: HarnessWriter[];
|
|
55
|
+
}): HarnessWriter[] {
|
|
56
|
+
return [...allHarnessWriters, ...(extensions.harnessWriters ?? [])];
|
|
57
|
+
}
|
package/pnpm-workspace.yaml
CHANGED
|
File without changes
|