Skip to content
3 changes: 0 additions & 3 deletions CLAUDE.md

This file was deleted.

44 changes: 41 additions & 3 deletions internal/cli/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/fullsend-ai/fullsend/internal/dispatch/gcf"
"github.com/fullsend-ai/fullsend/internal/forge"
gh "github.com/fullsend-ai/fullsend/internal/forge/github"
"github.com/fullsend-ai/fullsend/internal/harness"
"github.com/fullsend-ai/fullsend/internal/inference"
"github.com/fullsend-ai/fullsend/internal/inference/vertex"
"github.com/fullsend-ai/fullsend/internal/layers"
Expand Down Expand Up @@ -1346,7 +1347,7 @@ func runAppSetup(ctx context.Context, client forge.Client, printer *ui.Printer,
// of app-set B. Without this, nonflux-triage (app-set "nonflux") would
// prevent fullsend-ai-triage (app-set "fullsend-ai") from being detected
// and installed.
knownSlugs := filterSlugsByAppSet(loadKnownSlugs(ctx, client, org), appSet)
knownSlugs := filterSlugsByAppSet(loadKnownSlugs(ctx, client, org, forge.ConfigRepoName, "HEAD", printer), appSet)
for role, slug := range filterSlugsByAppSet(sharedSlugs, appSet) {
knownSlugs[role] = slug
}
Expand Down Expand Up @@ -2006,8 +2007,45 @@ func filterSlugsByAppSet(slugs map[string]string, appSet string) map[string]stri
return out
}

