diff --git a/cmd/core-agent/commands.go b/cmd/core-agent/commands.go index b7585f5..4660bae 100644 --- a/cmd/core-agent/commands.go +++ b/cmd/core-agent/commands.go @@ -118,8 +118,8 @@ func (commands appCommandSet) check(_ core.Options) core.Result { wsRoot := agentic.WorkspaceRoot() if fs.IsDir(wsRoot) { - entries := core.PathGlob(core.JoinPath(wsRoot, "*")) - core.Print(nil, " workspace: %s (%d entries)", wsRoot, len(entries)) + statusFiles := agentic.WorkspaceStatusPaths() + core.Print(nil, " workspace: %s (%d workspaces)", wsRoot, len(statusFiles)) } else { core.Print(nil, " workspace: %s (MISSING)", wsRoot) } diff --git a/cmd/core-agent/commands_test.go b/cmd/core-agent/commands_test.go index 5951509..64933e8 100644 --- a/cmd/core-agent/commands_test.go +++ b/cmd/core-agent/commands_test.go @@ -4,10 +4,12 @@ package main import ( "bytes" + "io" "os" "testing" agentpkg "dappco.re/go/agent" + "dappco.re/go/agent/pkg/agentic" "dappco.re/go/core" "github.com/stretchr/testify/assert" ) @@ -31,6 +33,35 @@ func withArgs(t *testing.T, args ...string) { }) } +func captureStdout(t *testing.T, run func()) string { + t.Helper() + + old := os.Stdout + reader, writer, err := os.Pipe() + if err != nil { + t.Fatalf("pipe stdout: %v", err) + } + os.Stdout = writer + defer func() { + os.Stdout = old + }() + + run() + + if err := writer.Close(); err != nil { + t.Fatalf("close writer: %v", err) + } + data, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("read stdout: %v", err) + } + if err := reader.Close(); err != nil { + t.Fatalf("close reader: %v", err) + } + + return string(data) +} + func TestCommands_ApplyLogLevel_Good(t *testing.T) { defer core.SetLevel(core.LevelInfo) @@ -111,6 +142,26 @@ func TestCommands_Check_Good(t *testing.T) { assert.True(t, r.OK) } +func TestCommands_Check_Good_BranchWorkspaceCount(t *testing.T) { + c := newTestCore(t) + + ws := core.JoinPath(agentic.WorkspaceRoot(), "core", "go-io", "feature", "new-ui") + assert.True(t, agentic.LocalFs().EnsureDir(agentic.WorkspaceRepoDir(ws)).OK) + assert.True(t, agentic.LocalFs().EnsureDir(agentic.WorkspaceMetaDir(ws)).OK) + assert.True(t, agentic.LocalFs().Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(agentic.WorkspaceStatus{ + Status: "running", + Repo: "go-io", + Agent: "codex", + })).OK) + + output := captureStdout(t, func() { + r := c.Cli().Run("check") + assert.True(t, r.OK) + }) + + assert.Contains(t, output, "1 workspaces") +} + func TestCommands_Env_Good(t *testing.T) { c := newTestCore(t) diff --git a/cmd/core-agent/mcp_service.go b/cmd/core-agent/mcp_service.go index 0faca67..ecf38f0 100644 --- a/cmd/core-agent/mcp_service.go +++ b/cmd/core-agent/mcp_service.go @@ -19,23 +19,23 @@ func registerMCPService(c *core.Core) core.Result { return core.Result{Value: core.E("main.registerMCPService", "core is required", nil), OK: false} } - var subsystems []mcp.Subsystem + var registeredSubsystems []mcp.Subsystem - if prep, ok := core.ServiceFor[*agentic.PrepSubsystem](c, "agentic"); ok { - subsystems = append(subsystems, prep) + if agenticSubsystem, ok := core.ServiceFor[*agentic.PrepSubsystem](c, "agentic"); ok { + registeredSubsystems = append(registeredSubsystems, agenticSubsystem) } - if mon, ok := core.ServiceFor[*monitor.Subsystem](c, "monitor"); ok { - subsystems = append(subsystems, mon) + if monitorSubsystem, ok := core.ServiceFor[*monitor.Subsystem](c, "monitor"); ok { + registeredSubsystems = append(registeredSubsystems, monitorSubsystem) } - if brn, ok := core.ServiceFor[*brain.DirectSubsystem](c, "brain"); ok { - subsystems = append(subsystems, brn) + if brainSubsystem, ok := core.ServiceFor[*brain.DirectSubsystem](c, "brain"); ok { + registeredSubsystems = append(registeredSubsystems, brainSubsystem) } - svc, err := mcp.New(mcp.Options{ - Subsystems: subsystems, + service, err := mcp.New(mcp.Options{ + Subsystems: registeredSubsystems, }) if err != nil { return core.Result{Value: core.E("main.registerMCPService", "create mcp service", err), OK: false} } - return core.Result{Value: svc, OK: true} + return core.Result{Value: service, OK: true} } diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index b33f4b5..1405f46 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -614,6 +614,26 @@ func TestCommands_CmdStatus_Good_DeepWorkspace(t *testing.T) { assert.Contains(t, output, "core/go-io/task-5") } +func TestCommands_CmdStatus_Good_BranchWorkspace(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + + ws := core.JoinPath(WorkspaceRoot(), "core", "go-io", "feature", "new-ui") + fs.EnsureDir(WorkspaceRepoDir(ws)) + fs.EnsureDir(WorkspaceMetaDir(ws)) + fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(WorkspaceStatus{ + Status: "completed", + Repo: "go-io", + Agent: "codex", + })) + + output := captureStdout(t, func() { + r := s.cmdStatus(core.NewOptions()) + assert.True(t, r.OK) + }) + + assert.Contains(t, output, "core/go-io/feature/new-ui") +} + func TestCommands_CmdPrompt_Bad_MissingRepo(t *testing.T) { s, _ := testPrepWithCore(t, nil) r := s.cmdPrompt(core.NewOptions()) diff --git a/pkg/agentic/paths.go b/pkg/agentic/paths.go index 6576baf..cc0cd20 100644 --- a/pkg/agentic/paths.go +++ b/pkg/agentic/paths.go @@ -4,6 +4,8 @@ package agentic import ( "context" + iofs "io/fs" + "sort" "strconv" core "dappco.re/go/core" @@ -82,9 +84,47 @@ func HomeDir() string { } func workspaceStatusPaths(wsRoot string) []string { - old := core.PathGlob(core.JoinPath(wsRoot, "*", "status.json")) - deep := core.PathGlob(core.JoinPath(wsRoot, "*", "*", "*", "status.json")) - return append(old, deep...) + if wsRoot == "" { + return nil + } + + var paths []string + seen := make(map[string]bool) + + var walk func(dir string, depth int) + walk = func(dir string, depth int) { + r := fs.List(dir) + if !r.OK { + return + } + + entries, ok := r.Value.([]iofs.DirEntry) + if !ok { + return + } + + statusPath := core.JoinPath(dir, "status.json") + if fs.IsFile(statusPath) { + if depth == 1 || depth == 3 || (fs.IsDir(core.JoinPath(dir, "repo")) && fs.IsDir(core.JoinPath(dir, ".meta"))) { + if !seen[statusPath] { + seen[statusPath] = true + paths = append(paths, statusPath) + } + return + } + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + walk(core.JoinPath(dir, entry.Name()), depth+1) + } + } + + walk(wsRoot, 0) + sort.Strings(paths) + return paths } // WorkspaceRepoDir returns the checked-out repo directory for a workspace. diff --git a/pkg/agentic/paths_test.go b/pkg/agentic/paths_test.go index 9234ae2..db896e6 100644 --- a/pkg/agentic/paths_test.go +++ b/pkg/agentic/paths_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" core "dappco.re/go/core" ) @@ -65,6 +66,18 @@ func TestPaths_WorkspaceHelpers_Good(t *testing.T) { assert.Contains(t, WorkspaceLogFiles(wsDir), core.JoinPath(metaDir, "agent-codex.log")) } +func TestPaths_WorkspaceHelpers_Good_BranchNameWithSlash(t *testing.T) { + t.Setenv("CORE_WORKSPACE", "/tmp/test-core") + wsDir := core.JoinPath(WorkspaceRoot(), "core", "go-io", "feature", "new-ui") + + require.True(t, fs.EnsureDir(WorkspaceRepoDir(wsDir)).OK) + require.True(t, fs.EnsureDir(WorkspaceMetaDir(wsDir)).OK) + require.True(t, fs.Write(WorkspaceStatusPath(wsDir), "{}").OK) + + assert.Equal(t, "core/go-io/feature/new-ui", WorkspaceName(wsDir)) + assert.Contains(t, WorkspaceStatusPaths(), WorkspaceStatusPath(wsDir)) +} + func TestPaths_PlansRoot_Good(t *testing.T) { t.Setenv("CORE_WORKSPACE", "/tmp/test-core") assert.Equal(t, "/tmp/test-core/plans", PlansRoot())