@@ -124,7 +124,11 @@ func InstallSkillsForAgents(ctx context.Context, src ManifestSource, targetAgent
124124 if err != nil {
125125 return fmt .Errorf ("failed to determine working directory: %w" , err )
126126 }
127+ incompatible := incompatibleAgentNames (targetAgents )
127128 targetAgents = filterProjectAgents (ctx , targetAgents )
129+ if len (targetAgents ) == 0 {
130+ return fmt .Errorf ("no agents support project-scoped skills. The following detected agents are global-only: %s" , strings .Join (incompatible , ", " ))
131+ }
128132 }
129133
130134 // Load existing state for idempotency checks.
@@ -144,6 +148,13 @@ func InstallSkillsForAgents(ctx context.Context, src ManifestSource, targetAgent
144148 return err
145149 }
146150
151+ params := installParams {
152+ baseDir : baseDir ,
153+ scope : scope ,
154+ cwd : cwd ,
155+ ref : latestTag ,
156+ }
157+
147158 // Install each skill in sorted order for determinism.
148159 skillNames := make ([]string , 0 , len (targetSkills ))
149160 for name := range targetSkills {
@@ -162,7 +173,7 @@ func InstallSkillsForAgents(ctx context.Context, src ManifestSource, targetAgent
162173 }
163174 }
164175
165- if err := installSkillForAgents (ctx , latestTag , name , meta .Files , targetAgents , baseDir , scope , cwd ); err != nil {
176+ if err := installSkillForAgents (ctx , name , meta .Files , targetAgents , params ); err != nil {
166177 return err
167178 }
168179 }
@@ -230,6 +241,17 @@ func filterProjectAgents(ctx context.Context, targetAgents []*agents.Agent) []*a
230241 return compatible
231242}
232243
244+ // incompatibleAgentNames returns the display names of agents that do not support project scope.
245+ func incompatibleAgentNames (targetAgents []* agents.Agent ) []string {
246+ var names []string
247+ for _ , a := range targetAgents {
248+ if ! a .SupportsProjectScope {
249+ names = append (names , a .DisplayName )
250+ }
251+ }
252+ return names
253+ }
254+
233255// resolveSkills filters the manifest skills based on the install options,
234256// experimental flag, and CLI version constraints.
235257func resolveSkills (ctx context.Context , skills map [string ]SkillMeta , opts InstallOptions ) (map [string ]SkillMeta , error ) {
@@ -348,17 +370,25 @@ func hasSkillsOnDisk(dir string) bool {
348370 return false
349371}
350372
351- func installSkillForAgents (ctx context.Context , ref , skillName string , files []string , detectedAgents []* agents.Agent , baseDir , scope , cwd string ) error {
352- canonicalDir := filepath .Join (baseDir , skillName )
353- if err := installSkillToDir (ctx , ref , skillName , canonicalDir , files ); err != nil {
373+ // installParams bundles the parameters for installSkillForAgents to keep the signature manageable.
374+ type installParams struct {
375+ baseDir string
376+ scope string
377+ cwd string
378+ ref string
379+ }
380+
381+ func installSkillForAgents (ctx context.Context , skillName string , files []string , detectedAgents []* agents.Agent , params installParams ) error {
382+ canonicalDir := filepath .Join (params .baseDir , skillName )
383+ if err := installSkillToDir (ctx , params .ref , skillName , canonicalDir , files ); err != nil {
354384 return err
355385 }
356386
357387 // For project scope, always symlink. For global, symlink when multiple agents.
358- useSymlinks := scope == ScopeProject || len (detectedAgents ) > 1
388+ useSymlinks := params . scope == ScopeProject || len (detectedAgents ) > 1
359389
360390 for _ , agent := range detectedAgents {
361- agentSkillDir , err := agentSkillsDirForScope (ctx , agent , scope , cwd )
391+ agentSkillDir , err := agentSkillsDirForScope (ctx , agent , params . scope , params . cwd )
362392 if err != nil {
363393 log .Warnf (ctx , "Skipped %s: %v" , agent .DisplayName , err )
364394 continue
@@ -372,16 +402,24 @@ func installSkillForAgents(ctx context.Context, ref, skillName string, files []s
372402 }
373403
374404 if useSymlinks {
375- if err := createSymlink (canonicalDir , destDir ); err != nil {
405+ symlinkTarget := canonicalDir
406+ // For project scope, use relative symlinks so they work for teammates.
407+ if params .scope == ScopeProject {
408+ rel , relErr := filepath .Rel (filepath .Dir (destDir ), canonicalDir )
409+ if relErr == nil {
410+ symlinkTarget = rel
411+ }
412+ }
413+ if err := createSymlink (symlinkTarget , destDir ); err != nil {
376414 log .Debugf (ctx , "Symlink failed for %s, copying instead: %v" , agent .DisplayName , err )
377- if err := installSkillToDir (ctx , ref , skillName , destDir , files ); err != nil {
415+ if err := installSkillToDir (ctx , params . ref , skillName , destDir , files ); err != nil {
378416 log .Warnf (ctx , "Failed to install for %s: %v" , agent .DisplayName , err )
379417 continue
380418 }
381419 }
382420 log .Debugf (ctx , "Installed %q for %s (symlinked)" , skillName , agent .DisplayName )
383421 } else {
384- if err := installSkillToDir (ctx , ref , skillName , destDir , files ); err != nil {
422+ if err := installSkillToDir (ctx , params . ref , skillName , destDir , files ); err != nil {
385423 log .Warnf (ctx , "Failed to install for %s: %v" , agent .DisplayName , err )
386424 continue
387425 }
@@ -414,8 +452,14 @@ func backupThirdPartySkill(ctx context.Context, destDir, canonicalDir, skillName
414452 // If it's a symlink to our canonical dir, no backup needed.
415453 if fi .Mode ()& os .ModeSymlink != 0 {
416454 target , err := os .Readlink (destDir )
417- if err == nil && target == canonicalDir {
418- return nil
455+ if err == nil {
456+ absTarget := target
457+ if ! filepath .IsAbs (target ) {
458+ absTarget = filepath .Clean (filepath .Join (filepath .Dir (destDir ), target ))
459+ }
460+ if absTarget == canonicalDir {
461+ return nil
462+ }
419463 }
420464 }
421465
0 commit comments