From deaa06a54d8936f236db3b7d0eefc5ab5eb87f15 Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 22 Mar 2026 03:41:07 +0000 Subject: [PATCH] refactor(pkg): migrate go-io/go-log to Core primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace separate go-io (coreio) and go-log (coreerr) packages with Core's built-in Fs and error/logging functions. This is the reference implementation for how all Core ecosystem packages should migrate. Changes: - coreio.Local.Read/Write/EnsureDir/Delete/IsFile → core.Fs methods - coreerr.E() → core.E(), coreerr.Info/Warn/Error → core.Info/Warn/Error - (value, error) return pattern → core.Result pattern (r.OK, r.Value) - go-io and go-log moved from direct to indirect deps in go.mod - Added AX usage-example comments on key public types - Added newFs("/") helper for unrestricted filesystem access Co-Authored-By: Virgil --- .gitignore | 9 ++--- go.mod | 4 +-- pkg/agentic/dispatch.go | 25 ++++++++------ pkg/agentic/epic.go | 14 ++++---- pkg/agentic/ingest.go | 14 ++++---- pkg/agentic/mirror.go | 8 ++--- pkg/agentic/paths.go | 29 ++++++++++++++-- pkg/agentic/plan.go | 53 ++++++++++++++++------------- pkg/agentic/plan_test.go | 7 ++-- pkg/agentic/pr.go | 26 +++++++------- pkg/agentic/prep.go | 66 ++++++++++++++++++++---------------- pkg/agentic/prep_test.go | 20 +++++------ pkg/agentic/queue.go | 7 ++-- pkg/agentic/queue_test.go | 10 +++--- pkg/agentic/remote.go | 17 +++++----- pkg/agentic/remote_client.go | 16 ++++----- pkg/agentic/remote_status.go | 4 +-- pkg/agentic/resume.go | 15 ++++---- pkg/agentic/review_queue.go | 23 ++++++------- pkg/agentic/scan.go | 16 ++++----- pkg/agentic/status.go | 27 +++++++++------ pkg/agentic/status_test.go | 18 +++++----- pkg/agentic/verify.go | 17 +++++----- pkg/agentic/watch.go | 4 +-- pkg/brain/brain.go | 19 +++++++++-- pkg/brain/direct.go | 24 +++++++------ pkg/brain/direct_test.go | 6 ++-- pkg/brain/messaging.go | 6 ++-- pkg/brain/tools.go | 10 +++--- pkg/monitor/harvest.go | 19 +++++------ pkg/monitor/monitor.go | 33 +++++++++++------- pkg/monitor/sync.go | 5 ++- 32 files changed, 311 insertions(+), 260 deletions(-) diff --git a/.gitignore b/.gitignore index 2365340..dae4197 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ .idea/ +.vscode/ +*.log .core/ -docker/.env -ui/node_modules -# Compiled binaries -core-agent -mcp -*.exe +var/ diff --git a/go.mod b/go.mod index 80d7e34..6775854 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,6 @@ go 1.26.0 require ( dappco.re/go/core v0.5.0 - dappco.re/go/core/io v0.2.0 - dappco.re/go/core/log v0.1.0 dappco.re/go/core/process v0.3.0 dappco.re/go/core/ws v0.3.0 forge.lthn.ai/core/api v0.1.5 @@ -19,6 +17,8 @@ require ( ) require ( + dappco.re/go/core/io v0.2.0 // indirect + dappco.re/go/core/log v0.1.0 // indirect forge.lthn.ai/core/go v0.3.3 // indirect forge.lthn.ai/core/go-ai v0.1.12 // indirect forge.lthn.ai/core/go-i18n v0.1.7 // indirect diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index 2100edf..1eb8d3b 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -11,8 +11,7 @@ import ( "syscall" "time" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "dappco.re/go/core/process" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -104,7 +103,7 @@ func agentCommand(agent, prompt string) (string, []string, error) { script := filepath.Join(home, "Code", "core", "agent", "scripts", "local-agent.sh") return "bash", []string{script, prompt}, nil default: - return "", nil, coreerr.E("agentCommand", "unknown agent: "+agent, nil) + return "", nil, core.E("agentCommand", "unknown agent: "+agent, nil) } } @@ -134,7 +133,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st Detach: true, }) if err != nil { - return 0, "", coreerr.E("dispatch.spawnAgent", "failed to spawn "+agent, err) + return 0, "", core.E("dispatch.spawnAgent", "failed to spawn "+agent, err) } // Close stdin immediately — agents use -p mode, not interactive stdin. @@ -162,7 +161,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st // Write captured output to log file if output := proc.Output(); output != "" { - coreio.Local.Write(outputFile, output) + fs.Write(outputFile, output) } // Determine final status: check exit code, BLOCKED.md, and output @@ -172,9 +171,9 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st question := "" blockedPath := filepath.Join(wsDir, "src", "BLOCKED.md") - if blockedContent, err := coreio.Local.Read(blockedPath); err == nil && strings.TrimSpace(blockedContent) != "" { + if r := fs.Read(blockedPath); r.OK && strings.TrimSpace(r.Value.(string)) != "" { finalStatus = "blocked" - question = strings.TrimSpace(blockedContent) + question = strings.TrimSpace(r.Value.(string)) } else if exitCode != 0 || procStatus == "failed" || procStatus == "killed" { finalStatus = "failed" if exitCode != 0 { @@ -215,10 +214,10 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, input DispatchInput) (*mcp.CallToolResult, DispatchOutput, error) { if input.Repo == "" { - return nil, DispatchOutput{}, coreerr.E("dispatch", "repo is required", nil) + return nil, DispatchOutput{}, core.E("dispatch", "repo is required", nil) } if input.Task == "" { - return nil, DispatchOutput{}, coreerr.E("dispatch", "task is required", nil) + return nil, DispatchOutput{}, core.E("dispatch", "task is required", nil) } if input.Org == "" { input.Org = "core" @@ -243,7 +242,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, } _, prepOut, err := s.prepWorkspace(ctx, req, prepInput) if err != nil { - return nil, DispatchOutput{}, coreerr.E("dispatch", "prep workspace failed", err) + return nil, DispatchOutput{}, core.E("dispatch", "prep workspace failed", err) } wsDir := prepOut.WorkspaceDir @@ -254,7 +253,11 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, if input.DryRun { // Read PROMPT.md for the dry run output - promptContent, _ := coreio.Local.Read(filepath.Join(srcDir, "PROMPT.md")) + r := fs.Read(filepath.Join(srcDir, "PROMPT.md")) + promptContent := "" + if r.OK { + promptContent = r.Value.(string) + } return nil, DispatchOutput{ Success: true, Agent: input.Agent, diff --git a/pkg/agentic/epic.go b/pkg/agentic/epic.go index 4668b35..b5c4957 100644 --- a/pkg/agentic/epic.go +++ b/pkg/agentic/epic.go @@ -10,7 +10,7 @@ import ( "net/http" "strings" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -54,13 +54,13 @@ func (s *PrepSubsystem) registerEpicTool(server *mcp.Server) { func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest, input EpicInput) (*mcp.CallToolResult, EpicOutput, error) { if input.Title == "" { - return nil, EpicOutput{}, coreerr.E("createEpic", "title is required", nil) + return nil, EpicOutput{}, core.E("createEpic", "title is required", nil) } if len(input.Tasks) == 0 { - return nil, EpicOutput{}, coreerr.E("createEpic", "at least one task is required", nil) + return nil, EpicOutput{}, core.E("createEpic", "at least one task is required", nil) } if s.forgeToken == "" { - return nil, EpicOutput{}, coreerr.E("createEpic", "no Forge token configured", nil) + return nil, EpicOutput{}, core.E("createEpic", "no Forge token configured", nil) } if input.Org == "" { input.Org = "core" @@ -113,7 +113,7 @@ func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest epicLabels := append(labelIDs, s.resolveLabelIDs(ctx, input.Org, input.Repo, []string{"epic"})...) epic, err := s.createIssue(ctx, input.Org, input.Repo, input.Title, body.String(), epicLabels) if err != nil { - return nil, EpicOutput{}, coreerr.E("createEpic", "failed to create epic", err) + return nil, EpicOutput{}, core.E("createEpic", "failed to create epic", err) } out := EpicOutput{ @@ -163,12 +163,12 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body resp, err := s.client.Do(req) if err != nil { - return ChildRef{}, coreerr.E("createIssue", "create issue request failed", err) + return ChildRef{}, core.E("createIssue", "create issue request failed", err) } defer resp.Body.Close() if resp.StatusCode != 201 { - return ChildRef{}, coreerr.E("createIssue", fmt.Sprintf("create issue returned %d", resp.StatusCode), nil) + return ChildRef{}, core.E("createIssue", fmt.Sprintf("create issue returned %d", resp.StatusCode), nil) } var result struct { diff --git a/pkg/agentic/ingest.go b/pkg/agentic/ingest.go index fee8452..8b2facd 100644 --- a/pkg/agentic/ingest.go +++ b/pkg/agentic/ingest.go @@ -10,8 +10,6 @@ import ( "os" "path/filepath" "strings" - - coreio "dappco.re/go/core/io" ) // ingestFindings reads the agent output log and creates issues via the API @@ -28,12 +26,12 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) { return } - contentStr, err := coreio.Local.Read(logFiles[0]) - if err != nil || len(contentStr) < 100 { + r := fs.Read(logFiles[0]) + if !r.OK || len(r.Value.(string)) < 100 { return } - body := contentStr + body := r.Value.(string) // Skip quota errors if strings.Contains(body, "QUOTA_EXHAUSTED") || strings.Contains(body, "QuotaError") { @@ -95,11 +93,11 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p // Read the agent API key from file home, _ := os.UserHomeDir() - apiKeyStr, err := coreio.Local.Read(filepath.Join(home, ".claude", "agent-api.key")) - if err != nil { + r := fs.Read(filepath.Join(home, ".claude", "agent-api.key")) + if !r.OK { return } - apiKey := strings.TrimSpace(apiKeyStr) + apiKey := strings.TrimSpace(r.Value.(string)) payload, _ := json.Marshal(map[string]string{ "title": title, diff --git a/pkg/agentic/mirror.go b/pkg/agentic/mirror.go index f56423f..0f39a5d 100644 --- a/pkg/agentic/mirror.go +++ b/pkg/agentic/mirror.go @@ -10,7 +10,7 @@ import ( "path/filepath" "strings" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -88,7 +88,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu fetchCmd.Run() // Check how far ahead local default branch is vs github - localBase := gitDefaultBranch(repoDir) + localBase := DefaultBranch(repoDir) ahead := commitsAhead(repoDir, "github/main", localBase) if ahead == 0 { continue // Already in sync @@ -120,7 +120,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu ensureDevBranch(repoDir) // Push local main to github dev (explicit main, not HEAD) - base := gitDefaultBranch(repoDir) + base := DefaultBranch(repoDir) pushCmd := exec.CommandContext(ctx, "git", "push", "github", base+":refs/heads/dev", "--force") pushCmd.Dir = repoDir if err := pushCmd.Run(); err != nil { @@ -187,7 +187,7 @@ func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string prCmd.Dir = repoDir prOut, err := prCmd.CombinedOutput() if err != nil { - return "", coreerr.E("createGitHubPR", string(prOut), err) + return "", core.E("createGitHubPR", string(prOut), err) } // gh pr create outputs the PR URL on the last line diff --git a/pkg/agentic/paths.go b/pkg/agentic/paths.go index 737878d..9f7fd20 100644 --- a/pkg/agentic/paths.go +++ b/pkg/agentic/paths.go @@ -7,10 +7,33 @@ import ( "os/exec" "path/filepath" "strings" + "unsafe" + + core "dappco.re/go/core" ) +// fs provides unrestricted filesystem access (root "/" = no sandbox). +// +// r := fs.Read("/etc/hostname") +// if r.OK { fmt.Println(r.Value.(string)) } +var fs = newFs("/") + +// newFs creates a core.Fs with the given root directory. +// Root "/" means unrestricted access (same as coreio.Local). +func newFs(root string) *core.Fs { + type fsRoot struct{ root string } + f := &core.Fs{} + (*fsRoot)(unsafe.Pointer(f)).root = root + return f +} + +// LocalFs returns an unrestricted filesystem instance for use by other packages. +func LocalFs() *core.Fs { return fs } + // WorkspaceRoot returns the root directory for agent workspaces. // Checks CORE_WORKSPACE env var first, falls back to ~/Code/.core/workspace. +// +// wsDir := filepath.Join(agentic.WorkspaceRoot(), "go-io-1774149757") func WorkspaceRoot() string { return filepath.Join(CoreRoot(), "workspace") } @@ -32,6 +55,8 @@ func PlansRoot() string { // AgentName returns the name of this agent based on hostname. // Checks AGENT_NAME env var first. +// +// name := agentic.AgentName() // "cladius" on Snider's Mac, "charon" elsewhere func AgentName() string { if name := os.Getenv("AGENT_NAME"); name != "" { return name @@ -44,8 +69,8 @@ func AgentName() string { return "charon" } -// gitDefaultBranch detects the default branch of a repo (main, master, etc.). -func gitDefaultBranch(repoDir string) string { +// DefaultBranch detects the default branch of a repo (main, master, etc.). +func DefaultBranch(repoDir string) string { cmd := exec.Command("git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short") cmd.Dir = repoDir if out, err := cmd.Output(); err == nil { diff --git a/pkg/agentic/plan.go b/pkg/agentic/plan.go index 9980c6c..c0971dd 100644 --- a/pkg/agentic/plan.go +++ b/pkg/agentic/plan.go @@ -12,12 +12,14 @@ import ( "strings" "time" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) // Plan represents an implementation plan for agent work. +// +// plan := &Plan{ID: "migrate-core-abc", Title: "Migrate Core", Status: "draft", Objective: "..."} +// writePlan(PlansRoot(), plan) type Plan struct { ID string `json:"id"` Title string `json:"title"` @@ -146,10 +148,10 @@ func (s *PrepSubsystem) registerPlanTools(server *mcp.Server) { func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, input PlanCreateInput) (*mcp.CallToolResult, PlanCreateOutput, error) { if input.Title == "" { - return nil, PlanCreateOutput{}, coreerr.E("planCreate", "title is required", nil) + return nil, PlanCreateOutput{}, core.E("planCreate", "title is required", nil) } if input.Objective == "" { - return nil, PlanCreateOutput{}, coreerr.E("planCreate", "objective is required", nil) + return nil, PlanCreateOutput{}, core.E("planCreate", "objective is required", nil) } id := generatePlanID(input.Title) @@ -178,7 +180,7 @@ func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, in path, err := writePlan(PlansRoot(), &plan) if err != nil { - return nil, PlanCreateOutput{}, coreerr.E("planCreate", "failed to write plan", err) + return nil, PlanCreateOutput{}, core.E("planCreate", "failed to write plan", err) } return nil, PlanCreateOutput{ @@ -190,7 +192,7 @@ func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, in func (s *PrepSubsystem) planRead(_ context.Context, _ *mcp.CallToolRequest, input PlanReadInput) (*mcp.CallToolResult, PlanReadOutput, error) { if input.ID == "" { - return nil, PlanReadOutput{}, coreerr.E("planRead", "id is required", nil) + return nil, PlanReadOutput{}, core.E("planRead", "id is required", nil) } plan, err := readPlan(PlansRoot(), input.ID) @@ -206,7 +208,7 @@ func (s *PrepSubsystem) planRead(_ context.Context, _ *mcp.CallToolRequest, inpu func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, input PlanUpdateInput) (*mcp.CallToolResult, PlanUpdateOutput, error) { if input.ID == "" { - return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "id is required", nil) + return nil, PlanUpdateOutput{}, core.E("planUpdate", "id is required", nil) } plan, err := readPlan(PlansRoot(), input.ID) @@ -217,7 +219,7 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in // Apply partial updates if input.Status != "" { if !validPlanStatus(input.Status) { - return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "invalid status: "+input.Status+" (valid: draft, ready, in_progress, needs_verification, verified, approved)", nil) + return nil, PlanUpdateOutput{}, core.E("planUpdate", "invalid status: "+input.Status+" (valid: draft, ready, in_progress, needs_verification, verified, approved)", nil) } plan.Status = input.Status } @@ -240,7 +242,7 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in plan.UpdatedAt = time.Now() if _, err := writePlan(PlansRoot(), plan); err != nil { - return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "failed to write plan", err) + return nil, PlanUpdateOutput{}, core.E("planUpdate", "failed to write plan", err) } return nil, PlanUpdateOutput{ @@ -251,16 +253,16 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in func (s *PrepSubsystem) planDelete(_ context.Context, _ *mcp.CallToolRequest, input PlanDeleteInput) (*mcp.CallToolResult, PlanDeleteOutput, error) { if input.ID == "" { - return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "id is required", nil) + return nil, PlanDeleteOutput{}, core.E("planDelete", "id is required", nil) } path := planPath(PlansRoot(), input.ID) if _, err := os.Stat(path); err != nil { - return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "plan not found: "+input.ID, nil) + return nil, PlanDeleteOutput{}, core.E("planDelete", "plan not found: "+input.ID, nil) } - if err := coreio.Local.Delete(path); err != nil { - return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "failed to delete plan", err) + if r := fs.Delete(path); !r.OK { + return nil, PlanDeleteOutput{}, core.E("planDelete", "failed to delete plan", nil) } return nil, PlanDeleteOutput{ @@ -271,13 +273,13 @@ func (s *PrepSubsystem) planDelete(_ context.Context, _ *mcp.CallToolRequest, in func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, input PlanListInput) (*mcp.CallToolResult, PlanListOutput, error) { dir := PlansRoot() - if err := coreio.Local.EnsureDir(dir); err != nil { - return nil, PlanListOutput{}, coreerr.E("planList", "failed to access plans directory", err) + if r := fs.EnsureDir(dir); !r.OK { + return nil, PlanListOutput{}, core.E("planList", "failed to access plans directory", nil) } entries, err := os.ReadDir(dir) if err != nil { - return nil, PlanListOutput{}, coreerr.E("planList", "failed to read plans directory", err) + return nil, PlanListOutput{}, core.E("planList", "failed to read plans directory", err) } var plans []Plan @@ -352,21 +354,21 @@ func generatePlanID(title string) string { } func readPlan(dir, id string) (*Plan, error) { - data, err := coreio.Local.Read(planPath(dir, id)) - if err != nil { - return nil, coreerr.E("readPlan", "plan not found: "+id, nil) + r := fs.Read(planPath(dir, id)) + if !r.OK { + return nil, core.E("readPlan", "plan not found: "+id, nil) } var plan Plan - if err := json.Unmarshal([]byte(data), &plan); err != nil { - return nil, coreerr.E("readPlan", "failed to parse plan "+id, err) + if err := json.Unmarshal([]byte(r.Value.(string)), &plan); err != nil { + return nil, core.E("readPlan", "failed to parse plan "+id, err) } return &plan, nil } func writePlan(dir string, plan *Plan) (string, error) { - if err := coreio.Local.EnsureDir(dir); err != nil { - return "", coreerr.E("writePlan", "failed to create plans directory", err) + if r := fs.EnsureDir(dir); !r.OK { + return "", core.E("writePlan", "failed to create plans directory", nil) } path := planPath(dir, plan.ID) @@ -375,7 +377,10 @@ func writePlan(dir string, plan *Plan) (string, error) { return "", err } - return path, coreio.Local.Write(path, string(data)) + if r := fs.Write(path, string(data)); !r.OK { + return "", core.E("writePlan", "failed to write plan", nil) + } + return path, nil } func validPlanStatus(status string) bool { diff --git a/pkg/agentic/plan_test.go b/pkg/agentic/plan_test.go index 3f59669..1440c24 100644 --- a/pkg/agentic/plan_test.go +++ b/pkg/agentic/plan_test.go @@ -7,7 +7,6 @@ import ( "strings" "testing" - coreio "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -31,7 +30,7 @@ func TestWritePlan_Good(t *testing.T) { assert.Equal(t, filepath.Join(dir, "test-plan-abc123.json"), path) // Verify file exists - assert.True(t, coreio.Local.IsFile(path)) + assert.True(t, fs.IsFile(path)) } func TestWritePlan_Good_CreatesDirectory(t *testing.T) { @@ -96,7 +95,7 @@ func TestReadPlan_Bad_NotFound(t *testing.T) { func TestReadPlan_Bad_InvalidJSON(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "bad-json.json"), "{broken")) + require.True(t, fs.Write(filepath.Join(dir, "bad-json.json"), "{broken").OK) _, err := readPlan(dir, "bad-json") assert.Error(t, err) @@ -205,7 +204,7 @@ func TestWritePlan_Good_OverwriteExisting(t *testing.T) { func TestReadPlan_Ugly_EmptyFile(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "empty.json"), "")) + require.True(t, fs.Write(filepath.Join(dir, "empty.json"), "").OK) _, err := readPlan(dir, "empty") assert.Error(t, err) diff --git a/pkg/agentic/pr.go b/pkg/agentic/pr.go index e1dcbd1..4ce041b 100644 --- a/pkg/agentic/pr.go +++ b/pkg/agentic/pr.go @@ -13,7 +13,7 @@ import ( "path/filepath" "strings" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -48,23 +48,23 @@ func (s *PrepSubsystem) registerCreatePRTool(server *mcp.Server) { func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, input CreatePRInput) (*mcp.CallToolResult, CreatePROutput, error) { if input.Workspace == "" { - return nil, CreatePROutput{}, coreerr.E("createPR", "workspace is required", nil) + return nil, CreatePROutput{}, core.E("createPR", "workspace is required", nil) } if s.forgeToken == "" { - return nil, CreatePROutput{}, coreerr.E("createPR", "no Forge token configured", nil) + return nil, CreatePROutput{}, core.E("createPR", "no Forge token configured", nil) } wsDir := filepath.Join(WorkspaceRoot(), input.Workspace) srcDir := filepath.Join(wsDir, "src") if _, err := os.Stat(srcDir); err != nil { - return nil, CreatePROutput{}, coreerr.E("createPR", "workspace not found: "+input.Workspace, nil) + return nil, CreatePROutput{}, core.E("createPR", "workspace not found: "+input.Workspace, nil) } // Read workspace status for repo, branch, issue context st, err := readStatus(wsDir) if err != nil { - return nil, CreatePROutput{}, coreerr.E("createPR", "no status.json", err) + return nil, CreatePROutput{}, core.E("createPR", "no status.json", err) } if st.Branch == "" { @@ -73,7 +73,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in branchCmd.Dir = srcDir out, err := branchCmd.Output() if err != nil { - return nil, CreatePROutput{}, coreerr.E("createPR", "failed to detect branch", err) + return nil, CreatePROutput{}, core.E("createPR", "failed to detect branch", err) } st.Branch = strings.TrimSpace(string(out)) } @@ -117,13 +117,13 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in pushCmd.Dir = srcDir pushOut, err := pushCmd.CombinedOutput() if err != nil { - return nil, CreatePROutput{}, coreerr.E("createPR", "git push failed: "+string(pushOut), err) + return nil, CreatePROutput{}, core.E("createPR", "git push failed: "+string(pushOut), err) } // Create PR via Forge API prURL, prNum, err := s.forgeCreatePR(ctx, org, st.Repo, st.Branch, base, title, body) if err != nil { - return nil, CreatePROutput{}, coreerr.E("createPR", "failed to create PR", err) + return nil, CreatePROutput{}, core.E("createPR", "failed to create PR", err) } // Update status with PR URL @@ -178,7 +178,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base resp, err := s.client.Do(req) if err != nil { - return "", 0, coreerr.E("forgeCreatePR", "request failed", err) + return "", 0, core.E("forgeCreatePR", "request failed", err) } defer resp.Body.Close() @@ -186,7 +186,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base var errBody map[string]any json.NewDecoder(resp.Body).Decode(&errBody) msg, _ := errBody["message"].(string) - return "", 0, coreerr.E("forgeCreatePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) + return "", 0, core.E("forgeCreatePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) } var pr struct { @@ -253,7 +253,7 @@ func (s *PrepSubsystem) registerListPRsTool(server *mcp.Server) { func (s *PrepSubsystem) listPRs(ctx context.Context, _ *mcp.CallToolRequest, input ListPRsInput) (*mcp.CallToolResult, ListPRsOutput, error) { if s.forgeToken == "" { - return nil, ListPRsOutput{}, coreerr.E("listPRs", "no Forge token configured", nil) + return nil, ListPRsOutput{}, core.E("listPRs", "no Forge token configured", nil) } if input.Org == "" { @@ -310,12 +310,12 @@ func (s *PrepSubsystem) listRepoPRs(ctx context.Context, org, repo, state string resp, err := s.client.Do(req) if err != nil { - return nil, coreerr.E("listRepoPRs", "failed to list PRs for "+repo, err) + return nil, core.E("listRepoPRs", "failed to list PRs for "+repo, err) } defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, coreerr.E("listRepoPRs", fmt.Sprintf("HTTP %d listing PRs for %s", resp.StatusCode, repo), nil) + return nil, core.E("listRepoPRs", fmt.Sprintf("HTTP %d listing PRs for %s", resp.StatusCode, repo), nil) } var prs []struct { diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index ddc863b..795ef81 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -18,9 +18,8 @@ import ( "sync" "time" + core "dappco.re/go/core" "dappco.re/go/agent/pkg/lib" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" "gopkg.in/yaml.v3" ) @@ -31,7 +30,10 @@ type CompletionNotifier interface { Poke() } -// PrepSubsystem provides agentic MCP tools. +// PrepSubsystem provides agentic MCP tools for workspace orchestration. +// +// sub := agentic.NewPrep() +// sub.RegisterTools(server) type PrepSubsystem struct { forgeURL string forgeToken string @@ -45,6 +47,10 @@ type PrepSubsystem struct { } // NewPrep creates an agentic subsystem. +// +// sub := agentic.NewPrep() +// sub.SetCompletionNotifier(monitor) +// sub.RegisterTools(server) func NewPrep() *PrepSubsystem { home, _ := os.UserHomeDir() @@ -55,8 +61,8 @@ func NewPrep() *PrepSubsystem { brainKey := os.Getenv("CORE_BRAIN_KEY") if brainKey == "" { - if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil { - brainKey = strings.TrimSpace(data) + if r := fs.Read(filepath.Join(home, ".claude", "brain.key")); r.OK { + brainKey = strings.TrimSpace(r.Value.(string)) } } @@ -145,7 +151,7 @@ type PrepOutput struct { func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolRequest, input PrepInput) (*mcp.CallToolResult, PrepOutput, error) { if input.Repo == "" { - return nil, PrepOutput{}, coreerr.E("prepWorkspace", "repo is required", nil) + return nil, PrepOutput{}, core.E("prepWorkspace", "repo is required", nil) } if input.Org == "" { input.Org = "core" @@ -164,7 +170,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques // Ensure workspace directory exists if err := os.MkdirAll(wsDir, 0755); err != nil { - return nil, PrepOutput{}, coreerr.E("prep", "failed to create workspace dir", err) + return nil, PrepOutput{}, core.E("prep", "failed to create workspace dir", err) } out := PrepOutput{WorkspaceDir: wsDir} @@ -172,7 +178,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques // Source repo path — sanitise to prevent path traversal repoName := filepath.Base(input.Repo) // strips ../ and absolute paths if repoName == "." || repoName == ".." || repoName == "" { - return nil, PrepOutput{}, coreerr.E("prep", "invalid repo name: "+input.Repo, nil) + return nil, PrepOutput{}, core.E("prep", "invalid repo name: "+input.Repo, nil) } repoPath := filepath.Join(s.codePath, "core", repoName) @@ -180,7 +186,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques srcDir := filepath.Join(wsDir, "src") cloneCmd := exec.CommandContext(ctx, "git", "clone", repoPath, srcDir) if err := cloneCmd.Run(); err != nil { - return nil, PrepOutput{}, coreerr.E("prep", "git clone failed for "+input.Repo, err) + return nil, PrepOutput{}, core.E("prep", "git clone failed for "+input.Repo, err) } // Create feature branch @@ -209,13 +215,13 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques branchCmd := exec.CommandContext(ctx, "git", "checkout", "-b", branchName) branchCmd.Dir = srcDir if err := branchCmd.Run(); err != nil { - return nil, PrepOutput{}, coreerr.E("prep.branch", fmt.Sprintf("failed to create branch %q", branchName), err) + return nil, PrepOutput{}, core.E("prep.branch", fmt.Sprintf("failed to create branch %q", branchName), err) } out.Branch = branchName // Create context dirs inside src/ - coreio.Local.EnsureDir(filepath.Join(srcDir, "kb")) - coreio.Local.EnsureDir(filepath.Join(srcDir, "specs")) + fs.EnsureDir(filepath.Join(srcDir, "kb")) + fs.EnsureDir(filepath.Join(srcDir, "specs")) // Remote stays as local clone origin — agent cannot push to forge. // Reviewer pulls changes from workspace and pushes after verification. @@ -253,13 +259,13 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques // Copy repo's own CLAUDE.md over template if it exists claudeMdPath := filepath.Join(repoPath, "CLAUDE.md") - if data, err := coreio.Local.Read(claudeMdPath); err == nil { - coreio.Local.Write(filepath.Join(srcDir, "CLAUDE.md"), data) + if r := fs.Read(claudeMdPath); r.OK { + fs.Write(filepath.Join(srcDir, "CLAUDE.md"), r.Value.(string)) } // Copy GEMINI.md from core/agent (ethics framework for all agents) agentGeminiMd := filepath.Join(s.codePath, "core", "agent", "GEMINI.md") - if data, err := coreio.Local.Read(agentGeminiMd); err == nil { - coreio.Local.Write(filepath.Join(srcDir, "GEMINI.md"), data) + if r := fs.Read(agentGeminiMd); r.OK { + fs.Write(filepath.Join(srcDir, "GEMINI.md"), r.Value.(string)) } // 3. Generate TODO.md from issue (overrides template) @@ -306,7 +312,7 @@ func (s *PrepSubsystem) writePromptTemplate(template, wsDir string) { } } - coreio.Local.Write(filepath.Join(wsDir, "src", "PROMPT.md"), prompt) + fs.Write(filepath.Join(wsDir, "src", "PROMPT.md"), prompt) } // --- Plan template rendering --- @@ -380,7 +386,7 @@ func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map plan.WriteString("\n**Commit after completing this phase.**\n\n---\n\n") } - coreio.Local.Write(filepath.Join(wsDir, "src", "PLAN.md"), plan.String()) + fs.Write(filepath.Join(wsDir, "src", "PLAN.md"), plan.String()) } // --- Helpers (unchanged) --- @@ -448,7 +454,7 @@ func (s *PrepSubsystem) pullWiki(ctx context.Context, org, repo, wsDir string) i return '-' }, page.Title) + ".md" - coreio.Local.Write(filepath.Join(wsDir, "src", "kb", filename), string(content)) + fs.Write(filepath.Join(wsDir, "src", "kb", filename), string(content)) count++ } @@ -461,8 +467,8 @@ func (s *PrepSubsystem) copySpecs(wsDir string) int { for _, file := range specFiles { src := filepath.Join(s.specsPath, file) - if data, err := coreio.Local.Read(src); err == nil { - coreio.Local.Write(filepath.Join(wsDir, "src", "specs", file), data) + if r := fs.Read(src); r.OK { + fs.Write(filepath.Join(wsDir, "src", "specs", file), r.Value.(string)) count++ } } @@ -515,7 +521,7 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string) content.WriteString(fmt.Sprintf("### %d. %s [%s] (score: %.3f)\n\n%s\n\n", i+1, memProject, memType, score, memContent)) } - coreio.Local.Write(filepath.Join(wsDir, "src", "CONTEXT.md"), content.String()) + fs.Write(filepath.Join(wsDir, "src", "CONTEXT.md"), content.String()) return len(result.Memories) } @@ -523,10 +529,11 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int { goWorkPath := filepath.Join(s.codePath, "go.work") modulePath := "forge.lthn.ai/core/" + repo - workData, err := coreio.Local.Read(goWorkPath) - if err != nil { + r := fs.Read(goWorkPath) + if !r.OK { return 0 } + workData := r.Value.(string) var consumers []string for _, line := range strings.Split(workData, "\n") { @@ -536,10 +543,11 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int { } dir := filepath.Join(s.codePath, strings.TrimPrefix(line, "./")) goMod := filepath.Join(dir, "go.mod") - modData, err := coreio.Local.Read(goMod) - if err != nil { + mr := fs.Read(goMod) + if !mr.OK { continue } + modData := mr.Value.(string) if strings.Contains(modData, modulePath) && !strings.HasPrefix(modData, "module "+modulePath) { consumers = append(consumers, filepath.Base(dir)) } @@ -552,7 +560,7 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int { content += "- " + c + "\n" } content += fmt.Sprintf("\n**Breaking change risk: %d consumers.**\n", len(consumers)) - coreio.Local.Write(filepath.Join(wsDir, "src", "CONSUMERS.md"), content) + fs.Write(filepath.Join(wsDir, "src", "CONSUMERS.md"), content) } return len(consumers) @@ -569,7 +577,7 @@ func (s *PrepSubsystem) gitLog(repoPath, wsDir string) int { lines := strings.Split(strings.TrimSpace(string(output)), "\n") if len(lines) > 0 && lines[0] != "" { content := "# Recent Changes\n\n```\n" + string(output) + "```\n" - coreio.Local.Write(filepath.Join(wsDir, "src", "RECENT.md"), content) + fs.Write(filepath.Join(wsDir, "src", "RECENT.md"), content) } return len(lines) @@ -606,7 +614,7 @@ func (s *PrepSubsystem) generateTodo(ctx context.Context, org, repo string, issu content += fmt.Sprintf("**Repo:** %s/%s\n\n---\n\n", org, repo) content += "## Objective\n\n" + issueData.Body + "\n" - coreio.Local.Write(filepath.Join(wsDir, "src", "TODO.md"), content) + fs.Write(filepath.Join(wsDir, "src", "TODO.md"), content) } // detectLanguage guesses the primary language from repo contents. diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index 39dbadf..ca0687b 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -5,8 +5,6 @@ package agentic import ( "path/filepath" "testing" - - coreio "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -28,43 +26,43 @@ func TestEnvOr_Good_UnsetUsesFallback(t *testing.T) { func TestDetectLanguage_Good_Go(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "go.mod"), "module test")) + require.True(t, fs.Write(filepath.Join(dir, "go.mod"), "module test").OK) assert.Equal(t, "go", detectLanguage(dir)) } func TestDetectLanguage_Good_PHP(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "composer.json"), "{}")) + require.True(t, fs.Write(filepath.Join(dir, "composer.json"), "{}").OK) assert.Equal(t, "php", detectLanguage(dir)) } func TestDetectLanguage_Good_TypeScript(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "package.json"), "{}")) + require.True(t, fs.Write(filepath.Join(dir, "package.json"), "{}").OK) assert.Equal(t, "ts", detectLanguage(dir)) } func TestDetectLanguage_Good_Rust(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "Cargo.toml"), "[package]")) + require.True(t, fs.Write(filepath.Join(dir, "Cargo.toml"), "[package]").OK) assert.Equal(t, "rust", detectLanguage(dir)) } func TestDetectLanguage_Good_Python(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "requirements.txt"), "flask")) + require.True(t, fs.Write(filepath.Join(dir, "requirements.txt"), "flask").OK) assert.Equal(t, "py", detectLanguage(dir)) } func TestDetectLanguage_Good_Cpp(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "CMakeLists.txt"), "cmake_minimum_required")) + require.True(t, fs.Write(filepath.Join(dir, "CMakeLists.txt"), "cmake_minimum_required").OK) assert.Equal(t, "cpp", detectLanguage(dir)) } func TestDetectLanguage_Good_Docker(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "Dockerfile"), "FROM alpine")) + require.True(t, fs.Write(filepath.Join(dir, "Dockerfile"), "FROM alpine").OK) assert.Equal(t, "docker", detectLanguage(dir)) } @@ -90,7 +88,7 @@ func TestDetectBuildCmd_Good(t *testing.T) { for _, tt := range tests { t.Run(tt.file, func(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, tt.file), tt.content)) + require.True(t, fs.Write(filepath.Join(dir, tt.file), tt.content).OK) assert.Equal(t, tt.expected, detectBuildCmd(dir)) }) } @@ -118,7 +116,7 @@ func TestDetectTestCmd_Good(t *testing.T) { for _, tt := range tests { t.Run(tt.file, func(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, tt.file), tt.content)) + require.True(t, fs.Write(filepath.Join(dir, tt.file), tt.content).OK) assert.Equal(t, tt.expected, detectTestCmd(dir)) }) } diff --git a/pkg/agentic/queue.go b/pkg/agentic/queue.go index b5dbf50..cae1bac 100644 --- a/pkg/agentic/queue.go +++ b/pkg/agentic/queue.go @@ -10,7 +10,6 @@ import ( "syscall" "time" - coreio "dappco.re/go/core/io" "gopkg.in/yaml.v3" ) @@ -47,12 +46,12 @@ func (s *PrepSubsystem) loadAgentsConfig() *AgentsConfig { } for _, path := range paths { - data, err := coreio.Local.Read(path) - if err != nil { + r := fs.Read(path) + if !r.OK { continue } var cfg AgentsConfig - if err := yaml.Unmarshal([]byte(data), &cfg); err != nil { + if err := yaml.Unmarshal([]byte(r.Value.(string)), &cfg); err != nil { continue } return &cfg diff --git a/pkg/agentic/queue_test.go b/pkg/agentic/queue_test.go index cf91bfd..1cb91fe 100644 --- a/pkg/agentic/queue_test.go +++ b/pkg/agentic/queue_test.go @@ -5,8 +5,6 @@ package agentic import ( "path/filepath" "testing" - - coreio "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -36,7 +34,7 @@ func TestCanDispatchAgent_Good_NoConfig(t *testing.T) { // With no running workspaces and default config, should be able to dispatch root := t.TempDir() t.Setenv("CORE_WORKSPACE", root) - require.NoError(t, coreio.Local.EnsureDir(filepath.Join(root, "workspace"))) + require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK) s := &PrepSubsystem{codePath: t.TempDir()} assert.True(t, s.canDispatchAgent("gemini")) @@ -46,7 +44,7 @@ func TestCanDispatchAgent_Good_UnknownAgent(t *testing.T) { // Unknown agent has no limit, so always allowed root := t.TempDir() t.Setenv("CORE_WORKSPACE", root) - require.NoError(t, coreio.Local.EnsureDir(filepath.Join(root, "workspace"))) + require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK) s := &PrepSubsystem{codePath: t.TempDir()} assert.True(t, s.canDispatchAgent("unknown-agent")) @@ -55,7 +53,7 @@ func TestCanDispatchAgent_Good_UnknownAgent(t *testing.T) { func TestCountRunningByAgent_Good_EmptyWorkspace(t *testing.T) { root := t.TempDir() t.Setenv("CORE_WORKSPACE", root) - require.NoError(t, coreio.Local.EnsureDir(filepath.Join(root, "workspace"))) + require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK) s := &PrepSubsystem{} assert.Equal(t, 0, s.countRunningByAgent("gemini")) @@ -68,7 +66,7 @@ func TestCountRunningByAgent_Good_NoRunning(t *testing.T) { // Create a workspace with completed status under workspace/ ws := filepath.Join(root, "workspace", "test-ws") - require.NoError(t, coreio.Local.EnsureDir(ws)) + require.True(t, fs.EnsureDir(ws).OK) require.NoError(t, writeStatus(ws, &WorkspaceStatus{ Status: "completed", Agent: "gemini", diff --git a/pkg/agentic/remote.go b/pkg/agentic/remote.go index 0a2e6e1..cab7eca 100644 --- a/pkg/agentic/remote.go +++ b/pkg/agentic/remote.go @@ -11,8 +11,7 @@ import ( "strings" "time" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -50,13 +49,13 @@ func (s *PrepSubsystem) registerRemoteDispatchTool(server *mcp.Server) { func (s *PrepSubsystem) dispatchRemote(ctx context.Context, _ *mcp.CallToolRequest, input RemoteDispatchInput) (*mcp.CallToolResult, RemoteDispatchOutput, error) { if input.Host == "" { - return nil, RemoteDispatchOutput{}, coreerr.E("dispatchRemote", "host is required", nil) + return nil, RemoteDispatchOutput{}, core.E("dispatchRemote", "host is required", nil) } if input.Repo == "" { - return nil, RemoteDispatchOutput{}, coreerr.E("dispatchRemote", "repo is required", nil) + return nil, RemoteDispatchOutput{}, core.E("dispatchRemote", "repo is required", nil) } if input.Task == "" { - return nil, RemoteDispatchOutput{}, coreerr.E("dispatchRemote", "task is required", nil) + return nil, RemoteDispatchOutput{}, core.E("dispatchRemote", "task is required", nil) } // Resolve host aliases @@ -105,7 +104,7 @@ func (s *PrepSubsystem) dispatchRemote(ctx context.Context, _ *mcp.CallToolReque return nil, RemoteDispatchOutput{ Host: input.Host, Error: fmt.Sprintf("init failed: %v", err), - }, coreerr.E("dispatchRemote", "MCP initialize failed", err) + }, core.E("dispatchRemote", "MCP initialize failed", err) } // Step 2: Call the tool @@ -115,7 +114,7 @@ func (s *PrepSubsystem) dispatchRemote(ctx context.Context, _ *mcp.CallToolReque return nil, RemoteDispatchOutput{ Host: input.Host, Error: fmt.Sprintf("call failed: %v", err), - }, coreerr.E("dispatchRemote", "tool call failed", err) + }, core.E("dispatchRemote", "tool call failed", err) } // Parse result @@ -195,8 +194,8 @@ func remoteToken(host string) string { fmt.Sprintf("%s/.core/agent-token", home), } for _, f := range tokenFiles { - if data, err := coreio.Local.Read(f); err == nil { - return strings.TrimSpace(data) + if r := fs.Read(f); r.OK { + return strings.TrimSpace(r.Value.(string)) } } diff --git a/pkg/agentic/remote_client.go b/pkg/agentic/remote_client.go index f49d024..c5af7b9 100644 --- a/pkg/agentic/remote_client.go +++ b/pkg/agentic/remote_client.go @@ -3,6 +3,7 @@ package agentic import ( + core "dappco.re/go/core" "bufio" "bytes" "context" @@ -11,7 +12,6 @@ import ( "net/http" "strings" - coreerr "dappco.re/go/core/log" ) // mcpInitialize performs the MCP initialize handshake over Streamable HTTP. @@ -35,18 +35,18 @@ func mcpInitialize(ctx context.Context, client *http.Client, url, token string) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body)) if err != nil { - return "", coreerr.E("mcpInitialize", "create request", err) + return "", core.E("mcpInitialize", "create request", err) } setHeaders(req, token, "") resp, err := client.Do(req) if err != nil { - return "", coreerr.E("mcpInitialize", "request failed", err) + return "", core.E("mcpInitialize", "request failed", err) } defer resp.Body.Close() if resp.StatusCode != 200 { - return "", coreerr.E("mcpInitialize", fmt.Sprintf("HTTP %d", resp.StatusCode), nil) + return "", core.E("mcpInitialize", fmt.Sprintf("HTTP %d", resp.StatusCode), nil) } sessionID := resp.Header.Get("Mcp-Session-Id") @@ -77,18 +77,18 @@ func mcpInitialize(ctx context.Context, client *http.Client, url, token string) func mcpCall(ctx context.Context, client *http.Client, url, token, sessionID string, body []byte) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body)) if err != nil { - return nil, coreerr.E("mcpCall", "create request", err) + return nil, core.E("mcpCall", "create request", err) } setHeaders(req, token, sessionID) resp, err := client.Do(req) if err != nil { - return nil, coreerr.E("mcpCall", "request failed", err) + return nil, core.E("mcpCall", "request failed", err) } defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, coreerr.E("mcpCall", fmt.Sprintf("HTTP %d", resp.StatusCode), nil) + return nil, core.E("mcpCall", fmt.Sprintf("HTTP %d", resp.StatusCode), nil) } // Parse SSE response — extract data: lines @@ -104,7 +104,7 @@ func readSSEData(resp *http.Response) ([]byte, error) { return []byte(strings.TrimPrefix(line, "data: ")), nil } } - return nil, coreerr.E("readSSEData", "no data in SSE response", nil) + return nil, core.E("readSSEData", "no data in SSE response", nil) } // setHeaders applies standard MCP HTTP headers. diff --git a/pkg/agentic/remote_status.go b/pkg/agentic/remote_status.go index 482dd0a..379b4cf 100644 --- a/pkg/agentic/remote_status.go +++ b/pkg/agentic/remote_status.go @@ -8,7 +8,7 @@ import ( "net/http" "time" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -37,7 +37,7 @@ func (s *PrepSubsystem) registerRemoteStatusTool(server *mcp.Server) { func (s *PrepSubsystem) statusRemote(ctx context.Context, _ *mcp.CallToolRequest, input RemoteStatusInput) (*mcp.CallToolResult, RemoteStatusOutput, error) { if input.Host == "" { - return nil, RemoteStatusOutput{}, coreerr.E("statusRemote", "host is required", nil) + return nil, RemoteStatusOutput{}, core.E("statusRemote", "host is required", nil) } addr := resolveHost(input.Host) diff --git a/pkg/agentic/resume.go b/pkg/agentic/resume.go index 9422502..20a54f7 100644 --- a/pkg/agentic/resume.go +++ b/pkg/agentic/resume.go @@ -8,8 +8,7 @@ import ( "os" "path/filepath" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -40,7 +39,7 @@ func (s *PrepSubsystem) registerResumeTool(server *mcp.Server) { func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, input ResumeInput) (*mcp.CallToolResult, ResumeOutput, error) { if input.Workspace == "" { - return nil, ResumeOutput{}, coreerr.E("resume", "workspace is required", nil) + return nil, ResumeOutput{}, core.E("resume", "workspace is required", nil) } wsDir := filepath.Join(WorkspaceRoot(), input.Workspace) @@ -48,17 +47,17 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu // Verify workspace exists if _, err := os.Stat(srcDir); err != nil { - return nil, ResumeOutput{}, coreerr.E("resume", "workspace not found: "+input.Workspace, nil) + return nil, ResumeOutput{}, core.E("resume", "workspace not found: "+input.Workspace, nil) } // Read current status st, err := readStatus(wsDir) if err != nil { - return nil, ResumeOutput{}, coreerr.E("resume", "no status.json in workspace", err) + return nil, ResumeOutput{}, core.E("resume", "no status.json in workspace", err) } if st.Status != "blocked" && st.Status != "failed" && st.Status != "completed" { - return nil, ResumeOutput{}, coreerr.E("resume", "workspace is "+st.Status+", not resumable (must be blocked, failed, or completed)", nil) + return nil, ResumeOutput{}, core.E("resume", "workspace is "+st.Status+", not resumable (must be blocked, failed, or completed)", nil) } // Determine agent @@ -71,8 +70,8 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu if input.Answer != "" { answerPath := filepath.Join(srcDir, "ANSWER.md") content := fmt.Sprintf("# Answer\n\n%s\n", input.Answer) - if err := coreio.Local.Write(answerPath, content); err != nil { - return nil, ResumeOutput{}, coreerr.E("resume", "failed to write ANSWER.md", err) + if r := fs.Write(answerPath, content); !r.OK { + return nil, ResumeOutput{}, core.E("resume", "failed to write ANSWER.md", nil) } } diff --git a/pkg/agentic/review_queue.go b/pkg/agentic/review_queue.go index 515722a..1eae089 100644 --- a/pkg/agentic/review_queue.go +++ b/pkg/agentic/review_queue.go @@ -14,8 +14,7 @@ import ( "strings" "time" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -223,7 +222,7 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer // Save findings for agent dispatch findingsFile := filepath.Join(repoDir, ".core", "coderabbit-findings.txt") - coreio.Local.Write(findingsFile, output) + fs.Write(findingsFile, output) // Dispatch fix agent with the findings task := fmt.Sprintf("Fix CodeRabbit findings. The review output is in .core/coderabbit-findings.txt. "+ @@ -248,7 +247,7 @@ func (s *PrepSubsystem) pushAndMerge(ctx context.Context, repoDir, repo string) pushCmd := exec.CommandContext(ctx, "git", "push", "github", "HEAD:refs/heads/dev", "--force") pushCmd.Dir = repoDir if out, err := pushCmd.CombinedOutput(); err != nil { - return coreerr.E("pushAndMerge", "push failed: "+string(out), err) + return core.E("pushAndMerge", "push failed: "+string(out), err) } // Mark PR ready if draft @@ -260,7 +259,7 @@ func (s *PrepSubsystem) pushAndMerge(ctx context.Context, repoDir, repo string) mergeCmd := exec.CommandContext(ctx, "gh", "pr", "merge", "--merge", "--delete-branch") mergeCmd.Dir = repoDir if out, err := mergeCmd.CombinedOutput(); err != nil { - return coreerr.E("pushAndMerge", "merge failed: "+string(out), err) + return core.E("pushAndMerge", "merge failed: "+string(out), err) } return nil @@ -279,7 +278,7 @@ func (s *PrepSubsystem) dispatchFixFromQueue(ctx context.Context, repo, task str return err } if !out.Success { - return coreerr.E("dispatchFixFromQueue", "dispatch failed for "+repo, nil) + return core.E("dispatchFixFromQueue", "dispatch failed for "+repo, nil) } return nil } @@ -336,13 +335,13 @@ func (s *PrepSubsystem) buildReviewCommand(ctx context.Context, repoDir, reviewe func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string) { home, _ := os.UserHomeDir() dataDir := filepath.Join(home, ".core", "training", "reviews") - coreio.Local.EnsureDir(dataDir) + fs.EnsureDir(dataDir) timestamp := time.Now().Format("2006-01-02T15-04-05") filename := fmt.Sprintf("%s_%s_%s.txt", repo, reviewer, timestamp) // Write raw output - coreio.Local.Write(filepath.Join(dataDir, filename), output) + fs.Write(filepath.Join(dataDir, filename), output) // Append to JSONL for structured training entry := map[string]string{ @@ -370,19 +369,19 @@ func (s *PrepSubsystem) saveRateLimitState(info *RateLimitInfo) { home, _ := os.UserHomeDir() path := filepath.Join(home, ".core", "coderabbit-ratelimit.json") data, _ := json.Marshal(info) - coreio.Local.Write(path, string(data)) + fs.Write(path, string(data)) } // loadRateLimitState reads persisted rate limit info. func (s *PrepSubsystem) loadRateLimitState() *RateLimitInfo { home, _ := os.UserHomeDir() path := filepath.Join(home, ".core", "coderabbit-ratelimit.json") - data, err := coreio.Local.Read(path) - if err != nil { + r := fs.Read(path) + if !r.OK { return nil } var info RateLimitInfo - if json.Unmarshal([]byte(data), &info) != nil { + if json.Unmarshal([]byte(r.Value.(string)), &info) != nil { return nil } return &info diff --git a/pkg/agentic/scan.go b/pkg/agentic/scan.go index c466605..cd7450f 100644 --- a/pkg/agentic/scan.go +++ b/pkg/agentic/scan.go @@ -9,7 +9,7 @@ import ( "net/http" "strings" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -39,7 +39,7 @@ type ScanIssue struct { func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input ScanInput) (*mcp.CallToolResult, ScanOutput, error) { if s.forgeToken == "" { - return nil, ScanOutput{}, coreerr.E("scan", "no Forge token configured", nil) + return nil, ScanOutput{}, core.E("scan", "no Forge token configured", nil) } if input.Org == "" { @@ -107,18 +107,18 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, u := fmt.Sprintf("%s/api/v1/orgs/%s/repos?limit=50&page=%d", s.forgeURL, org, page) req, err := http.NewRequestWithContext(ctx, "GET", u, nil) if err != nil { - return nil, coreerr.E("scan.listOrgRepos", "failed to create request", err) + return nil, core.E("scan.listOrgRepos", "failed to create request", err) } req.Header.Set("Authorization", "token "+s.forgeToken) resp, err := s.client.Do(req) if err != nil { - return nil, coreerr.E("scan.listOrgRepos", "failed to list repos", err) + return nil, core.E("scan.listOrgRepos", "failed to list repos", err) } if resp.StatusCode != 200 { resp.Body.Close() - return nil, coreerr.E("scan.listOrgRepos", fmt.Sprintf("HTTP %d listing repos", resp.StatusCode), nil) + return nil, core.E("scan.listOrgRepos", fmt.Sprintf("HTTP %d listing repos", resp.StatusCode), nil) } var repos []struct { @@ -148,18 +148,18 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str } req, err := http.NewRequestWithContext(ctx, "GET", u, nil) if err != nil { - return nil, coreerr.E("scan.listRepoIssues", "failed to create request", err) + return nil, core.E("scan.listRepoIssues", "failed to create request", err) } req.Header.Set("Authorization", "token "+s.forgeToken) resp, err := s.client.Do(req) if err != nil { - return nil, coreerr.E("scan.listRepoIssues", "failed to list issues for "+repo, err) + return nil, core.E("scan.listRepoIssues", "failed to list issues for "+repo, err) } defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, coreerr.E("scan.listRepoIssues", fmt.Sprintf("HTTP %d listing issues for %s", resp.StatusCode, repo), nil) + return nil, core.E("scan.listRepoIssues", fmt.Sprintf("HTTP %d listing issues for %s", resp.StatusCode, repo), nil) } var issues []struct { diff --git a/pkg/agentic/status.go b/pkg/agentic/status.go index 331aaf0..e5e4036 100644 --- a/pkg/agentic/status.go +++ b/pkg/agentic/status.go @@ -12,8 +12,7 @@ import ( "syscall" "time" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -31,6 +30,9 @@ import ( // running → failed (agent crashed / non-zero exit) // WorkspaceStatus represents the current state of an agent workspace. +// +// st, err := readStatus(wsDir) +// if err == nil && st.Status == "completed" { autoCreatePR(wsDir) } type WorkspaceStatus struct { Status string `json:"status"` // running, completed, blocked, failed Agent string `json:"agent"` // gemini, claude, codex @@ -53,16 +55,19 @@ func writeStatus(wsDir string, status *WorkspaceStatus) error { if err != nil { return err } - return coreio.Local.Write(filepath.Join(wsDir, "status.json"), string(data)) + if r := fs.Write(filepath.Join(wsDir, "status.json"), string(data)); !r.OK { + return core.E("writeStatus", "failed to write status", nil) + } + return nil } func readStatus(wsDir string) (*WorkspaceStatus, error) { - data, err := coreio.Local.Read(filepath.Join(wsDir, "status.json")) - if err != nil { - return nil, err + r := fs.Read(filepath.Join(wsDir, "status.json")) + if !r.OK { + return nil, core.E("readStatus", "status not found", nil) } var s WorkspaceStatus - if err := json.Unmarshal([]byte(data), &s); err != nil { + if err := json.Unmarshal([]byte(r.Value.(string)), &s); err != nil { return nil, err } return &s, nil @@ -102,7 +107,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu entries, err := os.ReadDir(wsRoot) if err != nil { - return nil, StatusOutput{}, coreerr.E("status", "no workspaces found", err) + return nil, StatusOutput{}, core.E("status", "no workspaces found", err) } var workspaces []WorkspaceInfo @@ -152,16 +157,16 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu if err := syscall.Kill(st.PID, 0); err != nil { // Process died — check for BLOCKED.md blockedPath := filepath.Join(wsDir, "src", "BLOCKED.md") - if data, err := coreio.Local.Read(blockedPath); err == nil { + if r := fs.Read(blockedPath); r.OK { info.Status = "blocked" - info.Question = strings.TrimSpace(data) + info.Question = strings.TrimSpace(r.Value.(string)) st.Status = "blocked" st.Question = info.Question } else { // Dead PID without BLOCKED.md — check exit code from log // If no evidence of success, mark as failed (not completed) logFile := filepath.Join(wsDir, fmt.Sprintf("agent-%s.log", st.Agent)) - if _, err := coreio.Local.Read(logFile); err != nil { + if r := fs.Read(logFile); !r.OK { info.Status = "failed" st.Status = "failed" st.Question = "Agent process died (no output log)" diff --git a/pkg/agentic/status_test.go b/pkg/agentic/status_test.go index 9a53404..0a85ca7 100644 --- a/pkg/agentic/status_test.go +++ b/pkg/agentic/status_test.go @@ -7,8 +7,6 @@ import ( "path/filepath" "testing" "time" - - coreio "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -28,12 +26,12 @@ func TestWriteStatus_Good(t *testing.T) { err := writeStatus(dir, status) require.NoError(t, err) - // Verify file was written via coreio - data, err := coreio.Local.Read(filepath.Join(dir, "status.json")) - require.NoError(t, err) + // Verify file was written via core.Fs + r := fs.Read(filepath.Join(dir, "status.json")) + require.True(t, r.OK) var read WorkspaceStatus - err = json.Unmarshal([]byte(data), &read) + err = json.Unmarshal([]byte(r.Value.(string)), &read) require.NoError(t, err) assert.Equal(t, "running", read.Status) @@ -77,7 +75,7 @@ func TestReadStatus_Good(t *testing.T) { data, err := json.MarshalIndent(status, "", " ") require.NoError(t, err) - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), string(data))) + require.True(t, fs.Write(filepath.Join(dir, "status.json"), string(data)).OK) read, err := readStatus(dir) require.NoError(t, err) @@ -99,7 +97,7 @@ func TestReadStatus_Bad_NoFile(t *testing.T) { func TestReadStatus_Bad_InvalidJSON(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), "not json{")) + require.True(t, fs.Write(filepath.Join(dir, "status.json"), "not json{").OK) _, err := readStatus(dir) assert.Error(t, err) @@ -117,7 +115,7 @@ func TestReadStatus_Good_BlockedWithQuestion(t *testing.T) { data, err := json.MarshalIndent(status, "", " ") require.NoError(t, err) - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), string(data))) + require.True(t, fs.Write(filepath.Join(dir, "status.json"), string(data)).OK) read, err := readStatus(dir) require.NoError(t, err) @@ -177,7 +175,7 @@ func TestWriteStatus_Good_OverwriteExisting(t *testing.T) { func TestReadStatus_Ugly_EmptyFile(t *testing.T) { dir := t.TempDir() - require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), "")) + require.True(t, fs.Write(filepath.Join(dir, "status.json"), "").OK) _, err := readStatus(dir) assert.Error(t, err) diff --git a/pkg/agentic/verify.go b/pkg/agentic/verify.go index 2a2f2ce..a327ea8 100644 --- a/pkg/agentic/verify.go +++ b/pkg/agentic/verify.go @@ -3,6 +3,7 @@ package agentic import ( + core "dappco.re/go/core" "bytes" "context" "encoding/json" @@ -14,8 +15,6 @@ import ( "strings" "time" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" ) // autoVerifyAndMerge runs inline tests (fast gate) and merges if they pass. @@ -112,7 +111,7 @@ func (s *PrepSubsystem) attemptVerifyAndMerge(srcDir, org, repo, branch string, // rebaseBranch rebases the current branch onto the default branch and force-pushes. func (s *PrepSubsystem) rebaseBranch(srcDir, branch string) bool { - base := gitDefaultBranch(srcDir) + base := DefaultBranch(srcDir) // Fetch latest default branch fetch := exec.Command("git", "fetch", "origin", base) @@ -282,15 +281,15 @@ func (s *PrepSubsystem) runPHPTests(srcDir string) verifyResult { } func (s *PrepSubsystem) runNodeTests(srcDir string) verifyResult { - data, err := coreio.Local.Read(filepath.Join(srcDir, "package.json")) - if err != nil { + r := fs.Read(filepath.Join(srcDir, "package.json")) + if !r.OK { return verifyResult{passed: true, testCmd: "none", output: "Could not read package.json"} } var pkg struct { Scripts map[string]string `json:"scripts"` } - if json.Unmarshal([]byte(data), &pkg) != nil || pkg.Scripts["test"] == "" { + if json.Unmarshal([]byte(r.Value.(string)), &pkg) != nil || pkg.Scripts["test"] == "" { return verifyResult{passed: true, testCmd: "none", output: "No test script in package.json"} } @@ -325,7 +324,7 @@ func (s *PrepSubsystem) forgeMergePR(ctx context.Context, org, repo string, prNu resp, err := s.client.Do(req) if err != nil { - return coreerr.E("forgeMergePR", "request failed", err) + return core.E("forgeMergePR", "request failed", err) } defer resp.Body.Close() @@ -333,7 +332,7 @@ func (s *PrepSubsystem) forgeMergePR(ctx context.Context, org, repo string, prNu var errBody map[string]any json.NewDecoder(resp.Body).Decode(&errBody) msg, _ := errBody["message"].(string) - return coreerr.E("forgeMergePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) + return core.E("forgeMergePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) } return nil @@ -352,5 +351,5 @@ func extractPRNumber(prURL string) int { // fileExists checks if a file exists. func fileExists(path string) bool { - return coreio.Local.IsFile(path) + return fs.IsFile(path) } diff --git a/pkg/agentic/watch.go b/pkg/agentic/watch.go index 6fc9999..1b3952a 100644 --- a/pkg/agentic/watch.go +++ b/pkg/agentic/watch.go @@ -8,7 +8,7 @@ import ( "path/filepath" "time" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -99,7 +99,7 @@ func (s *PrepSubsystem) watch(ctx context.Context, req *mcp.CallToolRequest, inp select { case <-ctx.Done(): - return nil, WatchOutput{}, coreerr.E("watch", "cancelled", ctx.Err()) + return nil, WatchOutput{}, core.E("watch", "cancelled", ctx.Err()) case <-time.After(pollInterval): } diff --git a/pkg/brain/brain.go b/pkg/brain/brain.go index 543d613..0e5998c 100644 --- a/pkg/brain/brain.go +++ b/pkg/brain/brain.go @@ -6,15 +6,30 @@ package brain import ( "context" + "unsafe" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "forge.lthn.ai/core/mcp/pkg/mcp/ide" "github.com/modelcontextprotocol/go-sdk/mcp" ) +// fs provides unrestricted filesystem access (root "/" = no sandbox). +// +// r := fs.Read(filepath.Join(home, ".claude", "brain.key")) +// if r.OK { apiKey = strings.TrimSpace(r.Value.(string)) } +var fs = newFs("/") + +// newFs creates a core.Fs with the given root directory. +func newFs(root string) *core.Fs { + type fsRoot struct{ root string } + f := &core.Fs{} + (*fsRoot)(unsafe.Pointer(f)).root = root + return f +} + // errBridgeNotAvailable is returned when a tool requires the Laravel bridge // but it has not been initialised (headless mode). -var errBridgeNotAvailable = coreerr.E("brain", "bridge not available", nil) +var errBridgeNotAvailable = core.E("brain", "bridge not available", nil) // Subsystem implements mcp.Subsystem for OpenBrain knowledge store operations. // It proxies brain_* tool calls to the Laravel backend via the shared IDE bridge. diff --git a/pkg/brain/direct.go b/pkg/brain/direct.go index bb5a195..7907d7a 100644 --- a/pkg/brain/direct.go +++ b/pkg/brain/direct.go @@ -14,9 +14,8 @@ import ( "strings" "time" + core "dappco.re/go/core" "dappco.re/go/agent/pkg/agentic" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -28,6 +27,9 @@ func agentName() string { // DirectSubsystem implements mcp.Subsystem for OpenBrain via direct HTTP calls. // Unlike Subsystem (which uses the IDE WebSocket bridge), this calls the // Laravel API directly — suitable for standalone core-mcp usage. +// +// sub := brain.NewDirect() +// sub.RegisterTools(server) type DirectSubsystem struct { apiURL string apiKey string @@ -46,8 +48,8 @@ func NewDirect() *DirectSubsystem { apiKey := os.Getenv("CORE_BRAIN_KEY") if apiKey == "" { home, _ := os.UserHomeDir() - if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil { - apiKey = strings.TrimSpace(data) + if r := fs.Read(filepath.Join(home, ".claude", "brain.key")); r.OK { + apiKey = strings.TrimSpace(r.Value.(string)) } } @@ -87,21 +89,21 @@ func (s *DirectSubsystem) Shutdown(_ context.Context) error { return nil } func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body any) (map[string]any, error) { if s.apiKey == "" { - return nil, coreerr.E("brain.apiCall", "no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)", nil) + return nil, core.E("brain.apiCall", "no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)", nil) } var reqBody io.Reader if body != nil { data, err := json.Marshal(body) if err != nil { - return nil, coreerr.E("brain.apiCall", "marshal request", err) + return nil, core.E("brain.apiCall", "marshal request", err) } reqBody = bytes.NewReader(data) } req, err := http.NewRequestWithContext(ctx, method, s.apiURL+path, reqBody) if err != nil { - return nil, coreerr.E("brain.apiCall", "create request", err) + return nil, core.E("brain.apiCall", "create request", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") @@ -109,22 +111,22 @@ func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body resp, err := s.client.Do(req) if err != nil { - return nil, coreerr.E("brain.apiCall", "API call failed", err) + return nil, core.E("brain.apiCall", "API call failed", err) } defer resp.Body.Close() respData, err := io.ReadAll(resp.Body) if err != nil { - return nil, coreerr.E("brain.apiCall", "read response", err) + return nil, core.E("brain.apiCall", "read response", err) } if resp.StatusCode >= 400 { - return nil, coreerr.E("brain.apiCall", fmt.Sprintf("API returned %d: %s", resp.StatusCode, string(respData)), nil) + return nil, core.E("brain.apiCall", fmt.Sprintf("API returned %d: %s", resp.StatusCode, string(respData)), nil) } var result map[string]any if err := json.Unmarshal(respData, &result); err != nil { - return nil, coreerr.E("brain.apiCall", "parse response", err) + return nil, core.E("brain.apiCall", "parse response", err) } return result, nil diff --git a/pkg/brain/direct_test.go b/pkg/brain/direct_test.go index d26b14b..b87c08d 100644 --- a/pkg/brain/direct_test.go +++ b/pkg/brain/direct_test.go @@ -9,8 +9,6 @@ import ( "net/http/httptest" "path/filepath" "testing" - - coreio "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -63,8 +61,8 @@ func TestNewDirect_Good_KeyFromFile(t *testing.T) { tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) keyDir := filepath.Join(tmpHome, ".claude") - require.NoError(t, coreio.Local.EnsureDir(keyDir)) - require.NoError(t, coreio.Local.Write(filepath.Join(keyDir, "brain.key"), " file-key-456 \n")) + require.True(t, fs.EnsureDir(keyDir).OK) + require.True(t, fs.Write(filepath.Join(keyDir, "brain.key"), " file-key-456 \n").OK) sub := NewDirect() assert.Equal(t, "file-key-456", sub.apiKey) diff --git a/pkg/brain/messaging.go b/pkg/brain/messaging.go index ad5e3a6..eb5148e 100644 --- a/pkg/brain/messaging.go +++ b/pkg/brain/messaging.go @@ -7,7 +7,7 @@ import ( "fmt" "net/url" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -75,7 +75,7 @@ type ConversationOutput struct { func (s *DirectSubsystem) sendMessage(ctx context.Context, _ *mcp.CallToolRequest, input SendInput) (*mcp.CallToolResult, SendOutput, error) { if input.To == "" || input.Content == "" { - return nil, SendOutput{}, coreerr.E("brain.sendMessage", "to and content are required", nil) + return nil, SendOutput{}, core.E("brain.sendMessage", "to and content are required", nil) } result, err := s.apiCall(ctx, "POST", "/v1/messages/send", map[string]any{ @@ -116,7 +116,7 @@ func (s *DirectSubsystem) inbox(ctx context.Context, _ *mcp.CallToolRequest, inp func (s *DirectSubsystem) conversation(ctx context.Context, _ *mcp.CallToolRequest, input ConversationInput) (*mcp.CallToolResult, ConversationOutput, error) { if input.Agent == "" { - return nil, ConversationOutput{}, coreerr.E("brain.conversation", "agent is required", nil) + return nil, ConversationOutput{}, core.E("brain.conversation", "agent is required", nil) } result, err := s.apiCall(ctx, "GET", "/v1/messages/conversation/"+url.PathEscape(input.Agent)+"?me="+url.QueryEscape(agentName()), nil) diff --git a/pkg/brain/tools.go b/pkg/brain/tools.go index 134c5ce..ae7f74b 100644 --- a/pkg/brain/tools.go +++ b/pkg/brain/tools.go @@ -6,7 +6,7 @@ import ( "context" "time" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "forge.lthn.ai/core/mcp/pkg/mcp/ide" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -140,7 +140,7 @@ func (s *Subsystem) brainRemember(_ context.Context, _ *mcp.CallToolRequest, inp }, }) if err != nil { - return nil, RememberOutput{}, coreerr.E("brain.remember", "failed to send brain_remember", err) + return nil, RememberOutput{}, core.E("brain.remember", "failed to send brain_remember", err) } return nil, RememberOutput{ @@ -163,7 +163,7 @@ func (s *Subsystem) brainRecall(_ context.Context, _ *mcp.CallToolRequest, input }, }) if err != nil { - return nil, RecallOutput{}, coreerr.E("brain.recall", "failed to send brain_recall", err) + return nil, RecallOutput{}, core.E("brain.recall", "failed to send brain_recall", err) } return nil, RecallOutput{ @@ -185,7 +185,7 @@ func (s *Subsystem) brainForget(_ context.Context, _ *mcp.CallToolRequest, input }, }) if err != nil { - return nil, ForgetOutput{}, coreerr.E("brain.forget", "failed to send brain_forget", err) + return nil, ForgetOutput{}, core.E("brain.forget", "failed to send brain_forget", err) } return nil, ForgetOutput{ @@ -210,7 +210,7 @@ func (s *Subsystem) brainList(_ context.Context, _ *mcp.CallToolRequest, input L }, }) if err != nil { - return nil, ListOutput{}, coreerr.E("brain.list", "failed to send brain_list", err) + return nil, ListOutput{}, core.E("brain.list", "failed to send brain_list", err) } return nil, ListOutput{ diff --git a/pkg/monitor/harvest.go b/pkg/monitor/harvest.go index 642b2d4..8f9617d 100644 --- a/pkg/monitor/harvest.go +++ b/pkg/monitor/harvest.go @@ -18,9 +18,8 @@ import ( "path/filepath" "strings" + core "dappco.re/go/core" "dappco.re/go/agent/pkg/agentic" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" ) // harvestResult tracks what happened during harvest. @@ -81,8 +80,8 @@ func (m *Subsystem) harvestCompleted() string { // harvestWorkspace checks a single workspace and pushes if ready. func (m *Subsystem) harvestWorkspace(wsDir string) *harvestResult { - data, err := coreio.Local.Read(filepath.Join(wsDir, "status.json")) - if err != nil { + r := fs.Read(filepath.Join(wsDir, "status.json")) + if !r.OK { return nil } @@ -91,7 +90,7 @@ func (m *Subsystem) harvestWorkspace(wsDir string) *harvestResult { Repo string `json:"repo"` Branch string `json:"branch"` } - if json.Unmarshal([]byte(data), &st) != nil { + if json.Unmarshal([]byte(r.Value.(string)), &st) != nil { return nil } @@ -262,19 +261,19 @@ func pushBranch(srcDir, branch string) error { cmd.Dir = srcDir out, err := cmd.CombinedOutput() if err != nil { - return coreerr.E("harvest.pushBranch", strings.TrimSpace(string(out)), err) + return core.E("harvest.pushBranch", strings.TrimSpace(string(out)), err) } return nil } // updateStatus updates the workspace status.json. func updateStatus(wsDir, status, question string) { - data, err := coreio.Local.Read(filepath.Join(wsDir, "status.json")) - if err != nil { + r := fs.Read(filepath.Join(wsDir, "status.json")) + if !r.OK { return } var st map[string]any - if json.Unmarshal([]byte(data), &st) != nil { + if json.Unmarshal([]byte(r.Value.(string)), &st) != nil { return } st["status"] = status @@ -284,5 +283,5 @@ func updateStatus(wsDir, status, question string) { delete(st, "question") // clear stale question from previous state } updated, _ := json.MarshalIndent(st, "", " ") - coreio.Local.Write(filepath.Join(wsDir, "status.json"), string(updated)) + fs.Write(filepath.Join(wsDir, "status.json"), string(updated)) } diff --git a/pkg/monitor/monitor.go b/pkg/monitor/monitor.go index 4f14c14..29b03ac 100644 --- a/pkg/monitor/monitor.go +++ b/pkg/monitor/monitor.go @@ -21,12 +21,17 @@ import ( "sync" "time" + core "dappco.re/go/core" "dappco.re/go/agent/pkg/agentic" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) +// fs provides unrestricted filesystem access (root "/" = no sandbox). +// +// r := fs.Read(filepath.Join(wsRoot, name, "status.json")) +// if r.OK { json.Unmarshal([]byte(r.Value.(string)), &st) } +var fs = agentic.LocalFs() + // ChannelNotifier pushes events to connected MCP sessions. // Matches the Notifier interface in core/mcp without importing it. type ChannelNotifier interface { @@ -34,6 +39,10 @@ type ChannelNotifier interface { } // Subsystem implements mcp.Subsystem for background monitoring. +// +// mon := monitor.New(monitor.Options{Interval: 2 * time.Minute}) +// mon.SetNotifier(notifier) +// mon.Start(ctx) type Subsystem struct { server *mcp.Server notifier ChannelNotifier @@ -222,8 +231,8 @@ func (m *Subsystem) checkCompletions() string { m.mu.Lock() seeded := m.completionsSeeded for _, entry := range entries { - data, err := coreio.Local.Read(entry) - if err != nil { + r := fs.Read(entry) + if !r.OK { continue } var st struct { @@ -231,7 +240,7 @@ func (m *Subsystem) checkCompletions() string { Repo string `json:"repo"` Agent string `json:"agent"` } - if json.Unmarshal([]byte(data), &st) != nil { + if json.Unmarshal([]byte(r.Value.(string)), &st) != nil { continue } @@ -291,11 +300,11 @@ func (m *Subsystem) checkInbox() string { if apiKeyStr == "" { home, _ := os.UserHomeDir() keyFile := filepath.Join(home, ".claude", "brain.key") - data, err := coreio.Local.Read(keyFile) - if err != nil { + r := fs.Read(keyFile) + if !r.OK { return "" } - apiKeyStr = data + apiKeyStr = r.Value.(string) } // Call the API to check inbox @@ -421,7 +430,7 @@ func (m *Subsystem) agentStatusResource(ctx context.Context, req *mcp.ReadResour wsRoot := agentic.WorkspaceRoot() entries, err := filepath.Glob(filepath.Join(wsRoot, "*/status.json")) if err != nil { - return nil, coreerr.E("monitor.agentStatus", "failed to scan workspaces", err) + return nil, core.E("monitor.agentStatus", "failed to scan workspaces", err) } type wsInfo struct { @@ -434,8 +443,8 @@ func (m *Subsystem) agentStatusResource(ctx context.Context, req *mcp.ReadResour var workspaces []wsInfo for _, entry := range entries { - data, err := coreio.Local.Read(entry) - if err != nil { + r := fs.Read(entry) + if !r.OK { continue } var st struct { @@ -444,7 +453,7 @@ func (m *Subsystem) agentStatusResource(ctx context.Context, req *mcp.ReadResour Agent string `json:"agent"` PRURL string `json:"pr_url"` } - if json.Unmarshal([]byte(data), &st) != nil { + if json.Unmarshal([]byte(r.Value.(string)), &st) != nil { continue } workspaces = append(workspaces, wsInfo{ diff --git a/pkg/monitor/sync.go b/pkg/monitor/sync.go index ae5a964..2323cd8 100644 --- a/pkg/monitor/sync.go +++ b/pkg/monitor/sync.go @@ -14,7 +14,6 @@ import ( "time" "dappco.re/go/agent/pkg/agentic" - coreio "dappco.re/go/core/io" ) // CheckinResponse is what the API returns for an agent checkin. @@ -53,8 +52,8 @@ func (m *Subsystem) syncRepos() string { brainKey := os.Getenv("CORE_BRAIN_KEY") if brainKey == "" { home, _ := os.UserHomeDir() - if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil { - brainKey = strings.TrimSpace(data) + if r := fs.Read(filepath.Join(home, ".claude", "brain.key")); r.OK { + brainKey = strings.TrimSpace(r.Value.(string)) } } if brainKey != "" {