@codemcp/ade 0.6.0 → 0.7.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.
- package/.beads/issues.jsonl +21 -0
- package/.beads/last-touched +1 -1
- package/.kiro/agents/ade.json +10 -2
- package/.kiro/settings/mcp.json +6 -1
- package/.opencode/agents/ade.md +7 -2
- package/.vibe/beads-state-ade-extension-override-skills-d44z9p.json +29 -0
- package/.vibe/beads-state-ade-fix-zod-7eypxn.json +34 -0
- package/.vibe/beads-state-ade-partially-skilled-ywlqhb.json +24 -0
- package/.vibe/development-plan-extension-override-skills.md +110 -0
- package/.vibe/development-plan-fix-zod.md +72 -0
- package/.vibe/development-plan-partially-skilled.md +44 -0
- package/config.lock.yaml +6 -1
- package/package.json +1 -1
- package/packages/cli/dist/index.js +33275 -12036
- package/packages/cli/package.json +1 -1
- package/packages/cli/tsup.config.ts +7 -1
- package/packages/core/package.json +1 -1
- package/packages/core/src/catalog/facets/process.ts +174 -0
- package/packages/core/src/resolver.spec.ts +330 -0
- package/packages/core/src/resolver.ts +16 -0
- package/packages/core/src/types.ts +14 -0
- package/packages/core/src/writers/skills.spec.ts +46 -0
- package/packages/harnesses/package.json +1 -1
- package/packages/harnesses/src/writers/opencode.spec.ts +3 -5
- package/packages/harnesses/src/writers/opencode.ts +3 -4
|
@@ -7,7 +7,13 @@ export default defineConfig({
|
|
|
7
7
|
tsconfig: "tsconfig.build.json",
|
|
8
8
|
target: "node22",
|
|
9
9
|
clean: true,
|
|
10
|
-
noExternal: [
|
|
10
|
+
noExternal: [
|
|
11
|
+
"@clack/prompts",
|
|
12
|
+
"@codemcp/ade-core",
|
|
13
|
+
"@codemcp/ade-harnesses",
|
|
14
|
+
"yaml",
|
|
15
|
+
"zod"
|
|
16
|
+
],
|
|
11
17
|
esbuildOptions(options) {
|
|
12
18
|
options.banner = {
|
|
13
19
|
js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`
|
|
@@ -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: {
|
|
@@ -246,6 +246,336 @@ describe("resolve", () => {
|
|
|
246
246
|
);
|
|
247
247
|
expect(agentskills).toBeUndefined();
|
|
248
248
|
});
|
|
249
|
+
|
|
250
|
+
it("removes a skill when another skill declares it in replaces", async () => {
|
|
251
|
+
const skillsCatalog: Catalog = {
|
|
252
|
+
facets: [
|
|
253
|
+
{
|
|
254
|
+
id: "process",
|
|
255
|
+
label: "Process",
|
|
256
|
+
description: "Process",
|
|
257
|
+
required: true,
|
|
258
|
+
options: [
|
|
259
|
+
{
|
|
260
|
+
id: "base",
|
|
261
|
+
label: "Base",
|
|
262
|
+
description: "Base process",
|
|
263
|
+
recipe: [
|
|
264
|
+
{
|
|
265
|
+
writer: "skills",
|
|
266
|
+
config: {
|
|
267
|
+
skills: [
|
|
268
|
+
{
|
|
269
|
+
name: "architecture",
|
|
270
|
+
description: "Generic architecture skill",
|
|
271
|
+
body: "Generic architecture content."
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "architecture",
|
|
282
|
+
label: "Architecture",
|
|
283
|
+
description: "Stack",
|
|
284
|
+
required: false,
|
|
285
|
+
options: [
|
|
286
|
+
{
|
|
287
|
+
id: "sabdx",
|
|
288
|
+
label: "SABDX",
|
|
289
|
+
description: "SABDX frontend",
|
|
290
|
+
recipe: [
|
|
291
|
+
{
|
|
292
|
+
writer: "skills",
|
|
293
|
+
config: {
|
|
294
|
+
skills: [
|
|
295
|
+
{
|
|
296
|
+
name: "sabdx-architecture",
|
|
297
|
+
description: "SABDX architecture skill",
|
|
298
|
+
body: "SABDX architecture content.",
|
|
299
|
+
replaces: ["architecture"]
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const userConfig: UserConfig = {
|
|
312
|
+
choices: { process: "base", architecture: "sabdx" }
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const result = await resolve(userConfig, skillsCatalog, registry);
|
|
316
|
+
|
|
317
|
+
expect(result.skills).toHaveLength(1);
|
|
318
|
+
expect(result.skills[0].name).toBe("sabdx-architecture");
|
|
319
|
+
expect(
|
|
320
|
+
result.skills.find((s) => s.name === "architecture")
|
|
321
|
+
).toBeUndefined();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("removes multiple skills when a single skill declares several replaces entries", async () => {
|
|
325
|
+
const skillsCatalog: Catalog = {
|
|
326
|
+
facets: [
|
|
327
|
+
{
|
|
328
|
+
id: "process",
|
|
329
|
+
label: "Process",
|
|
330
|
+
description: "Process",
|
|
331
|
+
required: true,
|
|
332
|
+
options: [
|
|
333
|
+
{
|
|
334
|
+
id: "base",
|
|
335
|
+
label: "Base",
|
|
336
|
+
description: "Base",
|
|
337
|
+
recipe: [
|
|
338
|
+
{
|
|
339
|
+
writer: "skills",
|
|
340
|
+
config: {
|
|
341
|
+
skills: [
|
|
342
|
+
{
|
|
343
|
+
name: "coding",
|
|
344
|
+
description: "Generic coding",
|
|
345
|
+
body: "Generic coding."
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "testing",
|
|
349
|
+
description: "Generic testing",
|
|
350
|
+
body: "Generic testing."
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: "architecture",
|
|
361
|
+
label: "Architecture",
|
|
362
|
+
description: "Stack",
|
|
363
|
+
required: false,
|
|
364
|
+
options: [
|
|
365
|
+
{
|
|
366
|
+
id: "ext",
|
|
367
|
+
label: "Extension",
|
|
368
|
+
description: "Extension",
|
|
369
|
+
recipe: [
|
|
370
|
+
{
|
|
371
|
+
writer: "skills",
|
|
372
|
+
config: {
|
|
373
|
+
skills: [
|
|
374
|
+
{
|
|
375
|
+
name: "ext-all",
|
|
376
|
+
description: "Replaces both",
|
|
377
|
+
body: "Extension content.",
|
|
378
|
+
replaces: ["coding", "testing"]
|
|
379
|
+
}
|
|
380
|
+
]
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const userConfig: UserConfig = {
|
|
391
|
+
choices: { process: "base", architecture: "ext" }
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const result = await resolve(userConfig, skillsCatalog, registry);
|
|
395
|
+
|
|
396
|
+
expect(result.skills).toHaveLength(1);
|
|
397
|
+
expect(result.skills[0].name).toBe("ext-all");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("keeps all skills when no replaces are declared", async () => {
|
|
401
|
+
const skillsCatalog: Catalog = {
|
|
402
|
+
facets: [
|
|
403
|
+
{
|
|
404
|
+
id: "process",
|
|
405
|
+
label: "Process",
|
|
406
|
+
description: "Process",
|
|
407
|
+
required: true,
|
|
408
|
+
options: [
|
|
409
|
+
{
|
|
410
|
+
id: "base",
|
|
411
|
+
label: "Base",
|
|
412
|
+
description: "Base",
|
|
413
|
+
recipe: [
|
|
414
|
+
{
|
|
415
|
+
writer: "skills",
|
|
416
|
+
config: {
|
|
417
|
+
skills: [
|
|
418
|
+
{ name: "skill-a", description: "A", body: "Body A." },
|
|
419
|
+
{ name: "skill-b", description: "B", body: "Body B." }
|
|
420
|
+
]
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
]
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
}
|
|
427
|
+
]
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const userConfig: UserConfig = { choices: { process: "base" } };
|
|
431
|
+
const result = await resolve(userConfig, skillsCatalog, registry);
|
|
432
|
+
|
|
433
|
+
expect(result.skills).toHaveLength(2);
|
|
434
|
+
expect(result.skills.map((s) => s.name)).toEqual(["skill-a", "skill-b"]);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("deduplicates skills by name — last writer wins", async () => {
|
|
438
|
+
const skillsCatalog: Catalog = {
|
|
439
|
+
facets: [
|
|
440
|
+
{
|
|
441
|
+
id: "process",
|
|
442
|
+
label: "Process",
|
|
443
|
+
description: "Process",
|
|
444
|
+
required: true,
|
|
445
|
+
options: [
|
|
446
|
+
{
|
|
447
|
+
id: "base",
|
|
448
|
+
label: "Base",
|
|
449
|
+
description: "Base",
|
|
450
|
+
recipe: [
|
|
451
|
+
{
|
|
452
|
+
writer: "skills",
|
|
453
|
+
config: {
|
|
454
|
+
skills: [
|
|
455
|
+
{
|
|
456
|
+
name: "shared-skill",
|
|
457
|
+
description: "First",
|
|
458
|
+
body: "First body."
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
]
|
|
464
|
+
}
|
|
465
|
+
]
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: "architecture",
|
|
469
|
+
label: "Architecture",
|
|
470
|
+
description: "Stack",
|
|
471
|
+
required: false,
|
|
472
|
+
options: [
|
|
473
|
+
{
|
|
474
|
+
id: "ext",
|
|
475
|
+
label: "Extension",
|
|
476
|
+
description: "Extension",
|
|
477
|
+
recipe: [
|
|
478
|
+
{
|
|
479
|
+
writer: "skills",
|
|
480
|
+
config: {
|
|
481
|
+
skills: [
|
|
482
|
+
{
|
|
483
|
+
name: "shared-skill",
|
|
484
|
+
description: "Second",
|
|
485
|
+
body: "Second body."
|
|
486
|
+
}
|
|
487
|
+
]
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
]
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const userConfig: UserConfig = {
|
|
498
|
+
choices: { process: "base", architecture: "ext" }
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const result = await resolve(userConfig, skillsCatalog, registry);
|
|
502
|
+
|
|
503
|
+
expect(result.skills).toHaveLength(1);
|
|
504
|
+
expect(result.skills[0]).toMatchObject({
|
|
505
|
+
name: "shared-skill",
|
|
506
|
+
body: "Second body."
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("works with external skills that declare replaces", async () => {
|
|
511
|
+
const skillsCatalog: Catalog = {
|
|
512
|
+
facets: [
|
|
513
|
+
{
|
|
514
|
+
id: "process",
|
|
515
|
+
label: "Process",
|
|
516
|
+
description: "Process",
|
|
517
|
+
required: true,
|
|
518
|
+
options: [
|
|
519
|
+
{
|
|
520
|
+
id: "base",
|
|
521
|
+
label: "Base",
|
|
522
|
+
description: "Base",
|
|
523
|
+
recipe: [
|
|
524
|
+
{
|
|
525
|
+
writer: "skills",
|
|
526
|
+
config: {
|
|
527
|
+
skills: [
|
|
528
|
+
{
|
|
529
|
+
name: "tdd",
|
|
530
|
+
description: "Generic TDD",
|
|
531
|
+
body: "Generic TDD."
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
]
|
|
537
|
+
}
|
|
538
|
+
]
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
id: "architecture",
|
|
542
|
+
label: "Architecture",
|
|
543
|
+
description: "Stack",
|
|
544
|
+
required: false,
|
|
545
|
+
options: [
|
|
546
|
+
{
|
|
547
|
+
id: "ext",
|
|
548
|
+
label: "Extension",
|
|
549
|
+
description: "Extension",
|
|
550
|
+
recipe: [
|
|
551
|
+
{
|
|
552
|
+
writer: "skills",
|
|
553
|
+
config: {
|
|
554
|
+
skills: [
|
|
555
|
+
{
|
|
556
|
+
name: "ext-tdd",
|
|
557
|
+
source: "org/repo/skills/ext-tdd",
|
|
558
|
+
replaces: ["tdd"]
|
|
559
|
+
}
|
|
560
|
+
]
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
]
|
|
564
|
+
}
|
|
565
|
+
]
|
|
566
|
+
}
|
|
567
|
+
]
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const userConfig: UserConfig = {
|
|
571
|
+
choices: { process: "base", architecture: "ext" }
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const result = await resolve(userConfig, skillsCatalog, registry);
|
|
575
|
+
|
|
576
|
+
expect(result.skills).toHaveLength(1);
|
|
577
|
+
expect(result.skills[0].name).toBe("ext-tdd");
|
|
578
|
+
});
|
|
249
579
|
});
|
|
250
580
|
|
|
251
581
|
describe("setup_notes merging", () => {
|
|
@@ -125,6 +125,22 @@ export async function resolve(
|
|
|
125
125
|
}
|
|
126
126
|
result.mcp_servers = Array.from(serversByRef.values());
|
|
127
127
|
|
|
128
|
+
// Dedup skills: collect all names declared as replaced, dedup by name
|
|
129
|
+
// (last-writer-wins), then filter out replaced ones.
|
|
130
|
+
const replacedNames = new Set<string>();
|
|
131
|
+
for (const skill of result.skills) {
|
|
132
|
+
for (const r of skill.replaces ?? []) {
|
|
133
|
+
replacedNames.add(r);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const skillsByName = new Map<string, (typeof result.skills)[number]>();
|
|
137
|
+
for (const skill of result.skills) {
|
|
138
|
+
skillsByName.set(skill.name, skill);
|
|
139
|
+
}
|
|
140
|
+
result.skills = Array.from(skillsByName.values()).filter(
|
|
141
|
+
(skill) => !replacedNames.has(skill.name)
|
|
142
|
+
);
|
|
143
|
+
|
|
128
144
|
return result;
|
|
129
145
|
}
|
|
130
146
|
|
|
@@ -54,11 +54,25 @@ export interface InlineSkill {
|
|
|
54
54
|
name: string;
|
|
55
55
|
description: string;
|
|
56
56
|
body: string;
|
|
57
|
+
/**
|
|
58
|
+
* Names of other skills that this skill supersedes.
|
|
59
|
+
* Any skill whose `name` appears here will be removed from the final
|
|
60
|
+
* resolved skills list. Used by extension-contributed skills to suppress
|
|
61
|
+
* the generic baseline skills registered by the process option.
|
|
62
|
+
*/
|
|
63
|
+
replaces?: string[];
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
export interface ExternalSkill {
|
|
60
67
|
name: string;
|
|
61
68
|
source: string;
|
|
69
|
+
/**
|
|
70
|
+
* Names of other skills that this skill supersedes.
|
|
71
|
+
* Any skill whose `name` appears here will be removed from the final
|
|
72
|
+
* resolved skills list. Used by extension-contributed skills to suppress
|
|
73
|
+
* the generic baseline skills registered by the process option.
|
|
74
|
+
*/
|
|
75
|
+
replaces?: string[];
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
export type SkillDefinition = InlineSkill | ExternalSkill;
|
|
@@ -85,6 +85,52 @@ describe("skillsWriter", () => {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it("passes through replaces field on inline skills", async () => {
|
|
89
|
+
const result = await skillsWriter.write(
|
|
90
|
+
{
|
|
91
|
+
skills: [
|
|
92
|
+
{
|
|
93
|
+
name: "ext-architecture",
|
|
94
|
+
description: "Extension architecture",
|
|
95
|
+
body: "Extension body.",
|
|
96
|
+
replaces: ["architecture"]
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
emptyContext
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(result.skills).toHaveLength(1);
|
|
104
|
+
expect(result.skills![0]).toEqual({
|
|
105
|
+
name: "ext-architecture",
|
|
106
|
+
description: "Extension architecture",
|
|
107
|
+
body: "Extension body.",
|
|
108
|
+
replaces: ["architecture"]
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("passes through replaces field on external skills", async () => {
|
|
113
|
+
const result = await skillsWriter.write(
|
|
114
|
+
{
|
|
115
|
+
skills: [
|
|
116
|
+
{
|
|
117
|
+
name: "ext-tdd",
|
|
118
|
+
source: "org/repo/skills/ext-tdd",
|
|
119
|
+
replaces: ["tdd"]
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
emptyContext
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(result.skills).toHaveLength(1);
|
|
127
|
+
expect(result.skills![0]).toEqual({
|
|
128
|
+
name: "ext-tdd",
|
|
129
|
+
source: "org/repo/skills/ext-tdd",
|
|
130
|
+
replaces: ["tdd"]
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
88
134
|
it("handles mixed inline and external skills", async () => {
|
|
89
135
|
const result = await skillsWriter.write(
|
|
90
136
|
{
|