@@ -37,6 +37,15 @@ function seedStore(home: string, skills: string[]): void {
3737 }
3838}
3939
40+ /** Seed skill bundles in the universal ~/.agents/skills store. */
41+ function seedUniversal ( home : string , skills : string [ ] ) : void {
42+ for ( const name of skills ) {
43+ const dir = join ( home , ".agents" , "skills" , name ) ;
44+ mkdirSync ( dir , { recursive : true } ) ;
45+ writeFileSync ( join ( dir , "SKILL.md" ) , `# ${ name } \n` , "utf8" ) ;
46+ }
47+ }
48+
4049/** Pretend an agent is installed by creating its marker dir. */
4150function installMarker ( home : string , marker : string ) : void {
4251 mkdirSync ( join ( home , ...marker . split ( "/" ) ) , { recursive : true } ) ;
@@ -243,6 +252,108 @@ describe("mirrorGlobalSkills", () => {
243252 expect ( realpathSync ( link ) ) . toBe ( realpathSync ( join ( home , ".claude" , "skills" , "hyperframes" ) ) ) ;
244253 } ) ;
245254
255+ it ( "does not mirror Codex when the universal store is available" , ( ) => {
256+ const home = makeHome ( ) ;
257+ seedStore ( home , [ "hyperframes" ] ) ;
258+ installMarker ( home , ".codex" ) ;
259+ mkdirSync ( join ( home , ".agents" , "skills" , "hyperframes" ) , { recursive : true } ) ;
260+ writeFileSync ( join ( home , ".agents" , "skills" , "hyperframes" , "SKILL.md" ) , "# universal\n" ) ;
261+ mkdirSync ( join ( home , ".codex" , "skills" , ".system" ) , { recursive : true } ) ;
262+
263+ const { mirrored } = mirrorGlobalSkills ( {
264+ skills : [ "hyperframes" ] ,
265+ home,
266+ platform : "linux" ,
267+ env : ENV ,
268+ } ) ;
269+
270+ expect ( mirrored . map ( ( entry ) => entry . agent ) ) . not . toContain ( "codex" ) ;
271+ expect ( existsSync ( join ( home , ".codex" , "skills" , ".system" ) ) ) . toBe ( true ) ;
272+ expect ( existsSync ( join ( home , ".codex" , "skills" , "hyperframes" ) ) ) . toBe ( false ) ;
273+ } ) ;
274+
275+ it ( "does not mirror Codex under a custom CODEX_HOME" , ( ) => {
276+ const home = makeHome ( ) ;
277+ const codexHome = makeHome ( ) ;
278+ seedStore ( home , [ "hyperframes" ] ) ;
279+ installMarker ( home , ".codex" ) ;
280+ mkdirSync ( join ( home , ".agents" , "skills" , "hyperframes" ) , { recursive : true } ) ;
281+ writeFileSync ( join ( home , ".agents" , "skills" , "hyperframes" , "SKILL.md" ) , "# universal\n" ) ;
282+ const existing = join ( codexHome , "skills" , "hyperframes" ) ;
283+ mkdirSync ( existing , { recursive : true } ) ;
284+ writeFileSync ( join ( existing , "SKILL.md" ) , "# locally managed\n" , "utf8" ) ;
285+
286+ const { mirrored } = mirrorGlobalSkills ( {
287+ skills : [ "hyperframes" ] ,
288+ home,
289+ platform : "linux" ,
290+ env : { CODEX_HOME : codexHome } ,
291+ } ) ;
292+
293+ expect ( mirrored . map ( ( entry ) => entry . agent ) ) . not . toContain ( "codex" ) ;
294+ expect ( readFileSync ( join ( existing , "SKILL.md" ) , "utf8" ) ) . toBe ( "# locally managed\n" ) ;
295+ } ) ;
296+
297+ it ( "removes Codex links an earlier mirror created and keeps the user's own entries" , ( ) => {
298+ const home = makeHome ( ) ;
299+ seedStore ( home , [ "hyperframes" , "media-use" ] ) ;
300+ const codexSkills = join ( home , ".codex" , "skills" ) ;
301+ mkdirSync ( join ( codexSkills , ".system" ) , { recursive : true } ) ;
302+ mkdirSync ( join ( codexSkills , "my-skill" ) , { recursive : true } ) ;
303+ // What main's mirror wrote: a relative link back into the Claude store.
304+ symlinkSync (
305+ join ( ".." , ".." , ".claude" , "skills" , "hyperframes" ) ,
306+ join ( codexSkills , "hyperframes" ) ,
307+ ) ;
308+ // Same skill name, but the user's own link to somewhere else.
309+ const userCopy = join ( home , "my-media-use" ) ;
310+ mkdirSync ( userCopy ) ;
311+ symlinkSync ( userCopy , join ( codexSkills , "media-use" ) ) ;
312+ seedUniversal ( home , [ "hyperframes" , "media-use" ] ) ;
313+
314+ mirrorGlobalSkills ( { skills : [ "hyperframes" , "media-use" ] , home, platform : "linux" , env : ENV } ) ;
315+
316+ expect ( existsSync ( join ( codexSkills , "hyperframes" ) ) ) . toBe ( false ) ;
317+ expect ( realpathSync ( join ( codexSkills , "media-use" ) ) ) . toBe ( realpathSync ( userCopy ) ) ;
318+ expect ( existsSync ( join ( codexSkills , ".system" ) ) ) . toBe ( true ) ;
319+ expect ( existsSync ( join ( codexSkills , "my-skill" ) ) ) . toBe ( true ) ;
320+ expect ( existsSync ( join ( home , ".claude" , "skills" , "hyperframes" , "SKILL.md" ) ) ) . toBe ( true ) ;
321+ } ) ;
322+
323+ it ( "keeps a Codex link when the universal store lacks that skill" , ( ) => {
324+ const home = makeHome ( ) ;
325+ seedStore ( home , [ "hyperframes" ] ) ;
326+ const codexLink = join ( home , ".codex" , "skills" , "hyperframes" ) ;
327+ mkdirSync ( join ( home , ".codex" , "skills" ) , { recursive : true } ) ;
328+ symlinkSync ( join ( ".." , ".." , ".claude" , "skills" , "hyperframes" ) , codexLink ) ;
329+
330+ mirrorGlobalSkills ( { skills : [ "hyperframes" ] , home, platform : "linux" , env : ENV } ) ;
331+
332+ expect ( lstatSync ( codexLink ) . isSymbolicLink ( ) ) . toBe ( true ) ;
333+ } ) ;
334+
335+ it ( "never unlinks through a Codex dir that aliases the Claude store" , ( ) => {
336+ const home = makeHome ( ) ;
337+ const checkout = join ( home , "checkout" , "hyperframes" ) ;
338+ mkdirSync ( checkout , { recursive : true } ) ;
339+ writeFileSync ( join ( checkout , "SKILL.md" ) , "# checkout\n" , "utf8" ) ;
340+ mkdirSync ( join ( home , ".claude" , "skills" ) , { recursive : true } ) ;
341+ symlinkSync ( checkout , join ( home , ".claude" , "skills" , "hyperframes" ) ) ;
342+ mkdirSync ( join ( home , ".codex" ) , { recursive : true } ) ;
343+ symlinkSync ( join ( ".." , ".claude" , "skills" ) , join ( home , ".codex" , "skills" ) ) ;
344+ seedUniversal ( home , [ "hyperframes" ] ) ;
345+
346+ const { skipped } = mirrorGlobalSkills ( {
347+ skills : [ "hyperframes" ] ,
348+ home,
349+ platform : "linux" ,
350+ env : ENV ,
351+ } ) ;
352+
353+ expect ( lstatSync ( join ( home , ".claude" , "skills" , "hyperframes" ) ) . isSymbolicLink ( ) ) . toBe ( true ) ;
354+ expect ( skipped ) . toContainEqual ( expect . objectContaining ( { agent : "codex" } ) ) ;
355+ } ) ;
356+
246357 // Pi natively discovers BOTH ~/.pi/agent/skills and the universal
247358 // ~/.agents/skills (pi's packages/coding-agent/docs/skills.md#locations).
248359 // A mirrored per-agent copy collides with the universal one and Pi skips
0 commit comments