diff --git a/pkg/artifact/kitfile.go b/pkg/artifact/kitfile.go index ad09c32fc..5e3541059 100644 --- a/pkg/artifact/kitfile.go +++ b/pkg/artifact/kitfile.go @@ -104,6 +104,7 @@ type ( } Prompt struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` Path string `json:"path,omitempty" yaml:"path,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` *LayerInfo `json:",inline" yaml:",inline"` diff --git a/pkg/lib/filesystem/unpack/filter.go b/pkg/lib/filesystem/unpack/filter.go index ffae8d9f0..adda29b7b 100644 --- a/pkg/lib/filesystem/unpack/filter.go +++ b/pkg/lib/filesystem/unpack/filter.go @@ -110,8 +110,7 @@ func shouldUnpackLayer(layer any, filters []FilterConf) bool { // Code does not have a ID/name field so we can only match on path return matchesFilters("code", l.Path, filters) case artifact.Prompt: - // Prompts do not have a ID/name field so we can only match on path - return matchesFilters("prompts", l.Path, filters) + return matchesFilters("prompts", l.Name, filters) || matchesFilters("prompts", l.Path, filters) default: return false } diff --git a/pkg/lib/filesystem/unpack/filter_test.go b/pkg/lib/filesystem/unpack/filter_test.go index bed310778..2b183f72b 100644 --- a/pkg/lib/filesystem/unpack/filter_test.go +++ b/pkg/lib/filesystem/unpack/filter_test.go @@ -19,6 +19,7 @@ package unpack import ( "testing" + "github.com/kitops-ml/kitops/pkg/artifact" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -111,6 +112,55 @@ func TestParseFilter_EdgeCases(t *testing.T) { } } +func TestShouldUnpackLayer_PromptByName(t *testing.T) { + tests := []struct { + name string + prompt artifact.Prompt + filter string + expect bool + }{ + { + name: "match by name", + prompt: artifact.Prompt{Name: "docx", Path: "skills/docx"}, + filter: "prompts:docx", + expect: true, + }, + { + name: "match by path fallback", + prompt: artifact.Prompt{Path: "skills/docx"}, + filter: "prompts:skills/docx", + expect: true, + }, + { + name: "no match", + prompt: artifact.Prompt{Name: "docx", Path: "skills/docx"}, + filter: "prompts:xlsx", + expect: false, + }, + { + name: "prompts type without specific filter matches all", + prompt: artifact.Prompt{Name: "docx", Path: "skills/docx"}, + filter: "prompts", + expect: true, + }, + { + name: "empty name matches by path", + prompt: artifact.Prompt{Path: "SKILL.md"}, + filter: "prompts:SKILL.md", + expect: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fc, err := ParseFilter(tt.filter) + require.NoError(t, err) + result := shouldUnpackLayer(tt.prompt, []FilterConf{*fc}) + assert.Equal(t, tt.expect, result) + }) + } +} + func TestFiltersFromUnpackConf(t *testing.T) { tests := []struct { name string diff --git a/pkg/lib/kitfile/generate/generate.go b/pkg/lib/kitfile/generate/generate.go index bfc9d2c18..db9165318 100644 --- a/pkg/lib/kitfile/generate/generate.go +++ b/pkg/lib/kitfile/generate/generate.go @@ -94,6 +94,25 @@ func GenerateKitfile(dir *DirectoryListing, packageOpt *artifact.Package) (*arti kitfile.Package = *packageOpt } + // SKILL.md at root: treat entire directory as a single skill + if found, _ := dirContainsSkillMD(*dir); found { + output.Logf(output.LogLevelTrace, "SKILL.md found; treating as a skill directory") + prompt, fm := buildPromptFromSkill(*dir) + kitfile.Prompts = append(kitfile.Prompts, prompt) + if fm != nil { + if kitfile.Package.Name == "" { + kitfile.Package.Name = fm.Name + } + if kitfile.Package.Description == "" { + kitfile.Package.Description = fm.Description + } + if kitfile.Package.License == "" { + kitfile.Package.License = fm.License + } + } + return kitfile, nil + } + // We can make sure all files are included by including a layer with path '.' // However, we only want to do this if it is necessary includeCatchallSection := false @@ -211,10 +230,19 @@ func GenerateKitfile(dir *DirectoryListing, packageOpt *artifact.Package) (*arti kitfile.Package.License = detectedLicenseType } + applySkillMetadataToPackage(kitfile, *dir) + return kitfile, nil } func addDirToKitfile(kitfile *artifact.KitFile, dir DirectoryListing) (modelFiles []FileListing, err error) { + if found, _ := dirContainsSkillMD(dir); found { + output.Logf(output.LogLevelTrace, "Directory %s contains SKILL.md; treating as skill", dir.Path) + prompt, _ := buildPromptFromSkill(dir) + kitfile.Prompts = append(kitfile.Prompts, prompt) + return nil, nil + } + switch dir.Name { case "docs": output.Logf(output.LogLevelTrace, "Directory %s interpreted as documentation", dir.Name) diff --git a/pkg/lib/kitfile/generate/skill.go b/pkg/lib/kitfile/generate/skill.go new file mode 100644 index 000000000..d00930bbe --- /dev/null +++ b/pkg/lib/kitfile/generate/skill.go @@ -0,0 +1,133 @@ +// Copyright 2026 The KitOps Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package generate + +import ( + "os" + "strings" + + "github.com/kitops-ml/kitops/pkg/artifact" + "github.com/kitops-ml/kitops/pkg/output" + + "go.yaml.in/yaml/v3" +) + +type SkillFrontmatter struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + License string `yaml:"license,omitempty"` +} + +func parseSkillFrontmatter(skillMDPath string) *SkillFrontmatter { + data, err := os.ReadFile(skillMDPath) + if err != nil { + output.Logf(output.LogLevelWarn, "Failed to read %s: %s", skillMDPath, err) + return nil + } + + content := strings.ReplaceAll(string(data), "\r\n", "\n") + lines := strings.Split(content, "\n") + if len(lines) == 0 || lines[0] != "---" { + return nil + } + + end := -1 + for i := 1; i < len(lines); i++ { + if lines[i] == "---" { + end = i + break + } + } + if end == -1 { + return nil + } + + frontmatterYAML := strings.Join(lines[1:end], "\n") + if strings.TrimSpace(frontmatterYAML) == "" { + return nil + } + + var fm SkillFrontmatter + if err := yaml.Unmarshal([]byte(frontmatterYAML), &fm); err != nil { + output.Logf(output.LogLevelWarn, "Malformed frontmatter in %s: %s", skillMDPath, err) + return nil + } + return &fm +} + +func dirContainsSkillMD(dir DirectoryListing) (bool, string) { + for _, file := range dir.Files { + if strings.EqualFold(file.Name, "skill.md") { + return true, file.Path + } + } + return false, "" +} + +func buildPromptFromSkill(dir DirectoryListing) (artifact.Prompt, *SkillFrontmatter) { + prompt := artifact.Prompt{ + Path: dir.Path, + } + + found, skillPath := dirContainsSkillMD(dir) + if !found { + return prompt, nil + } + + fm := parseSkillFrontmatter(skillPath) + if fm != nil { + prompt.Name = fm.Name + prompt.Description = fm.Description + } + return prompt, fm +} + +func applySkillMetadataToPackage(kitfile *artifact.KitFile, dir DirectoryListing) { + var skillFrontmatters []*SkillFrontmatter + for _, subDir := range dir.Subdirs { + if found, skillPath := dirContainsSkillMD(subDir); found { + if fm := parseSkillFrontmatter(skillPath); fm != nil { + skillFrontmatters = append(skillFrontmatters, fm) + } + } + } + + if len(skillFrontmatters) == 0 { + return + } + + // Only promote name/description when the Kitfile contains exclusively + // prompt layers. A mixed Kitfile (model + skill) should not inherit the + // skill's name as the package name. + hasOnlyPrompts := kitfile.Model == nil && len(kitfile.Code) == 0 && + len(kitfile.DataSets) == 0 && len(kitfile.Docs) == 0 + if hasOnlyPrompts && len(skillFrontmatters) == 1 { + fm := skillFrontmatters[0] + if kitfile.Package.Name == "" { + kitfile.Package.Name = fm.Name + } + if kitfile.Package.Description == "" { + kitfile.Package.Description = fm.Description + } + } + + first := skillFrontmatters[0] + if kitfile.Package.License == "" && first.License != "" { + kitfile.Package.License = first.License + output.Logf(output.LogLevelTrace, "Using license %q from skill %q", first.License, first.Name) + } +} diff --git a/pkg/lib/kitfile/generate/skill_test.go b/pkg/lib/kitfile/generate/skill_test.go new file mode 100644 index 000000000..2b18d77a2 --- /dev/null +++ b/pkg/lib/kitfile/generate/skill_test.go @@ -0,0 +1,447 @@ +// Copyright 2026 The KitOps Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package generate + +import ( + "os" + "path/filepath" + "testing" + + "github.com/kitops-ml/kitops/pkg/artifact" +) + +func TestParseSkillFrontmatter(t *testing.T) { + tests := []struct { + name string + content string + expectNil bool + expectName string + expectDesc string + expectLic string + }{ + { + name: "valid frontmatter", + content: "---\nname: pdf-tools\ndescription: PDF processing\nlicense: Apache-2.0\n---\n# Skill body", + expectName: "pdf-tools", + expectDesc: "PDF processing", + expectLic: "Apache-2.0", + }, + { + name: "partial frontmatter - name only", + content: "---\nname: my-skill\n---\n# Body", + expectName: "my-skill", + }, + { + name: "no frontmatter", + content: "# Just a markdown file\nNo frontmatter here.", + expectNil: true, + }, + { + name: "empty frontmatter", + content: "---\n---\n# Body", + expectNil: true, + }, + { + name: "malformed YAML", + content: "---\nname: [invalid yaml\n---\n# Body", + expectNil: true, + }, + { + name: "no closing delimiter", + content: "---\nname: test\n# No closing delimiter", + expectNil: true, + }, + { + name: "triple dashes in YAML value", + content: "---\nname: my-skill\ndescription: Use --- to separate sections\n---\n# Body", + expectName: "my-skill", + expectDesc: "Use --- to separate sections", + }, + { + name: "CRLF line endings", + content: "---\r\nname: crlf-skill\r\ndescription: Windows style\r\n---\r\n# Body", + expectName: "crlf-skill", + expectDesc: "Windows style", + }, + { + name: "dashes in value prefix not treated as delimiter", + content: "---\nname: test\nother: ---value\n---\n# Body", + expectName: "test", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "SKILL.md") + if err := os.WriteFile(path, []byte(tt.content), 0644); err != nil { + t.Fatal(err) + } + + fm := parseSkillFrontmatter(path) + if tt.expectNil { + if fm != nil { + t.Errorf("expected nil, got %+v", fm) + } + return + } + if fm == nil { + t.Fatal("expected non-nil frontmatter") + } + if fm.Name != tt.expectName { + t.Errorf("Name = %q, want %q", fm.Name, tt.expectName) + } + if fm.Description != tt.expectDesc { + t.Errorf("Description = %q, want %q", fm.Description, tt.expectDesc) + } + if fm.License != tt.expectLic { + t.Errorf("License = %q, want %q", fm.License, tt.expectLic) + } + }) + } +} + +func TestDirContainsSkillMD(t *testing.T) { + tests := []struct { + name string + files []string + expect bool + }{ + { + name: "has SKILL.md", + files: []string{"SKILL.md", "README.md"}, + expect: true, + }, + { + name: "has skill.md lowercase", + files: []string{"skill.md"}, + expect: true, + }, + { + name: "has Skill.md mixed case", + files: []string{"Skill.md"}, + expect: true, + }, + { + name: "no skill file", + files: []string{"README.md", "main.py"}, + expect: false, + }, + { + name: "empty directory", + files: []string{}, + expect: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := DirectoryListing{Name: "test", Path: "test"} + for _, f := range tt.files { + dir.Files = append(dir.Files, FileListing{Name: f, Path: "test/" + f}) + } + + found, _ := dirContainsSkillMD(dir) + if found != tt.expect { + t.Errorf("dirContainsSkillMD() = %v, want %v", found, tt.expect) + } + }) + } +} + +func TestBuildPromptFromSkill(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + skillContent := "---\nname: test-skill\ndescription: A test skill\n---\n# Body" + skillDir := filepath.Join(tmpDir, "myskill") + if err := os.MkdirAll(skillDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(skillContent), 0644); err != nil { + t.Fatal(err) + } + + dir := DirectoryListing{ + Name: "myskill", + Path: "myskill", + Files: []FileListing{ + {Name: "SKILL.md", Path: "myskill/SKILL.md"}, + }, + } + + prompt, _ := buildPromptFromSkill(dir) + if prompt.Path != "myskill" { + t.Errorf("Path = %q, want %q", prompt.Path, "myskill") + } + if prompt.Name != "test-skill" { + t.Errorf("Name = %q, want %q", prompt.Name, "test-skill") + } + if prompt.Description != "A test skill" { + t.Errorf("Description = %q, want %q", prompt.Description, "A test skill") + } +} + +func TestGenerateKitfile_RootSkillMD(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + skillContent := "---\nname: root-skill\ndescription: Root level skill\nlicense: MIT\n---\n# Body" + if err := os.WriteFile(filepath.Join(tmpDir, "SKILL.md"), []byte(skillContent), 0644); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(tmpDir, "scripts"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tmpDir, "scripts", "run.py"), []byte("print('hi')"), 0644); err != nil { + t.Fatal(err) + } + + dir, err := DirectoryListingFromFS(tmpDir) + if err != nil { + t.Fatal(err) + } + + kitfile, err := GenerateKitfile(dir, nil) + if err != nil { + t.Fatal(err) + } + + if len(kitfile.Prompts) != 1 { + t.Fatalf("expected 1 prompt, got %d", len(kitfile.Prompts)) + } + if kitfile.Prompts[0].Path != "." { + t.Errorf("prompt path = %q, want %q", kitfile.Prompts[0].Path, ".") + } + if kitfile.Prompts[0].Name != "root-skill" { + t.Errorf("prompt name = %q, want %q", kitfile.Prompts[0].Name, "root-skill") + } + if kitfile.Package.Name != "root-skill" { + t.Errorf("package name = %q, want %q", kitfile.Package.Name, "root-skill") + } + if kitfile.Package.License != "MIT" { + t.Errorf("package license = %q, want %q", kitfile.Package.License, "MIT") + } + // Root SKILL.md should consolidate everything — no code/docs/datasets + if len(kitfile.Code) != 0 { + t.Errorf("expected 0 code layers, got %d", len(kitfile.Code)) + } +} + +func TestGenerateKitfile_SubdirSkillMD(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + skillDir := filepath.Join(tmpDir, "pdf-tools") + if err := os.MkdirAll(filepath.Join(skillDir, "scripts"), 0755); err != nil { + t.Fatal(err) + } + skillContent := "---\nname: pdf-tools\ndescription: PDF processing\nlicense: Apache-2.0\n---\n# Body" + if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(skillContent), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(skillDir, "scripts", "run.py"), []byte(""), 0644); err != nil { + t.Fatal(err) + } + + dir, err := DirectoryListingFromFS(tmpDir) + if err != nil { + t.Fatal(err) + } + + kitfile, err := GenerateKitfile(dir, nil) + if err != nil { + t.Fatal(err) + } + + if len(kitfile.Prompts) != 1 { + t.Fatalf("expected 1 prompt, got %d", len(kitfile.Prompts)) + } + if kitfile.Prompts[0].Path != "pdf-tools" { + t.Errorf("prompt path = %q, want %q", kitfile.Prompts[0].Path, "pdf-tools") + } + if kitfile.Prompts[0].Name != "pdf-tools" { + t.Errorf("prompt name = %q, want %q", kitfile.Prompts[0].Name, "pdf-tools") + } + if kitfile.Package.Name != "pdf-tools" { + t.Errorf("package name = %q, want %q", kitfile.Package.Name, "pdf-tools") + } + if kitfile.Package.License != "Apache-2.0" { + t.Errorf("package license = %q, want %q", kitfile.Package.License, "Apache-2.0") + } +} + +func TestGenerateKitfile_UserOverridesFrontmatter(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + skillContent := "---\nname: skill-name\ndescription: skill desc\nlicense: MIT\n---\n# Body" + if err := os.WriteFile(filepath.Join(tmpDir, "SKILL.md"), []byte(skillContent), 0644); err != nil { + t.Fatal(err) + } + + dir, err := DirectoryListingFromFS(tmpDir) + if err != nil { + t.Fatal(err) + } + + userPkg := &artifact.Package{ + Name: "user-name", + Description: "user desc", + } + kitfile, err := GenerateKitfile(dir, userPkg) + if err != nil { + t.Fatal(err) + } + + if kitfile.Package.Name != "user-name" { + t.Errorf("package name = %q, want %q (user override)", kitfile.Package.Name, "user-name") + } + if kitfile.Package.Description != "user desc" { + t.Errorf("package desc = %q, want %q (user override)", kitfile.Package.Description, "user desc") + } + // License not set by user, should come from frontmatter + if kitfile.Package.License != "MIT" { + t.Errorf("package license = %q, want %q (from frontmatter)", kitfile.Package.License, "MIT") + } +} + +func TestGenerateKitfile_MultiSkill(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + // Create two skill directories + for _, skill := range []struct { + name, desc, license string + }{ + {"docx", "Word processing", "Apache-2.0"}, + {"xlsx", "Spreadsheet processing", "Apache-2.0"}, + } { + dir := filepath.Join(tmpDir, skill.name) + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatal(err) + } + content := "---\nname: " + skill.name + "\ndescription: " + skill.desc + "\nlicense: " + skill.license + "\n---\n# Body" + if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(content), 0644); err != nil { + t.Fatal(err) + } + } + + dir, err := DirectoryListingFromFS(tmpDir) + if err != nil { + t.Fatal(err) + } + + kitfile, err := GenerateKitfile(dir, nil) + if err != nil { + t.Fatal(err) + } + + if len(kitfile.Prompts) != 2 { + t.Fatalf("expected 2 prompts, got %d", len(kitfile.Prompts)) + } + // Multi-skill: name/desc should NOT be promoted to package level + if kitfile.Package.Name != "" { + t.Errorf("package name should be empty for multi-skill, got %q", kitfile.Package.Name) + } + // License from first skill should be promoted + if kitfile.Package.License != "Apache-2.0" { + t.Errorf("package license = %q, want %q", kitfile.Package.License, "Apache-2.0") + } +} + +func TestGenerateKitfile_MultiSkillMixedDirs(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + // One skill directory + skillDir := filepath.Join(tmpDir, "my-skill") + if err := os.MkdirAll(skillDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("---\nname: my-skill\n---\n# Body"), 0644); err != nil { + t.Fatal(err) + } + + // One regular docs directory + docsDir := filepath.Join(tmpDir, "docs") + if err := os.MkdirAll(docsDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(docsDir, "guide.md"), []byte("# Guide"), 0644); err != nil { + t.Fatal(err) + } + + dir, err := DirectoryListingFromFS(tmpDir) + if err != nil { + t.Fatal(err) + } + + kitfile, err := GenerateKitfile(dir, nil) + if err != nil { + t.Fatal(err) + } + + if len(kitfile.Prompts) != 1 { + t.Fatalf("expected 1 prompt, got %d", len(kitfile.Prompts)) + } + if kitfile.Prompts[0].Name != "my-skill" { + t.Errorf("prompt name = %q, want %q", kitfile.Prompts[0].Name, "my-skill") + } + if len(kitfile.Docs) != 1 { + t.Fatalf("expected 1 docs layer, got %d", len(kitfile.Docs)) + } + // Mixed content: skill name should NOT be promoted to package + if kitfile.Package.Name != "" { + t.Errorf("package name should be empty for mixed content, got %q", kitfile.Package.Name) + } +} + +func TestGenerateKitfile_RootSkillOverridesSubdir(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + // Root SKILL.md + if err := os.WriteFile(filepath.Join(tmpDir, "SKILL.md"), []byte("---\nname: root\n---\n# Root"), 0644); err != nil { + t.Fatal(err) + } + // Subdirectory also has SKILL.md + subDir := filepath.Join(tmpDir, "sub") + if err := os.MkdirAll(subDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(subDir, "SKILL.md"), []byte("---\nname: sub\n---\n# Sub"), 0644); err != nil { + t.Fatal(err) + } + + dir, err := DirectoryListingFromFS(tmpDir) + if err != nil { + t.Fatal(err) + } + + kitfile, err := GenerateKitfile(dir, nil) + if err != nil { + t.Fatal(err) + } + + // Root SKILL.md takes precedence — single prompt with path "." + if len(kitfile.Prompts) != 1 { + t.Fatalf("expected 1 prompt (root takes precedence), got %d", len(kitfile.Prompts)) + } + if kitfile.Prompts[0].Path != "." { + t.Errorf("prompt path = %q, want %q", kitfile.Prompts[0].Path, ".") + } + if kitfile.Prompts[0].Name != "root" { + t.Errorf("prompt name = %q, want %q", kitfile.Prompts[0].Name, "root") + } +} diff --git a/testing/testdata/kitfile-generation/test_prompt-handling.yaml b/testing/testdata/kitfile-generation/test_prompt-handling.yaml index cc8069063..89dcc6808 100644 --- a/testing/testdata/kitfile-generation/test_prompt-handling.yaml +++ b/testing/testdata/kitfile-generation/test_prompt-handling.yaml @@ -4,7 +4,6 @@ files: # Special-cased names - AGENTS.md - CLAUDE.md - - SKILL.md # filenames containing .prompt - my.prompt.md - test.prompt.pdf @@ -30,7 +29,6 @@ expectedKitfile: prompts: - path: AGENTS.md - path: CLAUDE.md - - path: SKILL.md - path: my.prompt.md - path: suffix.prompt - path: test.prompt.pdf