// loadKnownSlugs tries to read agent slugs from an existing config.
func loadKnownSlugs(ctx context.Context, client forge.Client, org string) map[string]string {
// loadKnownSlugs discovers agent slugs from harness wrapper files in the
// config repo, falling back to the config.yaml agents: block.
func loadKnownSlugs(ctx context.Context, client forge.Client, org, configRepo, ref string, printer *ui.Printer) map[string]string {
agents, err := harness.DiscoverRemoteAgents(ctx, client, org, configRepo, ref)
if err != nil {
printer.StepWarn(fmt.Sprintf("harness discovery: %v", err))
}
if len(agents) > 0 {
slugs := make(map[string]string, len(agents))
seen := make(map[string]bool, len(agents))
for _, a := range agents {
if a.Role == "" && a.Slug == "" {
continue
}
if a.Role == "" || a.Slug == "" {
printer.StepWarn(fmt.Sprintf("harness %s has role=%q slug=%q; both must be set", a.Filename, a.Role, a.Slug))
continue
}
if seen[a.Role] {
printer.StepInfo(fmt.Sprintf("duplicate role %q in harness file %s, using first occurrence", a.Role, a.Filename))
continue
}
seen[a.Role] = true
slugs[a.Role] = a.Slug
}
if len(slugs) > 0 {
return slugs
}
}

slugs := loadKnownSlugsLegacy(ctx, client, org)
if len(slugs) > 0 {
printer.StepWarn("config.yaml agents: block is deprecated; agent identity should be in harness files with role/slug fields")
}
return slugs
}

// loadKnownSlugsLegacy reads agent slugs from the config.yaml agents: block.
func loadKnownSlugsLegacy(ctx context.Context, client forge.Client, org string) map[string]string {
data, err := client.GetFileContent(ctx, org, forge.ConfigRepoName, "config.yaml")
if err != nil {
return nil
Expand Down
188 changes: 188 additions & 0 deletions internal/cli/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2215,6 +2215,194 @@ func TestApplyPerRepoScaffold_ProtectedBranch_DuplicatePR(t *testing.T) {
assert.Contains(t, output, "Merge the PR")
}

func TestLoadKnownSlugs_HarnessFilesPreferred(t *testing.T) {
client := forge.NewFakeClient()
client.DirContents["myorg/.fullsend/harness@HEAD"] = []forge.DirectoryEntry{
{Path: "harness/triage.yaml", Type: "file"},
{Path: "harness/coder.yaml", Type: "file"},
}
client.FileContentsRef["myorg/.fullsend/harness/triage.yaml@HEAD"] = []byte("role: triage\nslug: fullsend-ai-triage\n")
client.FileContentsRef["myorg/.fullsend/harness/coder.yaml@HEAD"] = []byte("role: coder\nslug: fullsend-ai-coder\n")

// Also set up config.yaml agents: block — should NOT be used.
client.FileContents["myorg/.fullsend/config.yaml"] = []byte(`version: "1"
agents:
- role: triage
slug: old-triage-slug
name: old-triage
`)

var buf bytes.Buffer
printer := ui.New(&buf)
slugs := loadKnownSlugs(context.Background(), client, "myorg", forge.ConfigRepoName, "HEAD", printer)

assert.Equal(t, map[string]string{
"triage": "fullsend-ai-triage",
"coder": "fullsend-ai-coder",
}, slugs)
assert.NotContains(t, buf.String(), "agents: block")
}

func TestLoadKnownSlugs_FallbackToAgentsBlock(t *testing.T) {
client := forge.NewFakeClient()
// No harness/ directory → ErrNotFound from DirContents.

client.FileContents["myorg/.fullsend/config.yaml"] = []byte(`version: "1"
agents:
- role: triage
slug: fullsend-ai-triage
name: fullsend-ai-triage
- role: coder
slug: fullsend-ai-coder
name: fullsend-ai-coder
`)

var buf bytes.Buffer
printer := ui.New(&buf)
slugs := loadKnownSlugs(context.Background(), client, "myorg", forge.ConfigRepoName, "HEAD", printer)

assert.Equal(t, map[string]string{
"triage": "fullsend-ai-triage",
"coder": "fullsend-ai-coder",
}, slugs)
assert.Contains(t, buf.String(), "agents: block")
}

func TestLoadKnownSlugs_HarnessFilesWithoutRoleSlug_FallsBack(t *testing.T) {
client := forge.NewFakeClient()
// Harness files exist but lack role/slug (legacy format).
client.DirContents["myorg/.fullsend/harness@HEAD"] = []forge.DirectoryEntry{
{Path: "harness/triage.yaml", Type: "file"},
}
client.FileContentsRef["myorg/.fullsend/harness/triage.yaml@HEAD"] = []byte("agent: agents/triage.md\nmodel: opus\n")

client.FileContents["myorg/.fullsend/config.yaml"] = []byte(`version: "1"
agents:
- role: triage
slug: fullsend-ai-triage
name: fullsend-ai-triage
`)

var buf bytes.Buffer
printer := ui.New(&buf)
slugs := loadKnownSlugs(context.Background(), client, "myorg", forge.ConfigRepoName, "HEAD", printer)

assert.Equal(t, map[string]string{
"triage": "fullsend-ai-triage",
}, slugs)
assert.Contains(t, buf.String(), "agents: block")
}

func TestLoadKnownSlugs_NeitherSource_ReturnsNil(t *testing.T) {
client := forge.NewFakeClient()
// No harness/ dir, no config.yaml.

var buf bytes.Buffer
printer := ui.New(&buf)
slugs := loadKnownSlugs(context.Background(), client, "myorg", forge.ConfigRepoName, "HEAD", printer)

assert.Nil(t, slugs)
assert.NotContains(t, buf.String(), "agents: block")
}

func TestLoadKnownSlugs_DuplicateRoles_FirstWins(t *testing.T) {
client := forge.NewFakeClient()
client.DirContents["myorg/.fullsend/harness@HEAD"] = []forge.DirectoryEntry{
{Path: "harness/code.yaml", Type: "file"},
{Path: "harness/fix.yaml", Type: "file"},
}
// Both files declare role: coder. DiscoverRemoteAgents sorts by Role then
// Filename, so code.yaml comes first.
client.FileContentsRef["myorg/.fullsend/harness/code.yaml@HEAD"] = []byte("role: coder\nslug: fullsend-ai-coder\n")
client.FileContentsRef["myorg/.fullsend/harness/fix.yaml@HEAD"] = []byte("role: coder\nslug: fullsend-ai-fix\n")

var buf bytes.Buffer
printer := ui.New(&buf)
slugs := loadKnownSlugs(context.Background(), client, "myorg", forge.ConfigRepoName, "HEAD", printer)

assert.Equal(t, map[string]string{
"coder": "fullsend-ai-coder",
}, slugs)
assert.Contains(t, buf.String(), "duplicate role")
}

func TestLoadKnownSlugs_PartialError_LogsWarning(t *testing.T) {
client := forge.NewFakeClient()
client.DirContents["myorg/.fullsend/harness@HEAD"] = []forge.DirectoryEntry{
{Path: "harness/triage.yaml", Type: "file"},
{Path: "harness/bad.yaml", Type: "file"},
}
client.FileContentsRef["myorg/.fullsend/harness/triage.yaml@HEAD"] = []byte("role: triage\nslug: fullsend-ai-triage\n")
// bad.yaml is not in FileContentsRef → GetFileContentAtRef returns ErrNotFound.

var buf bytes.Buffer
printer := ui.New(&buf)
slugs := loadKnownSlugs(context.Background(), client, "myorg", forge.ConfigRepoName, "HEAD", printer)

assert.Equal(t, map[string]string{
"triage": "fullsend-ai-triage",
}, slugs)
assert.Contains(t, buf.String(), "harness discovery")
}

func TestLoadKnownSlugs_RoleWithoutSlug_WarnsAndSkips(t *testing.T) {
client := forge.NewFakeClient()
client.DirContents["myorg/.fullsend/harness@HEAD"] = []forge.DirectoryEntry{
{Path: "harness/triage.yaml", Type: "file"},
}
client.FileContentsRef["myorg/.fullsend/harness/triage.yaml@HEAD"] = []byte("role: triage\n")

client.FileContents["myorg/.fullsend/config.yaml"] = []byte(`version: "1"
agents:
- role: triage
slug: fullsend-ai-triage
name: fullsend-ai-triage
`)

var buf bytes.Buffer
printer := ui.New(&buf)
slugs := loadKnownSlugs(context.Background(), client, "myorg", forge.ConfigRepoName, "HEAD", printer)

assert.Equal(t, map[string]string{
"triage": "fullsend-ai-triage",
}, slugs)
assert.Contains(t, buf.String(), "both must be set")
}

func TestLoadKnownSlugs_HardError_ZeroAgents_FallsBack(t *testing.T) {
client := forge.NewFakeClient()
client.Errors["ListDirectoryContents"] = fmt.Errorf("network timeout")

client.FileContents["myorg/.fullsend/config.yaml"] = []byte(`version: "1"
agents:
- role: triage
slug: fullsend-ai-triage
name: fullsend-ai-triage
`)

var buf bytes.Buffer
printer := ui.New(&buf)
slugs := loadKnownSlugs(context.Background(), client, "myorg", forge.ConfigRepoName, "HEAD", printer)

assert.Equal(t, map[string]string{
"triage": "fullsend-ai-triage",
}, slugs)
assert.Contains(t, buf.String(), "harness discovery")
assert.Contains(t, buf.String(), "deprecated")
}

func TestLoadKnownSlugs_MalformedConfig_ReturnsNil(t *testing.T) {
client := forge.NewFakeClient()
// No harness/ dir, malformed config.yaml.
client.FileContents["myorg/.fullsend/config.yaml"] = []byte("not: valid: yaml: [")

var buf bytes.Buffer
printer := ui.New(&buf)
slugs := loadKnownSlugs(context.Background(), client, "myorg", forge.ConfigRepoName, "HEAD", printer)

assert.Nil(t, slugs)
}

func TestApplyPerRepoScaffold_ProtectedBranch_BranchUpToDate(t *testing.T) {
client := forge.NewFakeClient()
client.Repos = []forge.Repository{{FullName: "acme/widget", DefaultBranch: "main"}}
Expand Down
7 changes: 7 additions & 0 deletions qf-tests/GH-49/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# QualityFlow Tests — GH-49

Generated by the QualityFlow pipeline.

| Directory | Count | Framework |
|-----------|-------|-----------|
| `go/` | 7 files | Go |
104 changes: 104 additions & 0 deletions qf-tests/GH-49/go/agent_slug_dedup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package tests

import (
"bytes"
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

/*
Agent Slug Discovery — Duplicate Role Handling Tests

STP Reference: outputs/stp/GH-49/GH-49_test_plan.md
STD Reference: outputs/std/GH-49/GH-49_test_description.yaml
Jira: GH-49
*/

var _ = Describe("[GH-49] Agent Slug Discovery Deduplication", func() {

Context("Duplicate role handling", Ordered, func() {
var (
ctx context.Context
mockForge *MockForgeClient
printerOutput *bytes.Buffer
agents []AgentInfo
err error
)

// TS-GH-49-010: Verify duplicate roles keep first occurrence
Context("when harness files contain duplicate roles", Ordered, func() {
BeforeAll(func() {
ctx = context.Background()
mockForge = NewMockForgeClient(
withHarnessFiles(map[string]HarnessWrapperFile{
"dup-a.yaml": {Role: "shared-role", Slug: "slug-first"},
"dup-b.yaml": {Role: "shared-role", Slug: "slug-second"},
"unique.yaml": {Role: "unique-role", Slug: "unique-slug"},
}),
)
})

It("[test_id:TS-GH-49-010] should keep first occurrence sorted by Role then Filename", func() {
printerOutput = new(bytes.Buffer)
printer := NewPrinter(printerOutput)
agents, err = DiscoverAgentSlugs(ctx, mockForge, "config-repo", "main", printer)

Expect(err).NotTo(HaveOccurred())

// Count agents with the shared role — should be exactly 1
sharedRoleCount := 0
var retainedSlug string
for _, a := range agents {
if a.Role == "shared-role" {
sharedRoleCount++
retainedSlug = a.Slug
}
}
Expect(sharedRoleCount).To(Equal(1),
"only one agent per duplicate role should be retained")

// First occurrence by filename sort: dup-a.yaml < dup-b.yaml
Expect(retainedSlug).To(Equal("slug-first"),
"first occurrence by Role+Filename sort order should be retained")

// Unique role should still be present
hasUnique := false
for _, a := range agents {
if a.Role == "unique-role" {
hasUnique = true
}
}
Expect(hasUnique).To(BeTrue(), "non-duplicate roles should be preserved")
})
})

// TS-GH-49-011: Verify info message logged for duplicate role
Context("when duplicate roles are detected", Ordered, func() {
BeforeAll(func() {
ctx = context.Background()
mockForge = NewMockForgeClient(
withHarnessFiles(map[string]HarnessWrapperFile{
"first.yaml": {Role: "dup-role", Slug: "slug-1"},
"second.yaml": {Role: "dup-role", Slug: "slug-2"},
}),
)
})

It("[test_id:TS-GH-49-011] should log info message about duplicate", func() {
printerOutput = new(bytes.Buffer)
printer := NewPrinter(printerOutput)
_, err = DiscoverAgentSlugs(ctx, mockForge, "config-repo", "main", printer)

Expect(err).NotTo(HaveOccurred())

output := printerOutput.String()
Expect(output).To(SatisfyAny(
ContainSubstring("duplicate"),
ContainSubstring("already"),
), "info message should be logged when duplicate role is detected")
})
})
})
})
Loading
Loading