Skip to content
Closed
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
3 changes: 0 additions & 3 deletions CLAUDE.md

This file was deleted.

76 changes: 76 additions & 0 deletions internal/harness/discover_remote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package harness

import (
"context"
"errors"
"fmt"
"path"
"sort"
"strings"

"github.com/fullsend-ai/fullsend/internal/forge"
)

// DiscoverRemoteAgents discovers agent identity (role, slug) from harness files
// in a remote config repo via the forge API. It is the remote counterpart of
// DiscoverAgents, which reads from the local filesystem.
//
// Files where both role and slug are empty are skipped. Per-file errors (parse
// failures, GetFileContentAtRef failures) are collected into a multi-error;
// valid files are still returned alongside the error.
//
// Results are sorted by Role, then by Filename for deterministic output.
// Returns (nil, nil) when the harness/ directory does not exist.
func DiscoverRemoteAgents(ctx context.Context, client forge.Client, owner, repo, ref string) ([]AgentInfo, error) {
entries, err := client.ListDirectoryContents(ctx, owner, repo, "harness", ref, false)
if forge.IsNotFound(err) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("listing harness directory: %w", err)
}

var agents []AgentInfo
var errs []error

for _, e := range entries {
if e.Type != "file" {
continue
}
name := path.Base(e.Path)
if !strings.HasSuffix(name, ".yaml") && !strings.HasSuffix(name, ".yml") {
continue
}

data, err := client.GetFileContentAtRef(ctx, owner, repo, "harness/"+name, ref)
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w", name, err))
continue
}

h, err := parseRaw(data)
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w", name, err))
continue
}

if h.Role == "" && h.Slug == "" {
continue
}

agents = append(agents, AgentInfo{
Role: h.Role,
Slug: h.Slug,
Filename: name,
})
}

sort.Slice(agents, func(i, j int) bool {
if agents[i].Role != agents[j].Role {
return agents[i].Role < agents[j].Role
}
return agents[i].Filename < agents[j].Filename
})

return agents, errors.Join(errs...)
}
226 changes: 226 additions & 0 deletions internal/harness/discover_remote_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package harness

import (
"context"
"fmt"
"testing"

"github.com/fullsend-ai/fullsend/internal/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDiscoverRemoteAgents(t *testing.T) {
ctx := context.Background()
const (
owner = "acme"
repo = ".fullsend"
ref = "main"
)

t.Run("multiple harnesses sorted by role", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "triage.yaml", Type: "file"},
{Path: "code.yaml", Type: "file"},
{Path: "review.yaml", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/triage.yaml@%s", owner, repo, ref)] = []byte("agent: agents/triage.md\nrole: triage\nslug: fs-triage\n")
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/code.yaml@%s", owner, repo, ref)] = []byte("agent: agents/code.md\nrole: coder\nslug: fs-coder\n")
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/review.yaml@%s", owner, repo, ref)] = []byte("agent: agents/review.md\nrole: review\nslug: fs-review\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
require.Len(t, agents, 3)

assert.Equal(t, "coder", agents[0].Role)
assert.Equal(t, "fs-coder", agents[0].Slug)
assert.Equal(t, "code.yaml", agents[0].Filename)

assert.Equal(t, "review", agents[1].Role)
assert.Equal(t, "triage", agents[2].Role)
})

t.Run("no harness directory returns nil nil", func(t *testing.T) {
fc := forge.NewFakeClient()

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
assert.Nil(t, agents)
})

t.Run("skips files without role or slug", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "legacy.yaml", Type: "file"},
{Path: "modern.yaml", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/legacy.yaml@%s", owner, repo, ref)] = []byte("agent: agents/legacy.md\n")
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/modern.yaml@%s", owner, repo, ref)] = []byte("agent: agents/modern.md\nrole: triage\nslug: fs-triage\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
require.Len(t, agents, 1)
assert.Equal(t, "triage", agents[0].Role)
})

t.Run("role only without slug is included", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "partial.yaml", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/partial.yaml@%s", owner, repo, ref)] = []byte("agent: agents/partial.md\nrole: triage\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
require.Len(t, agents, 1)
assert.Equal(t, "triage", agents[0].Role)
assert.Empty(t, agents[0].Slug)
})

t.Run("slug only without role is included", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "slug-only.yaml", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/slug-only.yaml@%s", owner, repo, ref)] = []byte("agent: agents/slug.md\nslug: fs-triage\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
require.Len(t, agents, 1)
assert.Equal(t, "fs-triage", agents[0].Slug)
assert.Empty(t, agents[0].Role)
})

t.Run("malformed YAML returns multi-error with valid files", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "good.yaml", Type: "file"},
{Path: "bad.yaml", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/good.yaml@%s", owner, repo, ref)] = []byte("agent: agents/good.md\nrole: triage\nslug: fs-triage\n")
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/bad.yaml@%s", owner, repo, ref)] = []byte(":\n :\n - [invalid yaml")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.Error(t, err)
assert.Contains(t, err.Error(), "bad.yaml")
require.Len(t, agents, 1)
assert.Equal(t, "triage", agents[0].Role)
})

