diff --git a/changelog.md b/changelog.md index 3ba6779..0f914e7 100644 --- a/changelog.md +++ b/changelog.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `AIService.diagnose()` now uses the `static.AI_DIR` constant (`.agents`) instead of the hardcoded `/.ai` path - `coldbox ai uninstall` now correctly checks, removes, and references the `.agents` directory - `coldbox ai skills add slug --list` was not working. +- **`coldbox ai skills remove` reinstalling removed skills during refresh** + - When a skill was removed via `coldbox ai skills remove`, the subsequent agent config regeneration step (`refresh()`) would detect the skill as "missing" (because its module dependency was still present in `box.json`) and immediately reinstall it + - Removed skills are now tracked in a new `manifest.excludes[]` array so that `refresh()` will never auto-reinstall them + - Explicitly re-installing a previously excluded skill via `coldbox ai skills install` lifts the exclusion and restores normal auto-management ### Added diff --git a/models/SkillManager.cfc b/models/SkillManager.cfc index c64ad8a..dabd32e 100644 --- a/models/SkillManager.cfc +++ b/models/SkillManager.cfc @@ -186,6 +186,9 @@ component singleton { // Ensure customSkills key exists (backwards compatibility with old manifests) ensureCustomSkillsSection( arguments.manifest ) + // Ensure excludes key exists (backwards compatibility with old manifests) + ensureExcludesSection( arguments.manifest ) + // ------------------------------------------------------------------ // 0. Install missing desired skills (core + module) not yet in manifest // ------------------------------------------------------------------ @@ -195,6 +198,10 @@ component singleton { ); var missingDesiredSkills = desiredTargets.filter( ( t ) => { + // Skip skills the user has explicitly excluded + if ( arguments.manifest.excludes.findNoCase( t.name ) ) { + return false; + } return !manifest.skills .filter( ( s ) => { return ( s.owner == t.owner && s.repo == t.repo && s.slug == t.slug ); @@ -828,6 +835,13 @@ component singleton { if ( structKeyExists( manifest, "customSkills" ) ) { manifest.customSkills = manifest.customSkills.filter( ( s ) => s.name != name ) } + + // Track the explicit exclusion so refresh() does not auto-reinstall it + ensureExcludesSection( manifest ) + if ( !manifest.excludes.findNoCase( arguments.name ) ) { + manifest.excludes.append( arguments.name ) + } + variables.aiService.saveManifest( arguments.directory, manifest ) return true @@ -1345,6 +1359,10 @@ component singleton { arguments.manifest.skills.append( entry ) } + // If this skill was previously excluded, lift the exclusion now that it is being explicitly installed + ensureExcludesSection( arguments.manifest ) + arguments.manifest.excludes = arguments.manifest.excludes.filter( ( excludedName ) => !excludedName.equalsIgnoreCase( resolvedName ) ) + return resolvedName } @@ -1397,6 +1415,20 @@ component singleton { } } + /** + * Ensure manifest has an excludes array (backwards compatibility). + * The excludes array holds skill names that the user explicitly removed and + * should never be auto-reinstalled by refresh(). + * Mutates manifest in place. + * + * @manifest The manifest struct to ensure has an excludes key + */ + private function ensureExcludesSection( required struct manifest ){ + if ( !structKeyExists( arguments.manifest, "excludes" ) ) { + arguments.manifest[ "excludes" ] = [] + } + } + /** * Delete a skill directory under .ai/skills/ if it exists. *