From eb0359aca1dd220d02c8ce45ca4b819cb9473b1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 15:04:14 +0000 Subject: [PATCH 1/4] Initial plan From 214c62f26f7678fe615854e0837710384ace5c73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 15:12:08 +0000 Subject: [PATCH 2/4] feat: separate custom skills into skills-custom/ folder with dedicated manifest section Agent-Logs-Url: https://github.com/ColdBox/coldbox-cli/sessions/bd6c5d76-044f-4462-b8fb-459c2651fc07 Co-authored-by: lmajano <137111+lmajano@users.noreply.github.com> --- commands/coldbox/ai/skills/create.cfc | 4 +- commands/coldbox/ai/skills/list.cfc | 18 +- commands/coldbox/ai/skills/override.cfc | 4 +- commands/coldbox/ai/skills/remove.cfc | 2 +- models/AIService.cfc | 30 +-- models/AgentRegistry.cfc | 13 +- models/SkillManager.cfc | 194 ++++++++++++------ .../ai/agents/agent-flat-instructions.md | 32 +-- .../ai/agents/agent-modern-instructions.md | 24 ++- 9 files changed, 209 insertions(+), 112 deletions(-) diff --git a/commands/coldbox/ai/skills/create.cfc b/commands/coldbox/ai/skills/create.cfc index 553bd0c..b4e37b8 100644 --- a/commands/coldbox/ai/skills/create.cfc +++ b/commands/coldbox/ai/skills/create.cfc @@ -1,6 +1,6 @@ /** * Create a custom skill template - * Scaffolds a new skill in .agents/skills/{name}/ + * Scaffolds a new skill in .agents/skills-custom/{name}/ * * Examples: * coldbox ai skills create api-development @@ -44,7 +44,7 @@ component extends="coldbox-cli.models.BaseAICommand" { printInfo( "Creating custom skill: #arguments.name# (#uCase( language )#)" ) // Check if already exists - var skillPath = skillManager.getSkillsDirectory( arguments.directory ) & "/#arguments.name#/SKILL.md" + var skillPath = skillManager.getCustomSkillsDirectory( arguments.directory ) & "/#arguments.name#/SKILL.md" if ( fileExists( skillPath ) ) { printError( "Skill '#arguments.name#' already exists at:" ) printError( " #skillPath#" ) diff --git a/commands/coldbox/ai/skills/list.cfc b/commands/coldbox/ai/skills/list.cfc index 10a50ba..f5fa40f 100644 --- a/commands/coldbox/ai/skills/list.cfc +++ b/commands/coldbox/ai/skills/list.cfc @@ -37,28 +37,32 @@ component extends="coldbox-cli.models.BaseAICommand" { printSuccess( "All skills are up to date." ) return } - info.skills = info.skills.filter( ( s ) => staleNames.find( s.name ) > 0 ) + info.skills = info.skills.filter( ( s ) => staleNames.find( s.name ) > 0 ) + info.customSkills = [] // custom skills have no remote SHA, cannot be outdated printWarn( "#staleNames.len()# skill(s) have updates available:" ) print.line() } - if ( info.skills.isEmpty() ) { + if ( info.skills.isEmpty() && info.customSkills.isEmpty() ) { printWarn( "No skills installed. Run 'coldbox ai skills install --list' to browse the registry." ) return } - // Group by owner/repo (custom skills get bucket "custom") + // Group by owner/repo (custom skills in their own bucket from customSkills manifest section) var groups = {} info.skills.each( ( skill ) => { - var bucket = ( skill.type ?: "" ) == "custom" - ? "custom" - : ( ( skill.owner ?: "" ) != "" ? "#skill.owner#/#skill.repo#" : "unknown" ) + var bucket = ( skill.owner ?: "" ) != "" ? "#skill.owner#/#skill.repo#" : "unknown" if ( !groups.keyExists( bucket ) ) { groups[ bucket ] = [] } groups[ bucket ].append( skill ) } ) + // Add custom skills from manifest.customSkills + if ( info.customSkills.len() ) { + groups[ "custom" ] = info.customSkills + } + // Sort groups: custom last, then alphabetical var groupKeys = groups .keyArray() @@ -105,7 +109,7 @@ component extends="coldbox-cli.models.BaseAICommand" { // Summary print.line() - printInfo( "Total: #info.skills.len()# skill(s) installed" ) + printInfo( "Total: #info.skills.len() + info.customSkills.len()# skill(s) installed" ) print.line() if ( !outdated ) { diff --git a/commands/coldbox/ai/skills/override.cfc b/commands/coldbox/ai/skills/override.cfc index e16c1de..703526b 100644 --- a/commands/coldbox/ai/skills/override.cfc +++ b/commands/coldbox/ai/skills/override.cfc @@ -48,8 +48,8 @@ component extends="coldbox-cli.models.BaseAICommand" { var skill = existing[ 1 ] - // Check if override already exists (flat path) - var overridePath = skillManager.getSkillsDirectory( arguments.directory ) & "/#arguments.name#/SKILL.md" + // Check if override already exists (in skills-custom/) + var overridePath = skillManager.getCustomSkillsDirectory( arguments.directory ) & "/#arguments.name#/SKILL.md" if ( fileExists( overridePath ) ) { printWarn( "Skill '#arguments.name#' already exists at:" ) printWarn( " #overridePath#" ) diff --git a/commands/coldbox/ai/skills/remove.cfc b/commands/coldbox/ai/skills/remove.cfc index 5d0ee7d..a179c24 100644 --- a/commands/coldbox/ai/skills/remove.cfc +++ b/commands/coldbox/ai/skills/remove.cfc @@ -1,6 +1,6 @@ /** * Remove a skill from the project by name. - * Skills are stored in the flat .agents/skills/{name}/ directory. + * Checks both .agents/skills/{name}/ and .agents/skills-custom/{name}/ directories. * * Examples: * coldbox ai skills remove boxlang-syntax diff --git a/models/AIService.cfc b/models/AIService.cfc index 2af716b..25ce026 100644 --- a/models/AIService.cfc +++ b/models/AIService.cfc @@ -70,6 +70,7 @@ component singleton { "templateType" : templateType, "guidelines" : [], "skills" : [], + "customSkills" : [], "agents" : listToArray( arguments.agents ), "mcpServers" : { "core" : [], @@ -258,6 +259,7 @@ component singleton { "lastSync" : manifest.lastSync ?: "never", "guidelines" : manifest.guidelines ?: [], "skills" : manifest.skills ?: [], + "customSkills" : manifest.customSkills ?: [], "agents" : manifest.agents ?: [], "mcpServers" : manifest.mcpServers ?: { "core" : [], @@ -439,7 +441,8 @@ component singleton { "#aiDir#/guidelines", "#aiDir#/guidelines/core", "#aiDir#/guidelines/custom", - "#aiDir#/skills" + "#aiDir#/skills", + "#aiDir#/skills-custom" ]; dirs.each( ( dir ) => { @@ -474,10 +477,10 @@ component singleton { "onDemandSize" : 0 }, "skills" : { - "total" : info.skills.len(), + "total" : info.skills.len() + info.customSkills.len(), "core" : 0, "module" : 0, - "custom" : 0, + "custom" : info.customSkills.len(), "override" : 0, "totalSize" : 0, "avgSize" : 0 @@ -565,20 +568,23 @@ component singleton { stats.skills.override++; } else if ( source == "core" ) { stats.skills.core++; - } else if ( source == "custom" || type == "custom" ) { - stats.skills.custom++; } else { stats.skills.module++; } } ); - // Skills size (all on-demand) - var skillsDir = aiDir & "/skills"; + // Skills size (all on-demand) โ€” includes both skills/ and skills-custom/ + var skillsDir = aiDir & "/skills"; + var customSkillsDir = aiDir & "/skills-custom"; + var skillsSize = 0; if ( directoryExists( skillsDir ) ) { - var skillsSize = calculateDirectorySize( skillsDir ); - stats.skills.totalSize = skillsSize; - stats.skills.avgSize = stats.skills.total > 0 ? int( skillsSize / stats.skills.total ) : 0; + skillsSize += calculateDirectorySize( skillsDir ); } + if ( directoryExists( customSkillsDir ) ) { + skillsSize += calculateDirectorySize( customSkillsDir ); + } + stats.skills.totalSize = skillsSize; + stats.skills.avgSize = stats.skills.total > 0 ? int( skillsSize / stats.skills.total ) : 0; // Count MCP servers var mcpServers = manifest.mcpServers ?: { @@ -607,10 +613,10 @@ component singleton { // Inlined guidelines (part of base context, shown separately for clarity) stats.contextEstimate.inlinedKB = int( stats.guidelines.inlinedSize / 1024 ); // On-demand resources (not in base context, but available) - stats.contextEstimate.onDemandKB = int( ( stats.guidelines.onDemandSize + stats.skills.totalSize ) / 1024 ); + stats.contextEstimate.onDemandKB = int( ( stats.guidelines.onDemandSize + skillsSize ) / 1024 ); // Total available if all resources were loaded stats.contextEstimate.totalAvailableKB = int( - ( stats.agents.filesSize + stats.guidelines.onDemandSize + stats.skills.totalSize ) / 1024 + ( stats.agents.filesSize + stats.guidelines.onDemandSize + skillsSize ) / 1024 ); return stats; diff --git a/models/AgentRegistry.cfc b/models/AgentRegistry.cfc index fefcb22..02da38a 100644 --- a/models/AgentRegistry.cfc +++ b/models/AgentRegistry.cfc @@ -710,7 +710,10 @@ component singleton { var aiService = variables.wirebox.getInstance( "AIService@coldbox-cli" ) var manifest = aiService.loadManifest( arguments.directory ) - if ( !structKeyExists( manifest, "skills" ) || !manifest.skills.len() ) { + var hasSkills = structKeyExists( manifest, "skills" ) && manifest.skills.len() + var hasCustomSkills = structKeyExists( manifest, "customSkills" ) && manifest.customSkills.len() + + if ( !hasSkills && !hasCustomSkills ) { return "No skills installed yet. Run 'coldbox ai install' to get started." } @@ -726,9 +729,9 @@ component singleton { } var content = [] - var coreSkills = manifest.skills.filter( ( s ) => s.source == "core" ) - var moduleSkills = manifest.skills.filter( ( s ) => s.source != "core" && s.source != "custom" ) - var customSkills = manifest.skills.filter( ( s ) => s.source == "custom" ) + var coreSkills = hasSkills ? manifest.skills.filter( ( s ) => s.source == "core" ) : [] + var moduleSkills = hasSkills ? manifest.skills.filter( ( s ) => s.source != "core" && s.source != "custom" ) : [] + var customSkills = manifest.customSkills ?: [] // Helper: group skills by prefix and append formatted output to content var appendGroupedSkills = ( skills, sectionLabel ) => { @@ -772,7 +775,7 @@ component singleton { appendGroupedSkills( moduleSkills, "Module Skills" ) appendGroupedSkills( customSkills, "Custom Skills" ) - content.append( "**To load a skill:** Use `read_file` on `.ai/skills/{skill-name}/SKILL.md` (e.g., `.ai/skills/coldbox-handler-development/SKILL.md`)." ) + content.append( "**To load a skill:** Use `read_file` on `.agents/skills/{skill-name}/SKILL.md` (e.g., `.agents/skills/coldbox-handler-development/SKILL.md`) for core skills, or `.agents/skills-custom/{skill-name}/SKILL.md` for custom project skills." ) return content.toList( chr( 10 ) ) } diff --git a/models/SkillManager.cfc b/models/SkillManager.cfc index fbb987d..eebce87 100644 --- a/models/SkillManager.cfc +++ b/models/SkillManager.cfc @@ -1,16 +1,26 @@ /** - * Manages AI skills โ€” remote-first, SHA-locked, flat storage. + * Manages AI skills โ€” remote-first, SHA-locked storage. * - * Skills are downloaded from skills.boxlang.io and stored at: + * Core/framework skills are downloaded from skills.boxlang.io and stored at: * {project}/.agents/skills/{name}/SKILL.md * - * The manifest records sha (from registry), owner, repo, path, and syncedAt. + * Project-authored (custom) skills are stored separately at: + * {project}/.agents/skills-custom/{name}/SKILL.md + * + * This separation allows .gitignore to ignore the auto-managed skills/ folder while + * safely committing project-specific skills in skills-custom/. + * + * The manifest records sha (from registry), owner, repo, path, and syncedAt for + * core/framework skills in manifest.skills[]. + * Custom skills are tracked in a separate manifest.customSkills[] array + * that is NOT SHA-hash-managed. + * * On refresh, stale skills (sha mismatch) are re-downloaded; orphaned module * skills (removed from box.json) are pruned automatically. * * Multi-directory lookup order for agent instructions: - * 1. .ai/skills/{name}/SKILL.md (coldbox-cli managed) - * 2. .agents/skills/{name}/SKILL.md + * 1. .agents/skills/{name}/SKILL.md (coldbox-cli managed) + * 2. .agents/skills-custom/{name}/SKILL.md (project custom skills) * 3. .claude/skills/{name}/SKILL.md */ component singleton { @@ -387,8 +397,16 @@ component singleton { var slug = skill.slug ?: "" if ( ( skill.type ?: "" ) != "custom" && owner.len() && repo.len() && slug.len() ) { missingRemoteSkills.append( skill ) - } else { - missingCustomSkills.append( skill.name ) + } + } + } + + // Check for deleted custom skills (in customSkills manifest section) + if ( structKeyExists( arguments.manifest, "customSkills" ) ) { + for ( var customSkill in arguments.manifest.customSkills ) { + var customSkillFile = getCustomSkillFilePath( arguments.directory, customSkill.name ) + if ( isNull( customSkillFile ) ) { + missingCustomSkills.append( customSkill.name ) } } } @@ -448,22 +466,28 @@ component singleton { // Remove custom skills whose files were deleted by the user missingCustomSkills.each( ( name ) => { variables.print.yellowLine( " ๐Ÿงน Removing deleted custom skill entry: #name#" ).toConsole() - manifest.skills = manifest.skills.filter( ( s ) => s.name != name ) + if ( !structKeyExists( arguments.manifest, "customSkills" ) ) { + arguments.manifest[ "customSkills" ] = [] + } + arguments.manifest.customSkills = arguments.manifest.customSkills.filter( ( s ) => s.name != name ) changes.removed.append( name ) } ) // ------------------------------------------------------------------ - // 5. Sync custom skills from .ai/skills/ that aren't in manifest yet + // 5. Sync custom skills from .agents/skills-custom/ that aren't in manifest yet // ------------------------------------------------------------------ - var skillsDir = getSkillsDirectory( arguments.directory ) - if ( directoryExists( skillsDir ) ) { - directoryList( skillsDir, false, "name" ).each( ( dirName ) => { - var skillFilePath = skillsDir & "/" & dirName & "/SKILL.md" + if ( !structKeyExists( arguments.manifest, "customSkills" ) ) { + arguments.manifest[ "customSkills" ] = [] + } + var customSkillsDir = getCustomSkillsDirectory( arguments.directory ) + if ( directoryExists( customSkillsDir ) ) { + directoryList( customSkillsDir, false, "name" ).each( ( dirName ) => { + var skillFilePath = customSkillsDir & "/" & dirName & "/SKILL.md" if ( !fileExists( skillFilePath ) ) { return; } - var alreadyInManifest = manifest.skills.filter( ( s ) => s.name == dirName ).len() > 0 + var alreadyInManifest = arguments.manifest.customSkills.filter( ( s ) => s.name == dirName ).len() > 0 if ( alreadyInManifest ) { return; } @@ -474,15 +498,9 @@ component singleton { var parsed = variables.utility.parseFrontmatter( content ) var description = parsed.frontmatter.description ?: "" - manifest.skills.append( { + arguments.manifest.customSkills.append( { "name" : dirName, - "owner" : "", - "repo" : "", - "path" : "", - "sha" : "", "description" : description, - "type" : "custom", - "source" : "custom", "syncedAt" : dateTimeFormat( now(), "iso" ) } ) changes.added.append( dirName ) @@ -664,8 +682,8 @@ component singleton { /** * Return the absolute path to a skill's SKILL.md file, checking three locations: - * 1. {directory}/.agents/skills/{name}/SKILL.md - * 2. {directory}/.agents/skills/{name}/SKILL.md + * 1. {directory}/.agents/skills/{name}/SKILL.md (core/framework skills) + * 2. {directory}/.agents/skills-custom/{name}/SKILL.md (project custom skills) * 3. {directory}/.claude/skills/{name}/SKILL.md * * @directory The project directory @@ -677,9 +695,11 @@ component singleton { required string directory, required string name ){ - var skillsDirectory = getSkillsDirectory( arguments.directory ) - var candidates = [ + var skillsDirectory = getSkillsDirectory( arguments.directory ) + var customSkillsDirectory = getCustomSkillsDirectory( arguments.directory ) + var candidates = [ skillsDirectory & "/#arguments.name#/SKILL.md", + customSkillsDirectory & "/#arguments.name#/SKILL.md", "#arguments.directory#/.claude/skills/#arguments.name#/SKILL.md" ] for ( var candidate in candidates ) { @@ -689,7 +709,24 @@ component singleton { } /** - * Create a custom skill from template in the flat .ai/skills/{name}/ directory. + * Return the absolute path to a custom skill's SKILL.md file. + * Only checks {directory}/.agents/skills-custom/{name}/SKILL.md. + * + * @directory The project directory + * @name The skill name (directory name) + * + * @return Absolute path string, or null if not found + */ + function getCustomSkillFilePath( + required string directory, + required string name + ){ + var candidate = getCustomSkillsDirectory( arguments.directory ) & "/#arguments.name#/SKILL.md" + return fileExists( candidate ) ? candidate : javacast( "null", "" ) + } + + /** + * Create a custom skill from template in the .agents/skills-custom/{name}/ directory. * * @directory The project directory * @name The custom skill name @@ -700,7 +737,7 @@ component singleton { required string name, string language = "boxlang" ){ - var targetDir = getSkillsDirectory( arguments.directory ) & "/#arguments.name#" + var targetDir = getCustomSkillsDirectory( arguments.directory ) & "/#arguments.name#" var skillFile = "#targetDir#/SKILL.md" if ( !directoryExists( targetDir ) ) { @@ -719,22 +756,19 @@ component singleton { fileWrite( skillFile, template ) var manifest = variables.aiService.loadManifest( arguments.directory ); - manifest.skills.append( { + if ( !structKeyExists( manifest, "customSkills" ) ) { + manifest[ "customSkills" ] = [] + } + manifest.customSkills.append( { "name" : arguments.name, - "owner" : "", - "repo" : "", - "path" : "", - "sha" : "", "description" : "", - "type" : "custom", - "source" : "custom", "syncedAt" : dateTimeFormat( now(), "iso" ) } ) variables.aiService.saveManifest( arguments.directory, manifest ) } /** - * Check if a skill exists in any of the three skill directories. + * Check if a skill exists in any of the skill directories (skills/ or skills-custom/). * * @directory The project directory * @name The skill name (directory name) @@ -745,12 +779,13 @@ component singleton { required string directory, required string name ){ - var skillDir = getSkillsDirectory( arguments.directory ) & "/#arguments.name#" - return directoryExists( skillDir ) + var skillDir = getSkillsDirectory( arguments.directory ) & "/#arguments.name#" + var customSkillDir = getCustomSkillsDirectory( arguments.directory ) & "/#arguments.name#" + return directoryExists( skillDir ) || directoryExists( customSkillDir ) } /** - * Remove a skill from the project (flat path). + * Remove a skill from the project (checks skills/ then skills-custom/). * * @directory The project directory * @name The skill name to remove @@ -762,27 +797,37 @@ component singleton { required string directory, required string name ){ - var skillDir = getSkillsDirectory( arguments.directory ) & "/#arguments.name#" + var skillDir = getSkillsDirectory( arguments.directory ) & "/#arguments.name#" + var customSkillDir = getCustomSkillsDirectory( arguments.directory ) & "/#arguments.name#" - if ( !directoryExists( skillDir ) ) { + if ( !directoryExists( skillDir ) && !directoryExists( customSkillDir ) ) { throw( type = "SkillManager.SkillNotFound", - message = "Skill '#arguments.name#' not found at: #skillDir#" + message = "Skill '#arguments.name#' not found at: #skillDir# or #customSkillDir#" ) } - // Delete the skill directory and all its contents - directoryDelete( skillDir, true ) - // Remove from manifest + // Delete whichever directory exists + if ( directoryExists( skillDir ) ) { + directoryDelete( skillDir, true ) + } + if ( directoryExists( customSkillDir ) ) { + directoryDelete( customSkillDir, true ) + } + + // Remove from manifest (both skills and customSkills sections) var manifest = variables.aiService.loadManifest( arguments.directory ) manifest.skills = manifest.skills.filter( ( s ) => s.name != name ) + if ( structKeyExists( manifest, "customSkills" ) ) { + manifest.customSkills = manifest.customSkills.filter( ( s ) => s.name != name ) + } variables.aiService.saveManifest( arguments.directory, manifest ) return true } /** - * Create a skill override (custom copy) in the flat .ai/skills/{name}/ directory. + * Create a skill override (custom copy) in the .agents/skills-custom/{name}/ directory. * Sets type=custom, empty owner/repo so refresh skips it. * * @directory The project directory @@ -798,7 +843,7 @@ component singleton { if ( isNull( sourcePath ) ) { throw( type = "SkillManager.SkillNotFound", - message = "Skill '#arguments.name#' not found in .ai/skills/, .agents/skills/, or .claude/skills/" + message = "Skill '#arguments.name#' not found in .agents/skills/, .agents/skills-custom/, or .claude/skills/" ) } @@ -826,36 +871,39 @@ component singleton { "all" ) - var targetDir = getSkillsDirectory( arguments.directory ) & "/#arguments.name#" + var targetDir = getCustomSkillsDirectory( arguments.directory ) & "/#arguments.name#" var targetFile = "#targetDir#/SKILL.md" if ( !directoryExists( targetDir ) ) directoryCreate( targetDir, true ) fileWrite( targetFile, content ) - // Find existing manifest entry + // Ensure customSkills section exists + if ( !structKeyExists( manifest, "customSkills" ) ) { + manifest[ "customSkills" ] = [] + } + + // Find existing customSkills entry for this skill var existingIndex = 0 - for ( var i = 1; i <= manifest.skills.len(); i++ ) { - if ( manifest.skills[ i ].name == arguments.name ) { + for ( var i = 1; i <= manifest.customSkills.len(); i++ ) { + if ( manifest.customSkills[ i ].name == arguments.name ) { existingIndex = i; break } } - var skillEntry = { + // Also remove from manifest.skills if it was there (migrating from old location) + manifest.skills = manifest.skills.filter( ( s ) => s.name != arguments.name ) + + var existingEntry = existingIndex ? manifest.customSkills[ existingIndex ] : {} + var skillEntry = { "name" : arguments.name, - "owner" : "", - "repo" : "", - "path" : "", - "sha" : "", - "description" : existingIndex ? ( manifest.skills[ existingIndex ].description ?: "" ) : "", - "type" : "custom", - "source" : "custom", + "description" : existingEntry.description ?: "", "syncedAt" : dateTimeFormat( now(), "iso" ) } if ( existingIndex ) { - manifest.skills[ existingIndex ] = skillEntry + manifest.customSkills[ existingIndex ] = skillEntry } else { - manifest.skills.append( skillEntry ) + manifest.customSkills.append( skillEntry ) } variables.aiService.saveManifest( arguments.directory, manifest ) @@ -886,6 +934,18 @@ component singleton { } } + // Also check custom skills + var customSkills = arguments.manifest.customSkills ?: [] + for ( var customSkill in customSkills ) { + var customSkillFile = getCustomSkillFilePath( arguments.directory, customSkill.name ) + if ( isNull( customSkillFile ) ) { + issues.warnings.append( "Missing custom skill file: #customSkill.name#" ) + issues.recommendations.append( + "Restore or recreate '#customSkill.name#' in .agents/skills-custom/, or run 'coldbox ai skills refresh' to clean up the manifest entry" + ) + } + } + return issues; } @@ -1296,7 +1356,7 @@ component singleton { /** - * Gets the skills directory path (.agents/skills) + * Gets the skills directory path (.agents/skills) for core/framework skills. * * @directory The target directory * @@ -1306,6 +1366,18 @@ component singleton { return variables.aiService.getAIInstallDirectory( arguments.directory ) & "/skills" } + /** + * Gets the custom skills directory path (.agents/skills-custom) for project-authored skills. + * Custom skills in this directory are meant to be committed to source control. + * + * @directory The target directory + * + * @return The full path to the skills-custom directory + */ + string function getCustomSkillsDirectory( required string directory ){ + return variables.aiService.getAIInstallDirectory( arguments.directory ) & "/skills-custom" + } + /** * Delete a skill directory under .ai/skills/ if it exists. * diff --git a/templates/ai/agents/agent-flat-instructions.md b/templates/ai/agents/agent-flat-instructions.md index 07289f9..10dee36 100644 --- a/templates/ai/agents/agent-flat-instructions.md +++ b/templates/ai/agents/agent-flat-instructions.md @@ -139,20 +139,24 @@ This project includes AI-powered development assistance with guidelines, skills, /modules/ - Module-specific guidelines /custom/ - Your custom guidelines /overrides/ - Override core guidelines - /skills/ - Implementation cookbooks (how-to guides) + /skills/ - Framework/core implementation cookbooks (auto-managed, can be .gitignored) /{name}/ - One folder per skill (flat, no subdirectories) - SKILL.md - Skill content (fetched from registry or created locally) + SKILL.md - Skill content (fetched from registry) + /skills-custom/ - Project-specific skills (commit these to source control) + /{name}/ - One folder per custom skill + SKILL.md - Custom skill content (project-authored or overrides) /mcp-servers/ - MCP server configurations ``` ### Manifest -The `.ai/manifest.json` file contains the complete AI integration configuration: +The `.agents/manifest.json` file contains the complete AI integration configuration: - **language**: Project language mode (boxlang, cfml, hybrid) - **templateType**: Application template (modern, flat) - **guidelines**: Array of installed guideline names -- **skills**: Array of installed skill names +- **skills**: Array of core/framework skill names (auto-managed) +- **customSkills**: Array of project-specific skill names (in `skills-custom/`) - **agents**: Array of configured AI agents - **mcpServers**: Configured MCP documentation servers (core, module, custom) - **activeAgent**: Currently active AI agent (if set) @@ -162,18 +166,20 @@ The `.ai/manifest.json` file contains the complete AI integration configuration: ### Using Guidelines & Skills -Guidelines and skills are stored locally in `.ai/` and loaded via `read_file` when needed: +Guidelines and skills are stored locally in `.agents/` and loaded via `read_file` when needed: -**Core Guidelines** (`.ai/guidelines/core/`) โ€” framework fundamentals: -- `read_file` on `.ai/guidelines/core/coldbox.md` โ€” ColdBox conventions, handler/routing/DI reference -- `read_file` on `.ai/guidelines/core/boxlang.md` โ€” BoxLang syntax, classes, lambdas (or `cfml.md` for CFML) +**Core Guidelines** (`.agents/guidelines/core/`) โ€” framework fundamentals: +- `read_file` on `.agents/guidelines/core/coldbox.md` โ€” ColdBox conventions, handler/routing/DI reference +- `read_file` on `.agents/guidelines/core/boxlang.md` โ€” BoxLang syntax, classes, lambdas (or `cfml.md` for CFML) -**Module/Custom Guidelines** โ€” load by name on request from `.ai/guidelines/modules/` or `.ai/guidelines/custom/`. +**Module/Custom Guidelines** โ€” load by name on request from `.agents/guidelines/modules/` or `.agents/guidelines/custom/`. -**Skills** (`.ai/skills/{name}/SKILL.md`) โ€” step-by-step implementation patterns. Examples: -- Implement a CRUD handler: `read_file` on `.ai/skills/coldbox-handler-development/SKILL.md` -- Build a REST API: `read_file` on `.ai/skills/coldbox-rest-api-development/SKILL.md` -- Write tests: `read_file` on `.ai/skills/coldbox-testing-handler/SKILL.md` +**Skills** (`.agents/skills/{name}/SKILL.md`) โ€” step-by-step implementation patterns. Examples: +- Implement a CRUD handler: `read_file` on `.agents/skills/coldbox-handler-development/SKILL.md` +- Build a REST API: `read_file` on `.agents/skills/coldbox-rest-api-development/SKILL.md` +- Write tests: `read_file` on `.agents/skills/coldbox-testing-handler/SKILL.md` + +**Custom Skills** (`.agents/skills-custom/{name}/SKILL.md`) โ€” project-specific patterns. Load by name from `.agents/skills-custom/`. **To load any skill or guideline:** use `read_file` on the path shown above or in the inventories below. diff --git a/templates/ai/agents/agent-modern-instructions.md b/templates/ai/agents/agent-modern-instructions.md index c0df357..d5108e5 100644 --- a/templates/ai/agents/agent-modern-instructions.md +++ b/templates/ai/agents/agent-modern-instructions.md @@ -176,20 +176,24 @@ This project includes AI-powered development assistance with guidelines, skills, /modules/ - Module-specific guidelines /custom/ - Your custom guidelines /overrides/ - Override core guidelines - /skills/ - Implementation cookbooks (how-to guides) + /skills/ - Framework/core implementation cookbooks (auto-managed, can be .gitignored) /{name}/ - One folder per skill (flat, no subdirectories) - SKILL.md - Skill content (fetched from registry or created locally) + SKILL.md - Skill content (fetched from registry) + /skills-custom/ - Project-specific skills (commit these to source control) + /{name}/ - One folder per custom skill + SKILL.md - Custom skill content (project-authored or overrides) /mcp-servers/ - MCP server configurations ``` ### Manifest -The `.ai/manifest.json` file contains the complete AI integration configuration: +The `.agents/manifest.json` file contains the complete AI integration configuration: - **language**: Project language mode (boxlang, cfml, hybrid) - **templateType**: Application template (modern, flat) - **guidelines**: Array of installed guideline names -- **skills**: Array of installed skill names +- **skills**: Array of core/framework skill names (auto-managed) +- **customSkills**: Array of project-specific skill names (in `skills-custom/`) - **agents**: Array of configured AI agents - **mcpServers**: Configured MCP documentation servers (core, module, custom) - **activeAgent**: Currently active AI agent (if set) @@ -199,7 +203,7 @@ The `.ai/manifest.json` file contains the complete AI integration configuration: ### Using Guidelines & Skills -Guidelines and skills are stored locally in `.ai/` and loaded via `read_file` when needed: +Guidelines and skills are stored locally in `.agents/` and loaded via `read_file` when needed: **Core Guidelines** (`.ai/guidelines/core/`) โ€” framework fundamentals: - `read_file` on `.ai/guidelines/core/coldbox.md` โ€” ColdBox conventions, handler/routing/DI reference @@ -207,10 +211,12 @@ Guidelines and skills are stored locally in `.ai/` and loaded via `read_file` wh **Module/Custom Guidelines** โ€” load by name on request from `.ai/guidelines/modules/` or `.ai/guidelines/custom/`. -**Skills** (`.ai/skills/{name}/SKILL.md`) โ€” step-by-step implementation patterns. Examples: -- Implement a CRUD handler: `read_file` on `.ai/skills/coldbox-handler-development/SKILL.md` -- Build a REST API: `read_file` on `.ai/skills/coldbox-rest-api-development/SKILL.md` -- Write tests: `read_file` on `.ai/skills/coldbox-testing-handler/SKILL.md` +**Skills** (`.agents/skills/{name}/SKILL.md`) โ€” step-by-step implementation patterns. Examples: +- Implement a CRUD handler: `read_file` on `.agents/skills/coldbox-handler-development/SKILL.md` +- Build a REST API: `read_file` on `.agents/skills/coldbox-rest-api-development/SKILL.md` +- Write tests: `read_file` on `.agents/skills/coldbox-testing-handler/SKILL.md` + +**Custom Skills** (`.agents/skills-custom/{name}/SKILL.md`) โ€” project-specific patterns. Load by name from `.agents/skills-custom/`. **To load any skill or guideline:** use `read_file` on the path shown above or in the inventories below. From 2b0099113e624eb29d5a9aca32235f849eaa6804 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 15:14:08 +0000 Subject: [PATCH 3/4] fix: address code review issues - path consistency, redundant checks, cleaner iteration Agent-Logs-Url: https://github.com/ColdBox/coldbox-cli/sessions/bd6c5d76-044f-4462-b8fb-459c2651fc07 Co-authored-by: lmajano <137111+lmajano@users.noreply.github.com> --- models/SkillManager.cfc | 33 ++++++++----------- .../ai/agents/agent-modern-instructions.md | 8 ++--- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/models/SkillManager.cfc b/models/SkillManager.cfc index eebce87..7e4d553 100644 --- a/models/SkillManager.cfc +++ b/models/SkillManager.cfc @@ -183,6 +183,11 @@ component singleton { "removed" : [] }; + // Ensure customSkills key exists (backwards compatibility with old manifests) + if ( !structKeyExists( arguments.manifest, "customSkills" ) ) { + arguments.manifest[ "customSkills" ] = [] + } + // ------------------------------------------------------------------ // 0. Install missing desired skills (core + module) not yet in manifest // ------------------------------------------------------------------ @@ -466,9 +471,6 @@ component singleton { // Remove custom skills whose files were deleted by the user missingCustomSkills.each( ( name ) => { variables.print.yellowLine( " ๐Ÿงน Removing deleted custom skill entry: #name#" ).toConsole() - if ( !structKeyExists( arguments.manifest, "customSkills" ) ) { - arguments.manifest[ "customSkills" ] = [] - } arguments.manifest.customSkills = arguments.manifest.customSkills.filter( ( s ) => s.name != name ) changes.removed.append( name ) } ) @@ -476,9 +478,6 @@ component singleton { // ------------------------------------------------------------------ // 5. Sync custom skills from .agents/skills-custom/ that aren't in manifest yet // ------------------------------------------------------------------ - if ( !structKeyExists( arguments.manifest, "customSkills" ) ) { - arguments.manifest[ "customSkills" ] = [] - } var customSkillsDir = getCustomSkillsDirectory( arguments.directory ) if ( directoryExists( customSkillsDir ) ) { directoryList( customSkillsDir, false, "name" ).each( ( dirName ) => { @@ -828,7 +827,7 @@ component singleton { /** * Create a skill override (custom copy) in the .agents/skills-custom/{name}/ directory. - * Sets type=custom, empty owner/repo so refresh skips it. + * Records in manifest.customSkills (no owner/repo, so refresh skips it). * * @directory The project directory * @name The name of the skill to override @@ -881,29 +880,23 @@ component singleton { manifest[ "customSkills" ] = [] } - // Find existing customSkills entry for this skill - var existingIndex = 0 - for ( var i = 1; i <= manifest.customSkills.len(); i++ ) { - if ( manifest.customSkills[ i ].name == arguments.name ) { - existingIndex = i; - break - } - } - // Also remove from manifest.skills if it was there (migrating from old location) manifest.skills = manifest.skills.filter( ( s ) => s.name != arguments.name ) - var existingEntry = existingIndex ? manifest.customSkills[ existingIndex ] : {} + // Find existing customSkills entry for this skill (for preserving description) + var existing = manifest.customSkills.filter( ( s ) => s.name == arguments.name ) + var existingEntry = existing.len() ? existing[ 1 ] : {} var skillEntry = { "name" : arguments.name, "description" : existingEntry.description ?: "", "syncedAt" : dateTimeFormat( now(), "iso" ) } - if ( existingIndex ) { - manifest.customSkills[ existingIndex ] = skillEntry - } else { + // Upsert into customSkills + if ( existingEntry.isEmpty() ) { manifest.customSkills.append( skillEntry ) + } else { + manifest.customSkills = manifest.customSkills.map( ( s ) => s.name == arguments.name ? skillEntry : s ) } variables.aiService.saveManifest( arguments.directory, manifest ) diff --git a/templates/ai/agents/agent-modern-instructions.md b/templates/ai/agents/agent-modern-instructions.md index d5108e5..78d103a 100644 --- a/templates/ai/agents/agent-modern-instructions.md +++ b/templates/ai/agents/agent-modern-instructions.md @@ -205,11 +205,11 @@ The `.agents/manifest.json` file contains the complete AI integration configurat Guidelines and skills are stored locally in `.agents/` and loaded via `read_file` when needed: -**Core Guidelines** (`.ai/guidelines/core/`) โ€” framework fundamentals: -- `read_file` on `.ai/guidelines/core/coldbox.md` โ€” ColdBox conventions, handler/routing/DI reference -- `read_file` on `.ai/guidelines/core/boxlang.md` โ€” BoxLang syntax, classes, lambdas (or `cfml.md` for CFML) +**Core Guidelines** (`.agents/guidelines/core/`) โ€” framework fundamentals: +- `read_file` on `.agents/guidelines/core/coldbox.md` โ€” ColdBox conventions, handler/routing/DI reference +- `read_file` on `.agents/guidelines/core/boxlang.md` โ€” BoxLang syntax, classes, lambdas (or `cfml.md` for CFML) -**Module/Custom Guidelines** โ€” load by name on request from `.ai/guidelines/modules/` or `.ai/guidelines/custom/`. +**Module/Custom Guidelines** โ€” load by name on request from `.agents/guidelines/modules/` or `.agents/guidelines/custom/`. **Skills** (`.agents/skills/{name}/SKILL.md`) โ€” step-by-step implementation patterns. Examples: - Implement a CRUD handler: `read_file` on `.agents/skills/coldbox-handler-development/SKILL.md` From d47f3eece864a95fdcfac40b1fdfceeb9fac1018 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 15:17:17 +0000 Subject: [PATCH 4/4] fix: address additional code review issues - null checks, performance, string interpolation Agent-Logs-Url: https://github.com/ColdBox/coldbox-cli/sessions/bd6c5d76-044f-4462-b8fb-459c2651fc07 Co-authored-by: lmajano <137111+lmajano@users.noreply.github.com> --- commands/coldbox/ai/skills/list.cfc | 2 +- models/SkillManager.cfc | 57 +++++++++++++++++++---------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/commands/coldbox/ai/skills/list.cfc b/commands/coldbox/ai/skills/list.cfc index f5fa40f..2dd0cfc 100644 --- a/commands/coldbox/ai/skills/list.cfc +++ b/commands/coldbox/ai/skills/list.cfc @@ -109,7 +109,7 @@ component extends="coldbox-cli.models.BaseAICommand" { // Summary print.line() - printInfo( "Total: #info.skills.len() + info.customSkills.len()# skill(s) installed" ) + printInfo( "Total: #(info.skills.len() + info.customSkills.len())# skill(s) installed" ) print.line() if ( !outdated ) { diff --git a/models/SkillManager.cfc b/models/SkillManager.cfc index 7e4d553..d10fb1a 100644 --- a/models/SkillManager.cfc +++ b/models/SkillManager.cfc @@ -184,9 +184,7 @@ component singleton { }; // Ensure customSkills key exists (backwards compatibility with old manifests) - if ( !structKeyExists( arguments.manifest, "customSkills" ) ) { - arguments.manifest[ "customSkills" ] = [] - } + ensureCustomSkillsSection( arguments.manifest ) // ------------------------------------------------------------------ // 0. Install missing desired skills (core + module) not yet in manifest @@ -409,6 +407,9 @@ component singleton { // Check for deleted custom skills (in customSkills manifest section) if ( structKeyExists( arguments.manifest, "customSkills" ) ) { for ( var customSkill in arguments.manifest.customSkills ) { + if ( !isStruct( customSkill ) || !structKeyExists( customSkill, "name" ) ) { + continue; + } var customSkillFile = getCustomSkillFilePath( arguments.directory, customSkill.name ) if ( isNull( customSkillFile ) ) { missingCustomSkills.append( customSkill.name ) @@ -486,7 +487,7 @@ component singleton { return; } - var alreadyInManifest = arguments.manifest.customSkills.filter( ( s ) => s.name == dirName ).len() > 0 + var alreadyInManifest = !arguments.manifest.customSkills.filter( ( s ) => s.name == dirName ).isEmpty() if ( alreadyInManifest ) { return; } @@ -755,9 +756,7 @@ component singleton { fileWrite( skillFile, template ) var manifest = variables.aiService.loadManifest( arguments.directory ); - if ( !structKeyExists( manifest, "customSkills" ) ) { - manifest[ "customSkills" ] = [] - } + ensureCustomSkillsSection( manifest ) manifest.customSkills.append( { "name" : arguments.name, "description" : "", @@ -876,27 +875,33 @@ component singleton { fileWrite( targetFile, content ) // Ensure customSkills section exists - if ( !structKeyExists( manifest, "customSkills" ) ) { - manifest[ "customSkills" ] = [] - } + ensureCustomSkillsSection( manifest ) // Also remove from manifest.skills if it was there (migrating from old location) - manifest.skills = manifest.skills.filter( ( s ) => s.name != arguments.name ) + var skillName = arguments.name + manifest.skills = manifest.skills.filter( ( s ) => s.name != skillName ) - // Find existing customSkills entry for this skill (for preserving description) - var existing = manifest.customSkills.filter( ( s ) => s.name == arguments.name ) - var existingEntry = existing.len() ? existing[ 1 ] : {} - var skillEntry = { - "name" : arguments.name, - "description" : existingEntry.description ?: "", + // Find existing customSkills entry index for this skill (for preserving description and upsert) + var existingIndex = 0 + for ( var i = 1; i <= manifest.customSkills.len(); i++ ) { + if ( manifest.customSkills[ i ].name == skillName ) { + existingIndex = i; + break + } + } + + var existingDescription = existingIndex ? ( manifest.customSkills[ existingIndex ].description ?: "" ) : "" + var skillEntry = { + "name" : skillName, + "description" : existingDescription, "syncedAt" : dateTimeFormat( now(), "iso" ) } // Upsert into customSkills - if ( existingEntry.isEmpty() ) { - manifest.customSkills.append( skillEntry ) + if ( existingIndex ) { + manifest.customSkills[ existingIndex ] = skillEntry } else { - manifest.customSkills = manifest.customSkills.map( ( s ) => s.name == arguments.name ? skillEntry : s ) + manifest.customSkills.append( skillEntry ) } variables.aiService.saveManifest( arguments.directory, manifest ) @@ -1371,6 +1376,18 @@ component singleton { return variables.aiService.getAIInstallDirectory( arguments.directory ) & "/skills-custom" } + /** + * Ensure manifest has a customSkills array (backwards compatibility). + * Mutates manifest in place. + * + * @manifest The manifest struct to ensure has a customSkills key + */ + private function ensureCustomSkillsSection( required struct manifest ){ + if ( !structKeyExists( arguments.manifest, "customSkills" ) ) { + arguments.manifest[ "customSkills" ] = [] + } + } + /** * Delete a skill directory under .ai/skills/ if it exists. *