t.Run("GetFileContentAtRef failure for one file returns multi-error", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "good.yaml", Type: "file"},
{Path: "missing.yaml", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/good.yaml@%s", owner, repo, ref)] = []byte("agent: agents/good.md\nrole: triage\nslug: fs-triage\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.Error(t, err)
assert.Contains(t, err.Error(), "missing.yaml")
require.Len(t, agents, 1)
assert.Equal(t, "triage", agents[0].Role)
})

t.Run("empty harness directory returns empty list", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{}

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
assert.Empty(t, agents)
})

t.Run("yml extension is discovered", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "agent.yml", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/agent.yml@%s", owner, repo, ref)] = []byte("agent: agents/agent.md\nrole: triage\nslug: fs-triage\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
require.Len(t, agents, 1)
assert.Equal(t, "agent.yml", agents[0].Filename)
})

t.Run("skips subdirectories", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "triage.yaml", Type: "file"},
{Path: "subdir", Type: "dir"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/triage.yaml@%s", owner, repo, ref)] = []byte("agent: agents/triage.md\nrole: triage\nslug: fs-triage\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
require.Len(t, agents, 1)
})

t.Run("skips non-YAML files", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "triage.yaml", Type: "file"},
{Path: "readme.md", Type: "file"},
{Path: "notes.txt", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/triage.yaml@%s", owner, repo, ref)] = []byte("agent: agents/triage.md\nrole: triage\nslug: fs-triage\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
require.Len(t, agents, 1)
})

t.Run("same role sorted by filename", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "fix.yaml", Type: "file"},
{Path: "code.yaml", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/fix.yaml@%s", owner, repo, ref)] = []byte("agent: agents/fix.md\nrole: coder\nslug: fs-coder\n")
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/code.yaml@%s", owner, repo, ref)] = []byte("agent: agents/code.md\nrole: coder\nslug: fs-coder-2\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
require.Len(t, agents, 2)
assert.Equal(t, "code.yaml", agents[0].Filename)
assert.Equal(t, "fix.yaml", agents[1].Filename)
})

t.Run("path field is empty for remote agents", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "triage.yaml", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/triage.yaml@%s", owner, repo, ref)] = []byte("agent: agents/triage.md\nrole: triage\nslug: fs-triage\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
require.Len(t, agents, 1)
assert.Empty(t, agents[0].Path)
})

t.Run("path prefix in entry is stripped to bare filename", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.DirContents[fmt.Sprintf("%s/%s/harness@%s", owner, repo, ref)] = []forge.DirectoryEntry{
{Path: "harness/triage.yaml", Type: "file"},
}
fc.FileContentsRef[fmt.Sprintf("%s/%s/harness/triage.yaml@%s", owner, repo, ref)] = []byte("agent: agents/triage.md\nrole: triage\nslug: fs-triage\n")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.NoError(t, err)
require.Len(t, agents, 1)
assert.Equal(t, "triage.yaml", agents[0].Filename)
})

t.Run("ListDirectoryContents error propagates", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.Errors["ListDirectoryContents"] = fmt.Errorf("network error")

agents, err := DiscoverRemoteAgents(ctx, fc, owner, repo, ref)
require.Error(t, err)
assert.Contains(t, err.Error(), "listing harness directory")
assert.Nil(t, agents)
})
}
19 changes: 12 additions & 7 deletions internal/harness/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,17 @@ func LoadWithOpts(path string, opts LoadOpts) (*Harness, error) {
return h, nil
}

// parseRaw unmarshals raw YAML bytes into a Harness without validation or
// forge resolution. Use this when you already have the bytes (e.g. from a
// forge API call); use LoadRaw for filesystem-based loading.
func parseRaw(data []byte) (*Harness, error) {
var h Harness
if err := yaml.Unmarshal(data, &h); err != nil {
return nil, fmt.Errorf("parsing harness YAML: %w", err)
}
return &h, nil
}

// LoadRaw reads and unmarshals a harness YAML file without calling Validate
// or ResolveForge. Used by base composition to load base harnesses without
// consuming their forge maps before merging, and by the lock command to
Expand All @@ -282,13 +293,7 @@ func LoadRaw(path string) (*Harness, error) {
if err != nil {
return nil, fmt.Errorf("reading harness file: %w", err)
}

var h Harness
if err := yaml.Unmarshal(data, &h); err != nil {
return nil, fmt.Errorf("parsing harness YAML: %w", err)
}

return &h, nil
return parseRaw(data)
}

// Validate checks that required fields are present.
Expand Down
7 changes: 7 additions & 0 deletions qf-tests/GH-42/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# QualityFlow Tests — GH-42

Generated by the QualityFlow pipeline.

| Directory | Count | Framework |
|-----------|-------|-----------|
| `go/` | 1 files | Go |
Loading
Loading