Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 32 additions & 0 deletions models/SkillManager.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ------------------------------------------------------------------
Expand All @@ -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 );
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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.
*
Expand Down