@@ -116,6 +116,43 @@ function canonicalSkillPath(value: string): string {
116116 return value ;
117117}
118118
119+ /**
120+ * Return the durable identity of a skill inside Codex's versioned plugin cache.
121+ * The concrete version directory is installation state, while marketplace,
122+ * plugin, and skill-relative path remain stable across plugin upgrades.
123+ */
124+ function versionNeutralPluginSkillPath ( value : string ) : string | undefined {
125+ const match = value . match (
126+ / ^ ( .* [ \\ / ] p l u g i n s [ \\ / ] c a c h e [ \\ / ] [ ^ \\ / ] + [ \\ / ] [ ^ \\ / ] + ) [ \\ / ] [ ^ \\ / ] + ( [ \\ / ] s k i l l s [ \\ / ] .+ [ \\ / ] S K I L L \. m d ) $ / ,
127+ ) ;
128+ return match ? `${ match [ 1 ] } ${ match [ 2 ] } ` : undefined ;
129+ }
130+
131+ function rebindVersionedPluginSkills (
132+ discovered : readonly AvailableSkill [ ] ,
133+ selection : SkillSelection ,
134+ ) : SkillSelection {
135+ const discoveredPaths = new Set ( discovered . map ( ( skill ) => canonicalSkillPath ( skill . path ) ) ) ;
136+ const discoveredByDurablePath = new Map < string , AvailableSkill [ ] > ( ) ;
137+ for ( const skill of discovered ) {
138+ const durablePath = versionNeutralPluginSkillPath ( canonicalSkillPath ( skill . path ) ) ;
139+ if ( durablePath === undefined ) continue ;
140+ const matches = discoveredByDurablePath . get ( durablePath ) ?? [ ] ;
141+ matches . push ( skill ) ;
142+ discoveredByDurablePath . set ( durablePath , matches ) ;
143+ }
144+
145+ return createSkillSelection (
146+ selection . map ( ( entry ) => {
147+ if ( discoveredPaths . has ( entry . path ) ) return entry ;
148+ const durablePath = versionNeutralPluginSkillPath ( entry . path ) ;
149+ if ( durablePath === undefined ) return entry ;
150+ const matches = discoveredByDurablePath . get ( durablePath ) ?? [ ] ;
151+ return matches . length === 1 ? { ...entry , path : matches [ 0 ] . path } : entry ;
152+ } ) ,
153+ ) ;
154+ }
155+
119156/**
120157 * Validate a complete selection and return its canonical deterministic order.
121158 * This is lexical only: resolving symlinks is I/O and belongs to a platform
@@ -201,11 +238,12 @@ export function compileSkillOverride(input: {
201238 const effective = selectEffectiveSkillSelection ( input ) ;
202239 if ( effective . selection === undefined )
203240 return { source : 'native' , skillsConfig : undefined , warnings : [ ] } ;
241+ const reboundSelection = rebindVersionedPluginSkills ( input . discovered , effective . selection ) ;
204242 const discoveredPaths = new Set ( input . discovered . map ( ( skill ) => canonicalSkillPath ( skill . path ) ) ) ;
205- const warnings = effective . selection
243+ const warnings = reboundSelection
206244 . filter ( ( entry ) => ! discoveredPaths . has ( entry . path ) )
207245 . map ( ( entry ) => `Saved skill path is no longer discovered: ${ entry . path } ` ) ;
208- const configured = applySkillSelectionSnapshot ( input . discovered , effective . selection ) ;
246+ const configured = applySkillSelectionSnapshot ( input . discovered , reboundSelection ) ;
209247 return {
210248 source : effective . source ,
211249 skillsConfig : configured
0 commit comments