Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions internal/discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ func (d DiscoveredSkill) HasEval() bool {

// Discover walks the given root directory and finds all skills with eval configs.
// A skill is a directory containing SKILL.md. An eval config is eval.yaml either
// in the same directory, in an evals/ subdirectory, or in a tests/ subdirectory.
// in the same directory, in an evals/ subdirectory, in a tests/ subdirectory, or
// in a project-layout evals/{name}/ directory two levels above the skill directory
// (e.g. project-root/skills/{name}/SKILL.md → project-root/evals/{name}/eval.yaml).
func Discover(root string) ([]DiscoveredSkill, error) {
absRoot, err := filepath.Abs(root)
if err != nil {
Expand Down Expand Up @@ -60,7 +62,7 @@ func Discover(root string) ([]DiscoveredSkill, error) {
if !info.IsDir() && info.Name() == "SKILL.md" {
dir := filepath.Dir(path)
name := filepath.Base(dir)
evalPath := findEvalConfig(dir)
evalPath := findEvalConfig(dir, name)

skills = append(skills, DiscoveredSkill{
Name: name,
Expand All @@ -80,12 +82,16 @@ func Discover(root string) ([]DiscoveredSkill, error) {
}

// findEvalConfig looks for eval.yaml in standard locations relative to a skill directory.
// Priority: tests/eval.yaml > evals/eval.yaml > eval.yaml
func findEvalConfig(skillDir string) string {
// Priority: tests/eval.yaml > evals/eval.yaml > eval.yaml > ../../evals/{name}/eval.yaml
// The last candidate handles the project layout produced by `waza new` in project mode,
// where SKILL.md lives at skills/{name}/SKILL.md and eval.yaml at evals/{name}/eval.yaml.
func findEvalConfig(skillDir, name string) string {
candidates := []string{
filepath.Join(skillDir, "tests", "eval.yaml"),
filepath.Join(skillDir, "evals", "eval.yaml"),
filepath.Join(skillDir, "eval.yaml"),
// Project layout: project-root/skills/{name}/SKILL.md → project-root/evals/{name}/eval.yaml
filepath.Join(filepath.Dir(filepath.Dir(skillDir)), "evals", name, "eval.yaml"),
}

for _, c := range candidates {
Expand Down
37 changes: 37 additions & 0 deletions internal/discovery/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,43 @@ func TestDiscoverEvalsSubdir(t *testing.T) {
}
}

func TestDiscoverProjectLayout(t *testing.T) {
// Simulates the layout produced by `waza new` in project mode:
// project-root/skills/{name}/SKILL.md
// project-root/evals/{name}/eval.yaml
root := t.TempDir()

setupSkillDir(t, filepath.Join(root, "skills", "my-skill"))
setupEvalFile(t, filepath.Join(root, "evals", "my-skill", "eval.yaml"))

skills, err := Discover(root)
if err != nil {
t.Fatal(err)
}

if len(skills) != 1 {
t.Fatalf("expected 1 skill, got %d", len(skills))
}
if skills[0].Name != "my-skill" {
t.Errorf("expected my-skill, got %s", skills[0].Name)
}
if !skills[0].HasEval() {
t.Error("my-skill should have eval (project layout evals/{name}/eval.yaml)")
}
// Check structural aspects: eval.yaml should be inside an evals/my-skill/ directory.
// Avoid exact path comparison because filepath.EvalSymlinks can expand short
// Windows paths (e.g. RUNNER~1 → runneradmin), making exact matches unreliable.
if filepath.Base(skills[0].EvalPath) != "eval.yaml" {
t.Errorf("expected eval.yaml filename, got %s", filepath.Base(skills[0].EvalPath))
}
if filepath.Base(filepath.Dir(skills[0].EvalPath)) != "my-skill" {
t.Errorf("expected eval inside my-skill dir, got %s", filepath.Dir(skills[0].EvalPath))
}
if filepath.Base(filepath.Dir(filepath.Dir(skills[0].EvalPath))) != "evals" {
t.Errorf("expected eval inside evals/ dir, got %s", filepath.Dir(filepath.Dir(skills[0].EvalPath)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Consider adding a priority ordering test — what happens when both skills/my-skill/eval.yaml (local) and �vals/my-skill/eval.yaml (project layout) exist? The priority ordering ensures local wins, but an explicit test would document this behavior and prevent regressions.

}
}

func TestDiscoverNonexistentRoot(t *testing.T) {
_, err := Discover("/nonexistent/path/that/does/not/exist")
if err == nil {
Expand Down
Loading