diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 111960a..4e1eb4b 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -49,6 +49,17 @@ {"id":"ade-12.3.8","title":"Move skills installation and knowledge source init from setup into configure","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:58:23.014156+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T08:10:35.327031+01:00","closed_at":"2026-03-26T08:10:35.327031+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.8","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.8","depends_on_id":"ade-12.3.7","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"ade-12.3.9","title":"Add optional agent configuration prompt at end of setup (\"configure now?\")","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:58:23.187146+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T08:10:35.44312+01:00","closed_at":"2026-03-26T08:10:35.44312+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.9","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.9","depends_on_id":"ade-12.3.7","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.9","depends_on_id":"ade-12.3.8","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"ade-12.4","title":"Commit","description":"Ensure code quality and documentation accuracy through systematic cleanup and review. **STEP 1: Code Cleanup** Systematically clean up development artifacts: 1. **Remove Debug Output**: Search for and remove all temporary debug output statements used during development. Look for language-specific debug output methods (console logging, print statements, debug output functions). Remove any debugging statements that were added for development purposes. 2. **Review TODO/FIXME Comments**: - Address each TODO/FIXME comment by either implementing the solution or documenting why it's deferred - Remove completed TODOs - Convert remaining TODOs to proper issue tracking if needed 3. **Remove Debugging Code Blocks**: - Remove temporary debugging code, test code blocks, and commented-out code - Clean up any experimental code that's no longer needed - Ensure proper error handling replaces temporary debug logging **STEP 2: Documentation Review** Review and update documentation to reflect final implementation: 1. **Update Long-Term Memory Documents**: Based on what was actually implemented: - If exists: Update it if requirements changed during development - If exists: Update it if architectural impacts were identified - If exists: Update it if design details were refined or changed - Otherwise: Document any changes in the plan file 2. **Compare Against Implementation**: Review documentation against actual implemented functionality 3. **Update Changed Sections**: Only modify documentation sections that have functional changes 4. **Remove Development Progress**: Remove references to development iterations, progress notes, and temporary decisions 5. **Focus on Final State**: Ensure documentation describes the final implemented state, not the development process 6. **Ask User to Review Document Updates** **STEP 3: Final Validation** - Run existing tests to ensure cleanup didn't break functionality - Verify documentation accuracy with a final review - Ensure code is ready for production/delivery Update task progress and mark completed work as you finalize the feature.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:03:17.318705+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:03:17.318705+01:00","dependencies":[{"issue_id":"ade-12.4","depends_on_id":"ade-12","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.4","depends_on_id":"ade-12.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-13","title":"ade: greenfield (development-plan-multi-file-skills.md)","description":"Responsible vibe engineering session using greenfield workflow for ade","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:06:13.383542+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:06:13.383542+02:00"} +{"id":"ade-13.1","title":"Ideation","description":"Understand WHAT the system should do, WHO will use it, WHY it's needed, and WHAT'S in/out of scope by exploring: - Are there existing solutions? What gaps do they have? - What technologies must/cannot be used? Why? - How will you measure product success? - Have you validated this need with potential users? Don't discuss technical implementation yet - focus purely on understanding the problem space and requirements. Document all findings in and create tasks as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:06:13.551206+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:06:13.551206+02:00","dependencies":[{"issue_id":"ade-13.1","depends_on_id":"ade-13","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-13.2","title":"Architecture","description":"Design the technical solution based on requirements from . - Ask about the user's technical preferences and experience - Challenge their choices by presenting alternatives - Evaluate pros and cons of different tech stacks, frameworks, and architectural patterns - Consider non-functional requirements like scalability, performance, maintainability, and deployment Create a comprehensive architecture document in . Don't start coding yet - focus on technical design decisions.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:06:13.740921+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:06:13.740921+02:00","dependencies":[{"issue_id":"ade-13.2","depends_on_id":"ade-13","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-13.2","depends_on_id":"ade-13.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-13.3","title":"Plan","description":"Create a detailed implementation strategy based on your completed architecture in and requirements from . **STEP 1: Break Down Work** - Break down the work into specific, actionable tasks - Consider the chosen tech stack and architectural decisions - Plan the implementation order and identify dependencies **STEP 2: Assess Risks** - Consider potential risks and mitigation strategies - Document the detailed design in **STEP 3: Create Tasks** - Create tasks thoroughly with clear milestones for implementation work - Ensure each task is actionable and has clear acceptance criteria","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:06:13.920777+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:06:13.920777+02:00","dependencies":[{"issue_id":"ade-13.3","depends_on_id":"ade-13","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-13.3","depends_on_id":"ade-13.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-13.4","title":"Code","description":"Build the solution following your plan and detailed design from using the architecture from . - Ensure all requirements from you are currently working on are met - Write clean, well-structured code with proper error handling - Prevent regression by building, linting, and executing existing tests - Stay flexible and adapt the plan as you learn more during implementation, but maintain alignment with your architecture decisions - Update task progress and create new tasks as needed for current phase work","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:06:14.122245+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:06:14.122245+02:00","dependencies":[{"issue_id":"ade-13.4","depends_on_id":"ade-13","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-13.4","depends_on_id":"ade-13.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-13.5","title":"Finalize","description":"This phase ensures code quality and documentation accuracy through systematic cleanup and review. **STEP 1: Code Cleanup** Systematically clean up development artifacts: 1. Remove all temporary debug output statements used during development (console logging, print statements, debug functions) 2. Address each TODO/FIXME comment by either implementing the solution or documenting why it's deferred - Remove completed TODOs - Convert remaining TODOs to proper issue tracking if needed 3. Remove debugging code blocks, temporary test code, and commented-out code - Ensure proper error handling replaces temporary debug logging **STEP 2: Documentation Review** Update documentation to reflect final implementation: 1. Update long-term memory documents based on what was actually implemented: - Update if requirements changed during development - Update if architectural decisions evolved - Update if design details were refined or changed 2. Review documentation against actual implemented functionality 3. Only modify documentation sections that have functional changes 4. Remove references to development iterations, progress notes, and temporary decisions 5. Ensure documentation describes the final implemented state, not the development process 6. Ask user to review document updates **STEP 3: Comprehensive Documentation** Ensure project has complete documentation for newcomers including setup, usage, architecture overview, and contribution guidelines. **STEP 4: Final Validation** - Run existing tests to ensure cleanup didn't break functionality - Verify documentation accuracy with a final review - Ensure project is ready for users and contributors - Update task progress and mark completed work as you finalize the project","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:06:14.337971+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:06:14.337971+02:00","dependencies":[{"issue_id":"ade-13.5","depends_on_id":"ade-13","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-13.5","depends_on_id":"ade-13.4","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-14","title":"ade: epcc (development-plan-multi-file-skills.md)","description":"Responsible vibe engineering session using epcc workflow for ade","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:21:22.997879+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:21:22.997879+02:00"} +{"id":"ade-14.1","title":"Explore","description":"Research the codebase to understand existing patterns and gather context about the problem space. - If uncertain about conventions or rules, ask the user about them - Read relevant files and documentation - If exists: Understand and document requirements there - Otherwise: Document requirements in your task management system Focus on understanding without writing code yet. Document your findings and create tasks as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:21:23.182541+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:21:23.182541+02:00","dependencies":[{"issue_id":"ade-14.1","depends_on_id":"ade-14","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-14.2","title":"Plan","description":"Create a detailed implementation strategy based on your exploration: - If exists: Base your strategy on requirements from it - Otherwise: Use existing task context Break down the work into specific, actionable tasks. Consider edge cases, dependencies, and potential challenges. - If architectural changes needed and exists: Document in - Otherwise: Create tasks to track architectural decisions - If exists: Adhere to the design in it - Otherwise: Elaborate design options and present them to the user Document the planning work thoroughly and create implementation tasks as part of the code phase as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:21:23.353521+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:21:23.353521+02:00","dependencies":[{"issue_id":"ade-14.2","depends_on_id":"ade-14","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-14.2","depends_on_id":"ade-14.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-14.3","title":"Code","description":"Follow your plan to build the solution: - If exists: Follow the design from it - Otherwise: Elaborate design options and present them to the user - If exists: Build according to the architecture from it - Otherwise: Elaborate architectural options and present them to the user - If exists: Ensure requirements from it are met - Otherwise: Ensure existing requirements are met based on your task context Write clean, well-structured code with proper error handling. Prevent regression by building, linting, and executing existing tests. Stay flexible and adapt the plan as you learn more during implementation. Update task progress and create new tasks as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:21:23.540563+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:21:23.540563+02:00","dependencies":[{"issue_id":"ade-14.3","depends_on_id":"ade-14","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-14.3","depends_on_id":"ade-14.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-14.4","title":"Commit","description":"Ensure code quality and documentation accuracy through systematic cleanup and review. **STEP 1: Code Cleanup** Systematically clean up development artifacts: 1. **Remove Debug Output**: Search for and remove all temporary debug output statements used during development. Look for language-specific debug output methods (console logging, print statements, debug output functions). Remove any debugging statements that were added for development purposes. 2. **Review TODO/FIXME Comments**: - Address each TODO/FIXME comment by either implementing the solution or documenting why it's deferred - Remove completed TODOs - Convert remaining TODOs to proper issue tracking if needed 3. **Remove Debugging Code Blocks**: - Remove temporary debugging code, test code blocks, and commented-out code - Clean up any experimental code that's no longer needed - Ensure proper error handling replaces temporary debug logging **STEP 2: Documentation Review** Review and update documentation to reflect final implementation: 1. **Update Long-Term Memory Documents**: Based on what was actually implemented: - If exists: Update it if requirements changed during development - If exists: Update it if architectural impacts were identified - If exists: Update it if design details were refined or changed - Otherwise: Document any changes in the plan file 2. **Compare Against Implementation**: Review documentation against actual implemented functionality 3. **Update Changed Sections**: Only modify documentation sections that have functional changes 4. **Remove Development Progress**: Remove references to development iterations, progress notes, and temporary decisions 5. **Focus on Final State**: Ensure documentation describes the final implemented state, not the development process 6. **Ask User to Review Document Updates** **STEP 3: Final Validation** - Run existing tests to ensure cleanup didn't break functionality - Verify documentation accuracy with a final review - Ensure code is ready for production/delivery Update task progress and mark completed work as you finalize the feature.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-04T12:21:23.729242+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-04T12:21:23.729242+02:00","dependencies":[{"issue_id":"ade-14.4","depends_on_id":"ade-14","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-14.4","depends_on_id":"ade-14.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"ade-2","title":"ade: epcc (development-plan-autonomy-facet.md)","description":"Responsible vibe engineering session using epcc workflow for ade","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:42:24.649306+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:42:24.649306+01:00"} {"id":"ade-2.1","title":"Explore","description":"Research the codebase to understand existing patterns and gather context about the problem space. - If uncertain about conventions or rules, ask the user about them - Read relevant files and documentation - If exists: Understand and document requirements there - Otherwise: Document requirements in your task management system Focus on understanding without writing code yet. Document your findings and create tasks as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:42:24.818903+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:42:24.818903+01:00","dependencies":[{"issue_id":"ade-2.1","depends_on_id":"ade-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"ade-2.1.1","title":"Review existing behavior-oriented facets and catalog tests","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:46:07.923572+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:46:37.327782+01:00","closed_at":"2026-03-18T08:46:37.327782+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.1.1","depends_on_id":"ade-2.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} diff --git a/.beads/last-touched b/.beads/last-touched index c843944..39f733a 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -ade-12.3.12 +ade-14.4 diff --git a/.vibe/beads-state-ade-multi-file-skills-4wcymb.json b/.vibe/beads-state-ade-multi-file-skills-4wcymb.json new file mode 100644 index 0000000..0b67ffd --- /dev/null +++ b/.vibe/beads-state-ade-multi-file-skills-4wcymb.json @@ -0,0 +1,29 @@ +{ + "conversationId": "ade-multi-file-skills-4wcymb", + "projectPath": "/Users/oliverjaegle/projects/privat/codemcp/ade", + "epicId": "ade-14", + "phaseTasks": [ + { + "phaseId": "explore", + "phaseName": "Explore", + "taskId": "ade-14.1" + }, + { + "phaseId": "plan", + "phaseName": "Plan", + "taskId": "ade-14.2" + }, + { + "phaseId": "code", + "phaseName": "Code", + "taskId": "ade-14.3" + }, + { + "phaseId": "commit", + "phaseName": "Commit", + "taskId": "ade-14.4" + } + ], + "createdAt": "2026-04-04T10:21:24.134Z", + "updatedAt": "2026-04-04T10:21:24.134Z" +} \ No newline at end of file diff --git a/.vibe/development-plan-multi-file-skills.md b/.vibe/development-plan-multi-file-skills.md new file mode 100644 index 0000000..2d581eb --- /dev/null +++ b/.vibe/development-plan-multi-file-skills.md @@ -0,0 +1,135 @@ +# Development Plan: ade (multi-file-skills branch) + +*Generated on 2026-04-04 by Vibe Feature MCP* +*Workflow: [epcc](https://mrsimpson.github.io/responsible-vibe-mcp/workflows/epcc)* + +## Goal + +Support **multi-part inline skills** so that skills defined in `ade.extensions.mjs` can supply not just a `body` (SKILL.md content) but also **additional asset files** (references, scripts, etc.) placed in subdirectories of the skill folder, following the agentskills.io "progressive disclosure" pattern. + +## Key Decisions + +### KD-1: Asset format — flat path-keyed map + +Inline skills in `ade.extensions.mjs` will declare additional files via a flat `assets` map where each key is a relative path (the path signals both type and filename): + +```js +{ + name: "sabdx-architecture", + description: "...", + body: "# Architecture\n\nSee [folder structure](references/folder-structure.md)\n...", + assets: { + "references/folder-structure.md": "## Folder Structure\n\nDetailed content...", + "references/file-naming.md": "## File Naming\n\n...", + "scripts/setup.sh": "#!/bin/bash\n..." + } +} +``` + +**Rationale:** The path itself carries the type and filename, maps 1:1 to the output file structure under `.ade/skills//`, requires no comment-marker parsing in the Markdown body, and is trivially extensible to new asset types (just use a new path prefix). + +### KD-2: No inline markers in body + +The `` HTML comment marker approach (previously observed in consumer extension files) is **not adopted** in ADE core. Authors reference asset files naturally in Markdown: `[see details](references/folder-structure.md)`. + +### KD-3: Type extension — `assets` field added to `InlineSkill` + +`InlineSkill` in `packages/core/src/types.ts` gains an optional `assets?: Record` field. Keys are relative paths; values are file contents. + +### KD-4: Bug in existing `writeInlineSkills` (fix as part of this work) + +In `packages/harnesses/src/util.ts:233-244`, there is an inverted logic bug: +- When existing file **matches** expected content, it adds to `modified` and `continue`s (skipping the write) — correct behavior would be to do nothing +- When existing file **differs** (or doesn't exist), it falls through to the write — but `modified` isn't populated in this case + +The correct logic should be: add to `modified` when the file differs (or is new), and skip the write when content is identical. This will be fixed alongside the asset writing work. + +## Notes + +### Agentskills.io progressive disclosure pattern +1. **Metadata** (~100 tokens): `name` + `description` in SKILL.md frontmatter — loaded at startup +2. **Instructions** (<5000 tokens): Full SKILL.md body — loaded when skill is activated +3. **Resources**: Files in `references/`, `scripts/`, `assets/` — loaded only when needed + +### Current relevant files +- `packages/core/src/types.ts` — `InlineSkill` type +- `packages/core/src/writers/skills.ts` — skills provision writer (minimal pass-through) +- `packages/core/src/writers/skills.spec.ts` — skills writer tests +- `packages/harnesses/src/util.ts` — `writeInlineSkills()` — writes SKILL.md to `.ade/skills//` +- `packages/harnesses/src/skills-installer.ts` — `installSkills()` — calls agentskills runAdd +- `packages/cli/src/commands/install.ts`, `setup.ts`, `configure.ts` — invoke writeInlineSkills + installSkills + +### Implementation tasks (Code phase) + +**T1: Extend `InlineSkill` type** (`packages/core/src/types.ts`) +- Add `assets?: Record` field to `InlineSkill` +- Keys are relative paths like `"references/folder-structure.md"` or `"scripts/setup.sh"` +- Values are file contents (strings) +- Add JSDoc explaining the path-as-type convention + +**T2: Fix bug in `writeInlineSkills`** (`packages/harnesses/src/util.ts`) +- The existing logic is inverted: it currently marks a skill as `modified` when the file is **unchanged** and writes when it differs — swap the logic +- Correct behavior: add to `modified` when content differs or file is new; skip write when identical + +**T3: Write asset files in `writeInlineSkills`** (`packages/harnesses/src/util.ts`) +- After writing `SKILL.md`, iterate over `skill.assets` if present +- For each entry `(relativePath, content)`: + - Resolve full path: `join(skillDir, relativePath)` + - Create parent directory with `mkdir(..., { recursive: true })` + - Write file content + - Track in `modified` if content differs from existing +- No special handling needed per asset type — the path convention is entirely up to the consumer + +**T4: Update tests in `skills.spec.ts`** (`packages/core/src/writers/skills.spec.ts`) +- Add test: `assets` field is passed through on inline skills (single asset) +- Add test: multiple assets with different path prefixes are all preserved + +**T5: Add/update tests for `writeInlineSkills`** (in `packages/harnesses/` test suite, if it exists; otherwise add tests) +- Test: skill with no assets writes only `SKILL.md` +- Test: skill with `assets` writes `SKILL.md` + each asset file at correct relative path +- Test: asset files in subdirectories (e.g. `references/foo.md`) have their parent dirs created +- Test: unchanged assets are not re-written (idempotent); changed assets are tracked in `modified` +- Test: bug fix — verify `modified` is populated correctly + +**T6: Update plan file and transition to Commit phase** + +## Accomplished + +- ✅ T1: Added `assets?: Record` to `InlineSkill` in `packages/core/src/types.ts` with JSDoc +- ✅ T2: Fixed inverted logic bug in `writeInlineSkills` — now correctly marks skill as modified only when content differs +- ✅ T3: Extended `writeInlineSkills` to write asset files — iterates `skill.assets`, resolves full paths, creates parent dirs, writes content, tracks in `modified` +- ✅ T4: Added two new tests in `skills.spec.ts` — single asset pass-through, multiple assets with different path prefixes +- ✅ T5: Added 10 new tests in `util.spec.ts` for `writeInlineSkills` — new skill, idempotency, modified detection, assets, subdirs, deduplication +- ✅ Full test suite: 254 tests across 32 files — all pass +- ✅ Typecheck: all 3 packages clean + +### KD-5: `InlineSkill` import in `util.ts` +Added `InlineSkill` to the explicit import from `@codemcp/ade-core` in `packages/harnesses/src/util.ts`. The harness resolves the type from `packages/core/dist/` (symlinked via pnpm workspace), so rebuilding core was required before the LSP and `tsc` agreed on the new type. + +## Explore + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + + +## Plan + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + + +## Code + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + + +## Commit + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 184323c..89a42a8 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -46,6 +46,17 @@ export interface InlineSkill { name: string; description: string; body: string; + /** + * Additional asset files to write alongside SKILL.md. + * + * Keys are relative paths such as `"references/folder-structure.md"` or + * `"scripts/setup.sh"`. The path itself signals the file type and location: + * it maps 1:1 to the output structure under `.ade/skills//`. + * + * Values are the raw file contents (strings). Authors reference these files + * naturally from the Markdown body, e.g. `[see details](references/folder-structure.md)`. + */ + assets?: Record; /** * Names of other skills that this skill supersedes. * Any skill whose `name` appears here will be removed from the final diff --git a/packages/core/src/writers/skills.spec.ts b/packages/core/src/writers/skills.spec.ts index 7dfbf83..dfd3f75 100644 --- a/packages/core/src/writers/skills.spec.ts +++ b/packages/core/src/writers/skills.spec.ts @@ -109,6 +109,62 @@ describe("skillsWriter", () => { }); }); + it("passes through single assets entry on inline skill", async () => { + const result = await skillsWriter.write( + { + skills: [ + { + name: "arch-skill", + description: "Architecture skill", + body: "See [details](references/details.md).", + assets: { + "references/details.md": "## Details\n\nMore info." + } + } + ] + }, + emptyContext + ); + + expect(result.skills).toHaveLength(1); + expect(result.skills![0]).toEqual({ + name: "arch-skill", + description: "Architecture skill", + body: "See [details](references/details.md).", + assets: { + "references/details.md": "## Details\n\nMore info." + } + }); + }); + + it("passes through multiple assets with different path prefixes", async () => { + const assets = { + "references/folder-structure.md": + "## Folder Structure\n\nDetailed content.", + "references/file-naming.md": "## File Naming\n\nConventions.", + "scripts/setup.sh": "#!/bin/bash\necho setup" + }; + + const result = await skillsWriter.write( + { + skills: [ + { + name: "multi-asset-skill", + description: "Skill with many assets", + body: "Main body.", + assets + } + ] + }, + emptyContext + ); + + expect(result.skills).toHaveLength(1); + expect( + (result.skills![0] as { assets: Record }).assets + ).toEqual(assets); + }); + it("passes through replaces field on external skills", async () => { const result = await skillsWriter.write( { diff --git a/packages/harnesses/src/util.spec.ts b/packages/harnesses/src/util.spec.ts index 6731f5b..317ceab 100644 --- a/packages/harnesses/src/util.spec.ts +++ b/packages/harnesses/src/util.spec.ts @@ -1,10 +1,27 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { mkdtemp, rm, mkdir, readFile, stat } from "node:fs/promises"; +import { + mkdtemp, + rm, + mkdir, + readFile, + stat, + writeFile +} from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import * as clack from "@clack/prompts"; -import type { GitHook } from "@codemcp/ade-core"; -import { writeGitHooks } from "./util.js"; +import type { GitHook, LogicalConfig } from "@codemcp/ade-core"; +import { writeGitHooks, writeInlineSkills, formatYamlValue } from "./util.js"; + +const emptyConfig = (): LogicalConfig => ({ + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [] +}); describe("writeGitHooks", () => { let dir: string; @@ -95,3 +112,250 @@ describe("writeGitHooks", () => { expect(prePush).toBe(hooks[1].script); }); }); + +describe("writeInlineSkills", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-util-inline-skills-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("returns empty array when there are no skills", async () => { + const config = emptyConfig(); + const result = await writeInlineSkills(config, dir); + expect(result).toEqual([]); + }); + + it("skips external skills (no body)", async () => { + const config = emptyConfig(); + config.skills = [{ name: "ext-skill", source: "org/repo/skills/ext" }]; + const result = await writeInlineSkills(config, dir); + expect(result).toEqual([]); + }); + + it("writes SKILL.md for a new inline skill", async () => { + const config = emptyConfig(); + config.skills = [ + { name: "my-skill", description: "A test skill", body: "Do the thing." } + ]; + + const result = await writeInlineSkills(config, dir); + + expect(result).toEqual(["my-skill"]); + + const content = await readFile( + join(dir, ".ade", "skills", "my-skill", "SKILL.md"), + "utf-8" + ); + expect(content).toBe( + "---\nname: my-skill\ndescription: A test skill\n---\n\nDo the thing.\n" + ); + }); + + it("does not re-write SKILL.md when content is unchanged (idempotent)", async () => { + const config = emptyConfig(); + config.skills = [ + { name: "my-skill", description: "A skill", body: "Body." } + ]; + + // First write + await writeInlineSkills(config, dir); + + // Second write — should be a no-op, returning empty modified list + const result = await writeInlineSkills(config, dir); + expect(result).toEqual([]); + }); + + it("tracks skill as modified when SKILL.md content changes", async () => { + const skillDir = join(dir, ".ade", "skills", "my-skill"); + await mkdir(skillDir, { recursive: true }); + await writeFile( + join(skillDir, "SKILL.md"), + "---\nname: my-skill\ndescription: Old\n---\n\nOld body.\n", + "utf-8" + ); + + const config = emptyConfig(); + config.skills = [ + { name: "my-skill", description: "New", body: "New body." } + ]; + + const result = await writeInlineSkills(config, dir); + expect(result).toContain("my-skill"); + }); + + it("writes asset files alongside SKILL.md", async () => { + const config = emptyConfig(); + config.skills = [ + { + name: "arch-skill", + description: "Architecture skill", + body: "See [details](references/details.md).", + assets: { + "references/details.md": "## Details\n\nMore info." + } + } + ]; + + await writeInlineSkills(config, dir); + + const assetContent = await readFile( + join(dir, ".ade", "skills", "arch-skill", "references", "details.md"), + "utf-8" + ); + expect(assetContent).toBe("## Details\n\nMore info."); + }); + + it("creates subdirectories for asset files", async () => { + const config = emptyConfig(); + config.skills = [ + { + name: "arch-skill", + description: "Arch", + body: "Body.", + assets: { + "references/nested/deep.md": "Deep content.", + "scripts/setup.sh": "#!/bin/bash\n" + } + } + ]; + + await writeInlineSkills(config, dir); + + const skillBase = join(dir, ".ade", "skills", "arch-skill"); + + const deep = await readFile( + join(skillBase, "references", "nested", "deep.md"), + "utf-8" + ); + expect(deep).toBe("Deep content."); + + const script = await readFile( + join(skillBase, "scripts", "setup.sh"), + "utf-8" + ); + expect(script).toBe("#!/bin/bash\n"); + }); + + it("does not re-write unchanged assets (idempotent)", async () => { + const config = emptyConfig(); + config.skills = [ + { + name: "arch-skill", + description: "Arch", + body: "Body.", + assets: { "references/foo.md": "Foo content." } + } + ]; + + // First write + await writeInlineSkills(config, dir); + + // Second write — nothing changed + const result = await writeInlineSkills(config, dir); + expect(result).toEqual([]); + }); + + it("tracks skill as modified when an asset changes", async () => { + const skillDir = join(dir, ".ade", "skills", "arch-skill"); + const refsDir = join(skillDir, "references"); + await mkdir(refsDir, { recursive: true }); + + const expected = "---\nname: arch-skill\ndescription: Arch\n---\n\nBody.\n"; + await writeFile(join(skillDir, "SKILL.md"), expected, "utf-8"); + await writeFile(join(refsDir, "foo.md"), "Old content.", "utf-8"); + + const config = emptyConfig(); + config.skills = [ + { + name: "arch-skill", + description: "Arch", + body: "Body.", + assets: { "references/foo.md": "New content." } + } + ]; + + const result = await writeInlineSkills(config, dir); + expect(result).toContain("arch-skill"); + + const written = await readFile(join(refsDir, "foo.md"), "utf-8"); + expect(written).toBe("New content."); + }); + + it("does not duplicate skill name in modified when both SKILL.md and asset change", async () => { + const config = emptyConfig(); + config.skills = [ + { + name: "new-skill", + description: "New", + body: "Body.", + assets: { "references/foo.md": "Foo." } + } + ]; + + const result = await writeInlineSkills(config, dir); + // skill name should appear only once + expect(result.filter((n) => n === "new-skill")).toHaveLength(1); + }); + + it("quotes description containing colon-space in YAML frontmatter", async () => { + const config = emptyConfig(); + config.skills = [ + { + name: "sabdx-arch", + description: + "Explains the topology of @sabdx projects: folder layout, file naming.", + body: "Body." + } + ]; + + await writeInlineSkills(config, dir); + + const content = await readFile( + join(dir, ".ade", "skills", "sabdx-arch", "SKILL.md"), + "utf-8" + ); + // description must be double-quoted so YAML parsers don't choke on ': ' + expect(content).toContain( + 'description: "Explains the topology of @sabdx projects: folder layout, file naming."' + ); + }); +}); + +describe("formatYamlValue", () => { + it("returns value unchanged when no special chars", () => { + expect(formatYamlValue("Simple description")).toBe("Simple description"); + }); + + it("quotes when value contains colon-space", () => { + expect(formatYamlValue("Key: value pair here")).toBe( + '"Key: value pair here"' + ); + }); + + it("quotes when value contains a hash", () => { + expect(formatYamlValue("Use # for comments")).toBe('"Use # for comments"'); + }); + + it("quotes when value starts with @", () => { + expect(formatYamlValue("@sabdx project")).toBe('"@sabdx project"'); + }); + + it("quotes when value starts with {", () => { + expect(formatYamlValue("{key: val}")).toBe('"{key: val}"'); + }); + + it("escapes internal double-quotes when quoting", () => { + expect(formatYamlValue('Say "hello": world')).toBe( + '"Say \\"hello\\": world"' + ); + }); + + it("does not quote em-dashes or other safe unicode", () => { + expect(formatYamlValue("Use this — not that")).toBe("Use this — not that"); + }); +}); diff --git a/packages/harnesses/src/util.ts b/packages/harnesses/src/util.ts index 690c4aa..585b7fc 100644 --- a/packages/harnesses/src/util.ts +++ b/packages/harnesses/src/util.ts @@ -224,24 +224,50 @@ export async function writeInlineSkills( const frontmatter = [ "---", `name: ${skill.name}`, - `description: ${skill.description}`, + `description: ${formatYamlValue(skill.description)}`, "---" ].join("\n"); const expected = `${frontmatter}\n\n${skill.body}\n`; + // Write SKILL.md — track as modified only when content changes or file is new + let skillMdChanged = false; try { const existing = await readFile(skillPath, "utf-8"); - if (existing !== expected) { - modified.push(skill.name); - continue; - } + skillMdChanged = existing !== expected; } catch { - // File doesn't exist yet — fall through to write + // File doesn't exist yet — treat as changed + skillMdChanged = true; + } + + if (skillMdChanged) { + await mkdir(skillDir, { recursive: true }); + await writeFile(skillPath, expected, "utf-8"); + modified.push(skill.name); } - await mkdir(skillDir, { recursive: true }); - await writeFile(skillPath, expected, "utf-8"); + // Write asset files (e.g. references/foo.md, scripts/setup.sh) + if (skill.assets) { + for (const [relativePath, content] of Object.entries(skill.assets)) { + const assetPath = join(skillDir, relativePath); + let assetChanged = false; + try { + const existing = await readFile(assetPath, "utf-8"); + assetChanged = existing !== content; + } catch { + // File doesn't exist yet — treat as changed + assetChanged = true; + } + + if (assetChanged) { + await mkdir(dirname(assetPath), { recursive: true }); + await writeFile(assetPath, content, "utf-8"); + if (!modified.includes(skill.name)) { + modified.push(skill.name); + } + } + } + } } return modified; @@ -257,3 +283,21 @@ export function formatYamlKey(value: string): string { ? value : JSON.stringify(value); } + +/** + * Format a YAML scalar value, adding double-quote wrapping when the value + * contains characters that a YAML parser would otherwise misinterpret: + * + * - `: ` (colon-space) — parsed as a mapping entry separator + * - `#` — parsed as a comment start + * - Leading `@`, `{`, `}`, `[`, `]`, `|`, `>`, `'`, `"`, `&`, `*`, `!`, `%` + * — YAML indicator characters + * + * Double-quotes inside the value are escaped as `\"`. + */ +export function formatYamlValue(value: string): string { + const needsQuoting = + /: /.test(value) || /#/.test(value) || /^[@{}[\]|>'"`&*!%]/.test(value); + if (!needsQuoting) return value; + return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; +}