@@ -100,6 +100,52 @@ func TestMatchSkillConventions(t *testing.T) {
100100 path : ".hidden/SKILL.md" ,
101101 wantNil : true ,
102102 },
103+ {
104+ name : "nested skills directory" ,
105+ path : "terraform/code-generation/skills/terraform-style-guide/SKILL.md" ,
106+ wantName : "terraform-style-guide" ,
107+ wantConvention : "skills" ,
108+ },
109+ {
110+ name : "deeply nested skills directory" ,
111+ path : "a/b/c/skills/my-skill/SKILL.md" ,
112+ wantName : "my-skill" ,
113+ wantConvention : "skills" ,
114+ },
115+ {
116+ name : "nested namespaced skills directory" ,
117+ path : "terraform/code-generation/skills/hashicorp/terraform-style-guide/SKILL.md" ,
118+ wantName : "terraform-style-guide" ,
119+ wantNamespace : "hashicorp" ,
120+ wantConvention : "skills-namespaced" ,
121+ },
122+ {
123+ name : "single prefix before skills directory" ,
124+ path : "packer/skills/packer-builder/SKILL.md" ,
125+ wantName : "packer-builder" ,
126+ wantConvention : "skills" ,
127+ },
128+ {
129+ name : "root-level skills still has priority" ,
130+ path : "skills/code-review/SKILL.md" ,
131+ wantName : "code-review" ,
132+ wantConvention : "skills" ,
133+ },
134+ {
135+ name : "nested skills dir itself is not a skill" ,
136+ path : "terraform/skills/SKILL.md" ,
137+ wantNil : true ,
138+ },
139+ {
140+ name : "nested skills under hidden dir excluded" ,
141+ path : ".claude/skills/code-review/SKILL.md" ,
142+ wantNil : true ,
143+ },
144+ {
145+ name : "nested plugins skills not matched as plain skills" ,
146+ path : "vendor/plugins/hubot/skills/pr-summary/SKILL.md" ,
147+ wantNil : true ,
148+ },
103149 }
104150 for _ , tt := range tests {
105151 t .Run (tt .name , func (t * testing.T ) {
@@ -865,6 +911,41 @@ func TestDiscoverSkills(t *testing.T) {
865911 },
866912 wantSkills : []string {"code-review" },
867913 },
914+ {
915+ name : "discovers skills in nested skills directory" ,
916+ stubs : func (reg * httpmock.Registry ) {
917+ reg .Register (
918+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/git/trees/abc123" ),
919+ httpmock .JSONResponse (map [string ]interface {}{
920+ "sha" : "abc123" , "truncated" : false ,
921+ "tree" : []map [string ]interface {}{
922+ {"path" : "terraform/code-generation/skills/terraform-style-guide" , "type" : "tree" , "sha" : "tree-sha-1" },
923+ {"path" : "terraform/code-generation/skills/terraform-style-guide/SKILL.md" , "type" : "blob" , "sha" : "blob-1" },
924+ {"path" : "terraform/code-generation/skills/terraform-test" , "type" : "tree" , "sha" : "tree-sha-2" },
925+ {"path" : "terraform/code-generation/skills/terraform-test/SKILL.md" , "type" : "blob" , "sha" : "blob-2" },
926+ {"path" : "README.md" , "type" : "blob" , "sha" : "readme" },
927+ },
928+ }))
929+ },
930+ wantSkills : []string {"terraform-style-guide" , "terraform-test" },
931+ },
932+ {
933+ name : "discovers mixed root-level and nested skills" ,
934+ stubs : func (reg * httpmock.Registry ) {
935+ reg .Register (
936+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/git/trees/abc123" ),
937+ httpmock .JSONResponse (map [string ]interface {}{
938+ "sha" : "abc123" , "truncated" : false ,
939+ "tree" : []map [string ]interface {}{
940+ {"path" : "skills/code-review" , "type" : "tree" , "sha" : "tree-sha-1" },
941+ {"path" : "skills/code-review/SKILL.md" , "type" : "blob" , "sha" : "blob-1" },
942+ {"path" : "terraform/skills/tf-lint" , "type" : "tree" , "sha" : "tree-sha-2" },
943+ {"path" : "terraform/skills/tf-lint/SKILL.md" , "type" : "blob" , "sha" : "blob-2" },
944+ },
945+ }))
946+ },
947+ wantSkills : []string {"code-review" , "tf-lint" },
948+ },
868949 }
869950 for _ , tt := range tests {
870951 t .Run (tt .name , func (t * testing.T ) {
@@ -967,12 +1048,13 @@ func TestDiscoverSkillsWithOptions(t *testing.T) {
9671048
9681049func TestDiscoverSkillByPath (t * testing.T ) {
9691050 tests := []struct {
970- name string
971- skillPath string
972- stubs func (* httpmock.Registry )
973- wantName string
974- wantNS string
975- wantErr string
1051+ name string
1052+ skillPath string
1053+ stubs func (* httpmock.Registry )
1054+ wantName string
1055+ wantNS string
1056+ wantConvention string
1057+ wantErr string
9761058 }{
9771059 {
9781060 name : "discovers skill by path" ,
@@ -1112,6 +1194,84 @@ func TestDiscoverSkillByPath(t *testing.T) {
11121194 },
11131195 wantErr : "no SKILL.md found" ,
11141196 },
1197+ {
1198+ name : "deeply nested path discovers skill" ,
1199+ skillPath : "terraform/code-generation/skills/terraform-style-guide" ,
1200+ stubs : func (reg * httpmock.Registry ) {
1201+ reg .Register (
1202+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/contents/terraform%2Fcode-generation%2Fskills" ),
1203+ httpmock .JSONResponse ([]map [string ]interface {}{
1204+ {"name" : "terraform-style-guide" , "path" : "terraform/code-generation/skills/terraform-style-guide" , "sha" : "tree-sha" , "type" : "dir" },
1205+ }))
1206+ reg .Register (
1207+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/git/trees/tree-sha" ),
1208+ httpmock .JSONResponse (map [string ]interface {}{
1209+ "sha" : "tree-sha" , "truncated" : false ,
1210+ "tree" : []map [string ]interface {}{
1211+ {"path" : "SKILL.md" , "type" : "blob" , "sha" : "blob-sha" },
1212+ },
1213+ }))
1214+ reg .Register (
1215+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/git/blobs/blob-sha" ),
1216+ httpmock .JSONResponse (map [string ]interface {}{
1217+ "sha" : "blob-sha" , "encoding" : "base64" , "content" : "IyBTa2lsbA==" ,
1218+ }))
1219+ },
1220+ wantName : "terraform-style-guide" ,
1221+ },
1222+ {
1223+ name : "deeply nested namespaced path sets namespace" ,
1224+ skillPath : "terraform/code-generation/skills/hashicorp/terraform-style-guide" ,
1225+ stubs : func (reg * httpmock.Registry ) {
1226+ reg .Register (
1227+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/contents/terraform%2Fcode-generation%2Fskills%2Fhashicorp" ),
1228+ httpmock .JSONResponse ([]map [string ]interface {}{
1229+ {"name" : "terraform-style-guide" , "path" : "terraform/code-generation/skills/hashicorp/terraform-style-guide" , "sha" : "tree-sha" , "type" : "dir" },
1230+ }))
1231+ reg .Register (
1232+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/git/trees/tree-sha" ),
1233+ httpmock .JSONResponse (map [string ]interface {}{
1234+ "sha" : "tree-sha" , "truncated" : false ,
1235+ "tree" : []map [string ]interface {}{
1236+ {"path" : "SKILL.md" , "type" : "blob" , "sha" : "blob-sha" },
1237+ },
1238+ }))
1239+ reg .Register (
1240+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/git/blobs/blob-sha" ),
1241+ httpmock .JSONResponse (map [string ]interface {}{
1242+ "sha" : "blob-sha" , "encoding" : "base64" , "content" : "IyBTa2lsbA==" ,
1243+ }))
1244+ },
1245+ wantName : "terraform-style-guide" ,
1246+ wantNS : "hashicorp" ,
1247+ },
1248+ {
1249+ name : "plugins path sets namespace and convention" ,
1250+ skillPath : "plugins/hubot/skills/pr-summary" ,
1251+ stubs : func (reg * httpmock.Registry ) {
1252+ reg .Register (
1253+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/contents/plugins%2Fhubot%2Fskills" ),
1254+ httpmock .JSONResponse ([]map [string ]interface {}{
1255+ {"name" : "pr-summary" , "path" : "plugins/hubot/skills/pr-summary" , "sha" : "tree-sha" , "type" : "dir" },
1256+ }))
1257+ reg .Register (
1258+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/git/trees/tree-sha" ),
1259+ httpmock .JSONResponse (map [string ]interface {}{
1260+ "sha" : "tree-sha" , "truncated" : false ,
1261+ "tree" : []map [string ]interface {}{
1262+ {"path" : "SKILL.md" , "type" : "blob" , "sha" : "blob-sha" },
1263+ },
1264+ }))
1265+ reg .Register (
1266+ httpmock .REST ("GET" , "repos/monalisa/octocat-skills/git/blobs/blob-sha" ),
1267+ httpmock .JSONResponse (map [string ]interface {}{
1268+ "sha" : "blob-sha" , "encoding" : "base64" , "content" : "IyBTa2lsbA==" ,
1269+ }))
1270+ },
1271+ wantName : "pr-summary" ,
1272+ wantNS : "hubot" ,
1273+ wantConvention : "plugins" ,
1274+ },
11151275 }
11161276 for _ , tt := range tests {
11171277 t .Run (tt .name , func (t * testing.T ) {
@@ -1131,6 +1291,9 @@ func TestDiscoverSkillByPath(t *testing.T) {
11311291 require .NoError (t , err )
11321292 assert .Equal (t , tt .wantName , skill .Name )
11331293 assert .Equal (t , tt .wantNS , skill .Namespace )
1294+ if tt .wantConvention != "" {
1295+ assert .Equal (t , tt .wantConvention , skill .Convention )
1296+ }
11341297 })
11351298 }
11361299}
@@ -1184,6 +1347,19 @@ func TestDiscoverLocalSkills(t *testing.T) {
11841347 setup : func (t * testing.T , dir string ) {},
11851348 wantErr : "could not access" ,
11861349 },
1350+ {
1351+ name : "discovers skills in nested skills/ directory" ,
1352+ createDir : true ,
1353+ setup : func (t * testing.T , dir string ) {
1354+ t .Helper ()
1355+ for _ , name := range []string {"terraform-style-guide" , "terraform-test" } {
1356+ skillDir := filepath .Join (dir , "terraform" , "code-generation" , "skills" , name )
1357+ require .NoError (t , os .MkdirAll (skillDir , 0o755 ))
1358+ require .NoError (t , os .WriteFile (filepath .Join (skillDir , "SKILL.md" ), []byte ("# " + name ), 0o644 ))
1359+ }
1360+ },
1361+ wantSkills : []string {"terraform-style-guide" , "terraform-test" },
1362+ },
11871363 }
11881364 for _ , tt := range tests {
11891365 t .Run (tt .name , func (t * testing.T ) {
@@ -1278,6 +1454,7 @@ func TestMatchesSkillPath(t *testing.T) {
12781454 {name : "plugins convention" , path : "plugins/hubot/skills/pr-summary/SKILL.md" , wantName : "pr-summary" },
12791455 {name : "non-skill file" , path : "README.md" , wantName : "" },
12801456 {name : "non-SKILL.md in skill dir" , path : "skills/code-review/prompt.txt" , wantName : "" },
1457+ {name : "nested skills convention" , path : "terraform/code-generation/skills/terraform-style-guide/SKILL.md" , wantName : "terraform-style-guide" },
12811458 }
12821459 for _ , tt := range tests {
12831460 t .Run (tt .name , func (t * testing.T ) {
@@ -1300,6 +1477,8 @@ func TestMatchSkillPath(t *testing.T) {
13001477 {name : "same name different namespace 1" , path : "skills/kynan/commit/SKILL.md" , wantName : "commit" , wantNamespace : "kynan" },
13011478 {name : "same name different namespace 2" , path : "skills/will/commit/SKILL.md" , wantName : "commit" , wantNamespace : "will" },
13021479 {name : "root convention" , path : "my-skill/SKILL.md" , wantName : "my-skill" , wantNamespace : "" },
1480+ {name : "nested skills convention" , path : "terraform/code-generation/skills/terraform-style-guide/SKILL.md" , wantName : "terraform-style-guide" , wantNamespace : "" },
1481+ {name : "nested namespaced convention" , path : "terraform/code-generation/skills/hashicorp/terraform-style-guide/SKILL.md" , wantName : "terraform-style-guide" , wantNamespace : "hashicorp" },
13031482 }
13041483 for _ , tt := range tests {
13051484 t .Run (tt .name , func (t * testing.T ) {
0 commit comments