From 91803e32dfc775f78c7dc01183aed8bf48b1e345 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 13 Apr 2026 09:32:00 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20AX=20compliance=20sweep=20=E2=80=94?= =?UTF-8?q?=20replace=20banned=20stdlib=20imports=20with=20core=20primitiv?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath, errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim, core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(), core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives. Framework boundary exceptions preserved where stdlib types are required by external interfaces (Gin, net/http, CGo, Wails, bubbletea). Co-Authored-By: Virgil --- pkg/mcp/agentic/dispatch.go | 22 ++-- pkg/mcp/agentic/ingest.go | 12 +- pkg/mcp/agentic/mirror.go | 9 +- pkg/mcp/agentic/plan.go | 37 +++--- pkg/mcp/agentic/pr.go | 36 +++--- pkg/mcp/agentic/prep.go | 187 +++++++++++++++++-------------- pkg/mcp/agentic/queue.go | 53 ++++++--- pkg/mcp/agentic/repo_helpers.go | 27 +++-- pkg/mcp/agentic/resume.go | 20 ++-- pkg/mcp/agentic/review_queue.go | 32 +++--- pkg/mcp/agentic/scan.go | 3 +- pkg/mcp/agentic/status.go | 3 + pkg/mcp/agentic/write_atomic.go | 3 + pkg/mcp/brain/direct.go | 142 ++++++++++++----------- pkg/mcp/mcp.go | 6 +- pkg/mcp/notify.go | 6 +- pkg/mcp/process_notifications.go | 8 +- pkg/mcp/transport_http.go | 3 +- 18 files changed, 318 insertions(+), 291 deletions(-) diff --git a/pkg/mcp/agentic/dispatch.go b/pkg/mcp/agentic/dispatch.go index 48985b4..349d3ae 100644 --- a/pkg/mcp/agentic/dispatch.go +++ b/pkg/mcp/agentic/dispatch.go @@ -4,17 +4,15 @@ package agentic import ( "context" - "fmt" "os" "os/exec" - "path/filepath" - "strings" "syscall" "time" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -54,7 +52,7 @@ func (s *PrepSubsystem) registerDispatchTool(svc *coremcp.Service) { // agentCommand returns the command and args for a given agent type. // Supports model variants: "gemini", "gemini:flash", "gemini:pro", "claude", "claude:haiku". func agentCommand(agent, prompt string) (string, []string, error) { - parts := strings.SplitN(agent, ":", 2) + parts := core.SplitN(agent, ":", 2) base := parts[0] model := "" if len(parts) > 1 { @@ -78,7 +76,7 @@ func agentCommand(agent, prompt string) (string, []string, error) { return "claude", args, nil case "local": home, _ := os.UserHomeDir() - script := filepath.Join(home, "Code", "core", "agent", "scripts", "local-agent.sh") + script := core.Path(home, "Code", "core", "agent", "scripts", "local-agent.sh") return "bash", []string{script, prompt}, nil default: return "", nil, coreerr.E("agentCommand", "unknown agent: "+agent, nil) @@ -119,14 +117,14 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, } wsDir := prepOut.WorkspaceDir - srcDir := filepath.Join(wsDir, "src") + srcDir := core.Path(wsDir, "src") // The prompt is just: read PROMPT.md and do the work prompt := "Read PROMPT.md for instructions. All context files (CLAUDE.md, TODO.md, CONTEXT.md, CONSUMERS.md, RECENT.md) are in the parent directory. Work in this directory." if input.DryRun { // Read PROMPT.md for the dry run output - promptRaw, _ := coreio.Local.Read(filepath.Join(wsDir, "PROMPT.md")) + promptRaw, _ := coreio.Local.Read(core.Path(wsDir, "PROMPT.md")) return nil, DispatchOutput{ Success: true, Agent: input.Agent, @@ -181,7 +179,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, return nil, DispatchOutput{}, err } - outputFile := filepath.Join(wsDir, fmt.Sprintf("agent-%s.log", input.Agent)) + outputFile := core.Path(wsDir, core.Sprintf("agent-%s.log", input.Agent)) outFile, err := os.Create(outputFile) if err != nil { return nil, DispatchOutput{}, coreerr.E("dispatch", "failed to create log file", err) @@ -247,7 +245,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, status := "completed" channel := coremcp.ChannelAgentComplete payload := map[string]any{ - "workspace": filepath.Base(wsDir), + "workspace": core.PathBase(wsDir), "repo": input.Repo, "org": input.Org, "agent": input.Agent, @@ -257,11 +255,11 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, // Update status to completed or blocked. if st, err := readStatus(wsDir); err == nil { st.PID = 0 - if data, err := coreio.Local.Read(filepath.Join(wsDir, "src", "BLOCKED.md")); err == nil { + if data, err := coreio.Local.Read(core.Path(wsDir, "src", "BLOCKED.md")); err == nil { status = "blocked" channel = coremcp.ChannelAgentBlocked st.Status = status - st.Question = strings.TrimSpace(data) + st.Question = core.Trim(data) if st.Question != "" { payload["question"] = st.Question } diff --git a/pkg/mcp/agentic/ingest.go b/pkg/mcp/agentic/ingest.go index 5535e67..ca87761 100644 --- a/pkg/mcp/agentic/ingest.go +++ b/pkg/mcp/agentic/ingest.go @@ -3,10 +3,8 @@ package agentic import ( - "bytes" "context" "net/http" - "os" core "dappco.re/go/core" coreio "dappco.re/go/core/io" @@ -96,26 +94,22 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p } // Read the agent API key from file - home, _ := os.UserHomeDir() + home := core.Env("HOME") apiKeyData, err := coreio.Local.Read(core.Path(home, ".claude", "agent-api.key")) if err != nil { return false } apiKey := core.Trim(apiKeyData) - r := core.JSONMarshal(map[string]string{ + payloadStr := core.JSONMarshalString(map[string]string{ "title": title, "description": description, "type": issueType, "priority": priority, "reporter": "cladius", }) - if !r.OK { - return false - } - payload := r.Value.([]byte) - req, err := http.NewRequest("POST", s.brainURL+"/v1/issues", bytes.NewReader(payload)) + req, err := http.NewRequest("POST", s.brainURL+"/v1/issues", core.NewReader(payloadStr)) if err != nil { return false } diff --git a/pkg/mcp/agentic/mirror.go b/pkg/mcp/agentic/mirror.go index 4799d1b..708c99c 100644 --- a/pkg/mcp/agentic/mirror.go +++ b/pkg/mcp/agentic/mirror.go @@ -4,12 +4,11 @@ package agentic import ( "context" - "fmt" "os/exec" - "path/filepath" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -64,7 +63,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu skipped := make([]string, 0) for _, repo := range repos { - repoDir := filepath.Join(basePath, repo) + repoDir := core.Path(basePath, repo) if !hasRemote(repoDir, "github") { skipped = append(skipped, repo+": no github remote") continue @@ -88,7 +87,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu } if files > maxFiles { - sync.Skipped = fmt.Sprintf("%d files exceeds limit of %d", files, maxFiles) + sync.Skipped = core.Sprintf("%d files exceeds limit of %d", files, maxFiles) synced = append(synced, sync) continue } diff --git a/pkg/mcp/agentic/plan.go b/pkg/mcp/agentic/plan.go index 223a2da..e872069 100644 --- a/pkg/mcp/agentic/plan.go +++ b/pkg/mcp/agentic/plan.go @@ -7,7 +7,6 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" - "strings" "time" core "dappco.re/go/core" @@ -430,33 +429,33 @@ func planPath(dir, id string) string { } func generatePlanID(title string) string { - slug := strings.Map(func(r rune) rune { - if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-' { - return r + b := core.NewBuilder() + b.Grow(len(title)) + for _, r := range title { + switch { + case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '-': + b.WriteRune(r) + case r >= 'A' && r <= 'Z': + b.WriteRune(r + 32) + case r == ' ': + b.WriteByte('-') } - if r >= 'A' && r <= 'Z' { - return r + 32 - } - if r == ' ' { - return '-' - } - return -1 - }, title) + } + slug := b.String() - // Trim consecutive dashes and cap length + // Collapse consecutive dashes and cap length for core.Contains(slug, "--") { slug = core.Replace(slug, "--", "-") } - slug = strings.Trim(slug, "-") + slug = trimDashes(slug) if len(slug) > 30 { - slug = slug[:30] + slug = trimDashes(slug[:30]) } - slug = strings.TrimRight(slug, "-") // Append short random suffix for uniqueness - b := make([]byte, 3) - rand.Read(b) - return slug + "-" + hex.EncodeToString(b) + rnd := make([]byte, 3) + rand.Read(rnd) + return slug + "-" + hex.EncodeToString(rnd) } func readPlan(dir, id string) (*Plan, error) { diff --git a/pkg/mcp/agentic/pr.go b/pkg/mcp/agentic/pr.go index 94996e9..9f624b0 100644 --- a/pkg/mcp/agentic/pr.go +++ b/pkg/mcp/agentic/pr.go @@ -6,15 +6,13 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" "os/exec" - "path/filepath" - "strings" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -66,8 +64,8 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in return nil, CreatePROutput{}, coreerr.E("createPR", "no Forge token configured", nil) } - wsDir := filepath.Join(s.workspaceRoot(), input.Workspace) - srcDir := filepath.Join(wsDir, "src") + wsDir := core.Path(s.workspaceRoot(), input.Workspace) + srcDir := core.Path(wsDir, "src") if _, err := coreio.Local.List(srcDir); err != nil { return nil, CreatePROutput{}, coreerr.E("createPR", "workspace not found: "+input.Workspace, nil) @@ -87,7 +85,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in if err != nil { return nil, CreatePROutput{}, coreerr.E("createPR", "failed to detect branch", err) } - st.Branch = strings.TrimSpace(string(out)) + st.Branch = core.Trim(string(out)) } org := st.Org @@ -105,7 +103,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in title = st.Task } if title == "" { - title = fmt.Sprintf("Agent work on %s", st.Branch) + title = core.Sprintf("Agent work on %s", st.Branch) } // Build PR body @@ -143,7 +141,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in // Comment on issue if tracked if st.Issue > 0 { - comment := fmt.Sprintf("Pull request created: %s", prURL) + comment := core.Sprintf("Pull request created: %s", prURL) s.commentOnIssue(ctx, org, st.Repo, st.Issue, comment) } @@ -159,17 +157,17 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in } func (s *PrepSubsystem) buildPRBody(st *WorkspaceStatus) string { - var b strings.Builder + b := core.NewBuilder() b.WriteString("## Summary\n\n") if st.Task != "" { b.WriteString(st.Task) b.WriteString("\n\n") } if st.Issue > 0 { - b.WriteString(fmt.Sprintf("Closes #%d\n\n", st.Issue)) + b.WriteString(core.Sprintf("Closes #%d\n\n", st.Issue)) } - b.WriteString(fmt.Sprintf("**Agent:** %s\n", st.Agent)) - b.WriteString(fmt.Sprintf("**Runs:** %d\n", st.Runs)) + b.WriteString(core.Sprintf("**Agent:** %s\n", st.Agent)) + b.WriteString(core.Sprintf("**Runs:** %d\n", st.Runs)) b.WriteString("\n---\n*Created by agentic dispatch*\n") return b.String() } @@ -185,7 +183,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base return "", 0, coreerr.E("forgeCreatePR", "failed to marshal PR payload", err) } - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls", s.forgeURL, org, repo) + url := core.Sprintf("%s/api/v1/repos/%s/%s/pulls", s.forgeURL, org, repo) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload)) if err != nil { return "", 0, coreerr.E("forgeCreatePR", "failed to build PR request", err) @@ -202,10 +200,10 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base if resp.StatusCode != 201 { var errBody map[string]any if err := json.NewDecoder(resp.Body).Decode(&errBody); err != nil { - return "", 0, coreerr.E("forgeCreatePR", fmt.Sprintf("HTTP %d with unreadable error body", resp.StatusCode), err) + return "", 0, coreerr.E("forgeCreatePR", core.Sprintf("HTTP %d with unreadable error body", resp.StatusCode), err) } msg, _ := errBody["message"].(string) - return "", 0, coreerr.E("forgeCreatePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) + return "", 0, coreerr.E("forgeCreatePR", core.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) } var pr struct { @@ -225,7 +223,7 @@ func (s *PrepSubsystem) commentOnIssue(ctx context.Context, org, repo string, is return } - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d/comments", s.forgeURL, org, repo, issue) + url := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d/comments", s.forgeURL, org, repo, issue) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload)) if err != nil { return @@ -337,7 +335,7 @@ func (s *PrepSubsystem) listPRs(ctx context.Context, _ *mcp.CallToolRequest, inp } func (s *PrepSubsystem) listRepoPRs(ctx context.Context, org, repo, state string) ([]PRInfo, error) { - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls?state=%s&limit=10", + url := core.Sprintf("%s/api/v1/repos/%s/%s/pulls?state=%s&limit=10", s.forgeURL, org, repo, state) req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req.Header.Set("Authorization", "token "+s.forgeToken) @@ -348,7 +346,7 @@ func (s *PrepSubsystem) listRepoPRs(ctx context.Context, org, repo, state string } defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, coreerr.E("listRepoPRs", fmt.Sprintf("HTTP %d for "+repo, resp.StatusCode), nil) + return nil, coreerr.E("listRepoPRs", core.Sprintf("HTTP %d for "+repo, resp.StatusCode), nil) } var prs []struct { diff --git a/pkg/mcp/agentic/prep.go b/pkg/mcp/agentic/prep.go index 4b0bcc2..71ca64e 100644 --- a/pkg/mcp/agentic/prep.go +++ b/pkg/mcp/agentic/prep.go @@ -8,18 +8,14 @@ import ( "context" "encoding/base64" "encoding/json" - "fmt" - goio "io" "net/http" - "os" "os/exec" - "path/filepath" - "strings" "time" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" "gopkg.in/yaml.v3" ) @@ -46,17 +42,17 @@ var ( // // prep := NewPrep() func NewPrep() *PrepSubsystem { - home, _ := os.UserHomeDir() + home := core.Env("HOME") - forgeToken := os.Getenv("FORGE_TOKEN") + forgeToken := core.Env("FORGE_TOKEN") if forgeToken == "" { - forgeToken = os.Getenv("GITEA_TOKEN") + forgeToken = core.Env("GITEA_TOKEN") } - brainKey := os.Getenv("CORE_BRAIN_KEY") + brainKey := core.Env("CORE_BRAIN_KEY") if brainKey == "" { - if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil { - brainKey = strings.TrimSpace(data) + if data, err := coreio.Local.Read(core.Path(home, ".claude", "brain.key")); err == nil { + brainKey = core.Trim(data) } } @@ -65,8 +61,8 @@ func NewPrep() *PrepSubsystem { forgeToken: forgeToken, brainURL: envOr("CORE_BRAIN_URL", "https://api.lthn.sh"), brainKey: brainKey, - specsPath: envOr("SPECS_PATH", filepath.Join(home, "Code", "host-uk", "specs")), - codePath: envOr("CODE_PATH", filepath.Join(home, "Code")), + specsPath: envOr("SPECS_PATH", core.Path(home, "Code", "host-uk", "specs")), + codePath: envOr("CODE_PATH", core.Path(home, "Code")), client: &http.Client{Timeout: 30 * time.Second}, } } @@ -84,24 +80,24 @@ func (s *PrepSubsystem) emitChannel(ctx context.Context, channel string, data an } func envOr(key, fallback string) string { - if v := os.Getenv(key); v != "" { + if v := core.Env(key); v != "" { return v } return fallback } func sanitizeRepoPathSegment(value, field string, allowSubdirs bool) (string, error) { - if strings.TrimSpace(value) != value { + if core.Trim(value) != value { return "", coreerr.E("prepWorkspace", field+" contains whitespace", nil) } if value == "" { return "", nil } - if strings.Contains(value, "\\") { + if core.Contains(value, "\\") { return "", coreerr.E("prepWorkspace", field+" contains invalid path separator", nil) } - parts := strings.Split(value, "/") + parts := core.Split(value, "/") if !allowSubdirs && len(parts) != 1 { return "", coreerr.E("prepWorkspace", field+" may not contain subdirectories", nil) } @@ -161,7 +157,7 @@ func (s *PrepSubsystem) Shutdown(_ context.Context) error { return nil } // workspaceRoot returns the base directory for agent workspaces. func (s *PrepSubsystem) workspaceRoot() string { - return filepath.Join(s.codePath, ".core", "workspace") + return core.Path(s.codePath, ".core", "workspace") } // --- Input/Output types --- @@ -227,8 +223,8 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques // Workspace root: .core/workspace/{repo}-{timestamp}/ wsRoot := s.workspaceRoot() coreio.Local.EnsureDir(wsRoot) - wsName := fmt.Sprintf("%s-%d", input.Repo, time.Now().Unix()) - wsDir := filepath.Join(wsRoot, wsName) + wsName := core.Sprintf("%s-%d", input.Repo, time.Now().Unix()) + wsDir := core.Path(wsRoot, wsName) // Create workspace structure // kb/ and specs/ will be created inside src/ after clone @@ -236,10 +232,10 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques out := PrepOutput{WorkspaceDir: wsDir} // Source repo path - repoPath := filepath.Join(s.codePath, "core", input.Repo) + repoPath := core.Path(s.codePath, "core", input.Repo) // 1. Clone repo into src/ and create feature branch - srcDir := filepath.Join(wsDir, "src") + srcDir := core.Path(wsDir, "src") cloneCmd := exec.CommandContext(ctx, "git", "clone", repoPath, srcDir) if err := cloneCmd.Run(); err != nil { return nil, PrepOutput{}, coreerr.E("prepWorkspace", "failed to clone repository", err) @@ -251,12 +247,12 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques taskSlug := branchSlug(input.Task) if input.Issue > 0 { issueSlug := branchSlug(input.Task) - branchName = fmt.Sprintf("agent/issue-%d", input.Issue) + branchName = core.Sprintf("agent/issue-%d", input.Issue) if issueSlug != "" { branchName += "-" + issueSlug } } else if taskSlug != "" { - branchName = fmt.Sprintf("agent/%s", taskSlug) + branchName = core.Sprintf("agent/%s", taskSlug) } } if branchName != "" { @@ -269,29 +265,29 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques } // Create context dirs inside src/ - coreio.Local.EnsureDir(filepath.Join(srcDir, "kb")) - coreio.Local.EnsureDir(filepath.Join(srcDir, "specs")) + coreio.Local.EnsureDir(core.Path(srcDir, "kb")) + coreio.Local.EnsureDir(core.Path(srcDir, "specs")) // Remote stays as local clone origin — agent cannot push to forge. // Reviewer pulls changes from workspace and pushes after verification. // 2. Copy CLAUDE.md and GEMINI.md to workspace - claudeMdPath := filepath.Join(repoPath, "CLAUDE.md") + claudeMdPath := core.Path(repoPath, "CLAUDE.md") if data, err := coreio.Local.Read(claudeMdPath); err == nil { - _ = writeAtomic(filepath.Join(wsDir, "src", "CLAUDE.md"), data) + _ = writeAtomic(core.Path(wsDir, "src", "CLAUDE.md"), data) out.ClaudeMd = true } // Copy GEMINI.md from core/agent (ethics framework for all agents) - agentGeminiMd := filepath.Join(s.codePath, "core", "agent", "GEMINI.md") + agentGeminiMd := core.Path(s.codePath, "core", "agent", "GEMINI.md") if data, err := coreio.Local.Read(agentGeminiMd); err == nil { - _ = writeAtomic(filepath.Join(wsDir, "src", "GEMINI.md"), data) + _ = writeAtomic(core.Path(wsDir, "src", "GEMINI.md"), data) } // Copy persona if specified if persona != "" { - personaPath := filepath.Join(s.codePath, "core", "agent", "prompts", "personas", persona+".md") + personaPath := core.Path(s.codePath, "core", "agent", "prompts", "personas", persona+".md") if data, err := coreio.Local.Read(personaPath); err == nil { - _ = writeAtomic(filepath.Join(wsDir, "src", "PERSONA.md"), data) + _ = writeAtomic(core.Path(wsDir, "src", "PERSONA.md"), data) } } @@ -299,9 +295,9 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques if input.Issue > 0 { s.generateTodo(ctx, input.Org, input.Repo, input.Issue, wsDir) } else if input.Task != "" { - todo := fmt.Sprintf("# TASK: %s\n\n**Repo:** %s/%s\n**Status:** ready\n\n## Objective\n\n%s\n", + todo := core.Sprintf("# TASK: %s\n\n**Repo:** %s/%s\n**Status:** ready\n\n## Objective\n\n%s\n", input.Task, input.Org, input.Repo, input.Task) - _ = writeAtomic(filepath.Join(wsDir, "src", "TODO.md"), todo) + _ = writeAtomic(core.Path(wsDir, "src", "TODO.md"), todo) } // 4. Generate CONTEXT.md from OpenBrain @@ -333,12 +329,12 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques // branchSlug converts a free-form string into a git-friendly branch suffix. func branchSlug(value string) string { - value = strings.ToLower(strings.TrimSpace(value)) + value = core.Lower(core.Trim(value)) if value == "" { return "" } - var b strings.Builder + b := core.NewBuilder() b.Grow(len(value)) lastDash := false for _, r := range value { @@ -359,14 +355,42 @@ func branchSlug(value string) string { } } - slug := strings.Trim(b.String(), "-") + slug := trimDashes(b.String()) if len(slug) > 40 { - slug = slug[:40] - slug = strings.Trim(slug, "-") + slug = trimDashes(slug[:40]) } return slug } +// sanitizeFilename replaces non-alphanumeric characters (except - _ .) with dashes. +func sanitizeFilename(title string) string { + b := core.NewBuilder() + b.Grow(len(title)) + for _, r := range title { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', + r == '-', r == '_', r == '.': + b.WriteRune(r) + default: + b.WriteByte('-') + } + } + return b.String() +} + +// trimDashes strips leading and trailing dash characters from a string. +func trimDashes(s string) string { + start := 0 + for start < len(s) && s[start] == '-' { + start++ + } + end := len(s) + for end > start && s[end-1] == '-' { + end-- + } + return s[start:end] +} + // --- Prompt templates --- func (s *PrepSubsystem) writePromptTemplate(template, wsDir string) { @@ -434,7 +458,7 @@ Do NOT push. Commit only — a reviewer will verify and push. prompt = "Read TODO.md and complete the task. Work in src/.\n" } - _ = writeAtomic(filepath.Join(wsDir, "src", "PROMPT.md"), prompt) + _ = writeAtomic(core.Path(wsDir, "src", "PROMPT.md"), prompt) } // --- Plan template rendering --- @@ -443,11 +467,11 @@ Do NOT push. Commit only — a reviewer will verify and push. // and writes PLAN.md into the workspace src/ directory. func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map[string]string, task string, wsDir string) { // Look for template in core/agent/prompts/templates/ - templatePath := filepath.Join(s.codePath, "core", "agent", "prompts", "templates", templateSlug+".yaml") + templatePath := core.Path(s.codePath, "core", "agent", "prompts", "templates", templateSlug+".yaml") content, err := coreio.Local.Read(templatePath) if err != nil { // Try .yml extension - templatePath = filepath.Join(s.codePath, "core", "agent", "prompts", "templates", templateSlug+".yml") + templatePath = core.Path(s.codePath, "core", "agent", "prompts", "templates", templateSlug+".yml") content, err = coreio.Local.Read(templatePath) if err != nil { return // Template not found, skip silently @@ -456,8 +480,8 @@ func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map // Substitute variables ({{variable_name}} → value) for key, value := range variables { - content = strings.ReplaceAll(content, "{{"+key+"}}", value) - content = strings.ReplaceAll(content, "{{ "+key+" }}", value) + content = core.Replace(content, "{{"+key+"}}", value) + content = core.Replace(content, "{{ "+key+" }}", value) } // Parse the YAML to render as markdown @@ -477,7 +501,7 @@ func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map } // Render as PLAN.md - var plan strings.Builder + plan := core.NewBuilder() plan.WriteString("# Plan: " + tmpl.Name + "\n\n") if task != "" { plan.WriteString("**Task:** " + task + "\n\n") @@ -495,7 +519,7 @@ func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map } for i, phase := range tmpl.Phases { - plan.WriteString(fmt.Sprintf("## Phase %d: %s\n\n", i+1, phase.Name)) + plan.WriteString(core.Sprintf("## Phase %d: %s\n\n", i+1, phase.Name)) if phase.Description != "" { plan.WriteString(phase.Description + "\n\n") } @@ -512,7 +536,7 @@ func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map plan.WriteString("\n**Commit after completing this phase.**\n\n---\n\n") } - _ = writeAtomic(filepath.Join(wsDir, "src", "PLAN.md"), plan.String()) + _ = writeAtomic(core.Path(wsDir, "src", "PLAN.md"), plan.String()) } // --- Helpers (unchanged) --- @@ -522,7 +546,7 @@ func (s *PrepSubsystem) pullWiki(ctx context.Context, org, repo, wsDir string) i return 0 } - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/wiki/pages", s.forgeURL, org, repo) + url := core.Sprintf("%s/api/v1/repos/%s/%s/wiki/pages", s.forgeURL, org, repo) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return 0 @@ -553,7 +577,7 @@ func (s *PrepSubsystem) pullWiki(ctx context.Context, org, repo, wsDir string) i subURL = page.Title } - pageURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/wiki/page/%s", s.forgeURL, org, repo, subURL) + pageURL := core.Sprintf("%s/api/v1/repos/%s/%s/wiki/page/%s", s.forgeURL, org, repo, subURL) pageReq, err := http.NewRequestWithContext(ctx, "GET", pageURL, nil) if err != nil { continue @@ -585,14 +609,9 @@ func (s *PrepSubsystem) pullWiki(ctx context.Context, org, repo, wsDir string) i if err != nil { continue } - filename := strings.Map(func(r rune) rune { - if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' { - return r - } - return '-' - }, page.Title) + ".md" + filename := sanitizeFilename(page.Title) + ".md" - _ = writeAtomic(filepath.Join(wsDir, "src", "kb", filename), string(content)) + _ = writeAtomic(core.Path(wsDir, "src", "kb", filename), string(content)) count++ } @@ -604,9 +623,9 @@ func (s *PrepSubsystem) copySpecs(wsDir string) int { count := 0 for _, file := range specFiles { - src := filepath.Join(s.specsPath, file) + src := core.Path(s.specsPath, file) if data, err := coreio.Local.Read(src); err == nil { - _ = writeAtomic(filepath.Join(wsDir, "src", "specs", file), data) + _ = writeAtomic(core.Path(wsDir, "src", "specs", file), data) count++ } } @@ -629,7 +648,7 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string) return 0 } - req, err := http.NewRequestWithContext(ctx, "POST", s.brainURL+"/v1/brain/recall", strings.NewReader(string(body))) + req, err := http.NewRequestWithContext(ctx, "POST", s.brainURL+"/v1/brain/recall", core.NewReader(string(body))) if err != nil { return 0 } @@ -646,18 +665,18 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string) return 0 } - respData, err := goio.ReadAll(resp.Body) - if err != nil { + readResult := core.ReadAll(resp.Body) + if !readResult.OK { return 0 } var result struct { Memories []map[string]any `json:"memories"` } - if err := json.Unmarshal(respData, &result); err != nil { + if ur := core.JSONUnmarshal([]byte(readResult.Value.(string)), &result); !ur.OK { return 0 } - var content strings.Builder + content := core.NewBuilder() content.WriteString("# Context — " + repo + "\n\n") content.WriteString("> Relevant knowledge from OpenBrain.\n\n") @@ -666,15 +685,15 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string) memContent, _ := mem["content"].(string) memProject, _ := mem["project"].(string) score, _ := mem["score"].(float64) - content.WriteString(fmt.Sprintf("### %d. %s [%s] (score: %.3f)\n\n%s\n\n", i+1, memProject, memType, score, memContent)) + content.WriteString(core.Sprintf("### %d. %s [%s] (score: %.3f)\n\n%s\n\n", i+1, memProject, memType, score, memContent)) } - _ = writeAtomic(filepath.Join(wsDir, "src", "CONTEXT.md"), content.String()) + _ = writeAtomic(core.Path(wsDir, "src", "CONTEXT.md"), content.String()) return len(result.Memories) } func (s *PrepSubsystem) findConsumers(repo, wsDir string) int { - goWorkPath := filepath.Join(s.codePath, "go.work") + goWorkPath := core.Path(s.codePath, "go.work") modulePath := "forge.lthn.ai/core/" + repo workData, err := coreio.Local.Read(goWorkPath) @@ -683,19 +702,19 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int { } var consumers []string - for _, line := range strings.Split(workData, "\n") { - line = strings.TrimSpace(line) - if !strings.HasPrefix(line, "./") { + for _, line := range core.Split(workData, "\n") { + line = core.Trim(line) + if !core.HasPrefix(line, "./") { continue } - dir := filepath.Join(s.codePath, strings.TrimPrefix(line, "./")) - goMod := filepath.Join(dir, "go.mod") + dir := core.Path(s.codePath, core.TrimPrefix(line, "./")) + goMod := core.Path(dir, "go.mod") modData, err := coreio.Local.Read(goMod) if err != nil { continue } - if strings.Contains(modData, modulePath) && !strings.HasPrefix(modData, "module "+modulePath) { - consumers = append(consumers, filepath.Base(dir)) + if core.Contains(modData, modulePath) && !core.HasPrefix(modData, "module "+modulePath) { + consumers = append(consumers, core.PathBase(dir)) } } @@ -705,8 +724,8 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int { for _, c := range consumers { content += "- " + c + "\n" } - content += fmt.Sprintf("\n**Breaking change risk: %d consumers.**\n", len(consumers)) - _ = writeAtomic(filepath.Join(wsDir, "src", "CONSUMERS.md"), content) + content += core.Sprintf("\n**Breaking change risk: %d consumers.**\n", len(consumers)) + _ = writeAtomic(core.Path(wsDir, "src", "CONSUMERS.md"), content) } return len(consumers) @@ -720,10 +739,10 @@ func (s *PrepSubsystem) gitLog(repoPath, wsDir string) int { return 0 } - lines := strings.Split(strings.TrimSpace(string(output)), "\n") + lines := core.Split(core.Trim(string(output)), "\n") if len(lines) > 0 && lines[0] != "" { content := "# Recent Changes\n\n```\n" + string(output) + "```\n" - _ = writeAtomic(filepath.Join(wsDir, "src", "RECENT.md"), content) + _ = writeAtomic(core.Path(wsDir, "src", "RECENT.md"), content) } return len(lines) @@ -734,7 +753,7 @@ func (s *PrepSubsystem) generateTodo(ctx context.Context, org, repo string, issu return } - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue) + url := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue) req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req.Header.Set("Authorization", "token "+s.forgeToken) @@ -753,11 +772,11 @@ func (s *PrepSubsystem) generateTodo(ctx context.Context, org, repo string, issu } json.NewDecoder(resp.Body).Decode(&issueData) - content := fmt.Sprintf("# TASK: %s\n\n", issueData.Title) - content += fmt.Sprintf("**Status:** ready\n") - content += fmt.Sprintf("**Source:** %s/%s/%s/issues/%d\n", s.forgeURL, org, repo, issue) - content += fmt.Sprintf("**Repo:** %s/%s\n\n---\n\n", org, repo) + content := core.Sprintf("# TASK: %s\n\n", issueData.Title) + content += core.Sprintf("**Status:** ready\n") + content += core.Sprintf("**Source:** %s/%s/%s/issues/%d\n", s.forgeURL, org, repo, issue) + content += core.Sprintf("**Repo:** %s/%s\n\n---\n\n", org, repo) content += "## Objective\n\n" + issueData.Body + "\n" - _ = writeAtomic(filepath.Join(wsDir, "src", "TODO.md"), content) + _ = writeAtomic(core.Path(wsDir, "src", "TODO.md"), content) } diff --git a/pkg/mcp/agentic/queue.go b/pkg/mcp/agentic/queue.go index ff5fe1a..a02c7b9 100644 --- a/pkg/mcp/agentic/queue.go +++ b/pkg/mcp/agentic/queue.go @@ -3,18 +3,19 @@ package agentic import ( - "fmt" "os" "os/exec" - "path/filepath" - "strings" "syscall" "time" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" "gopkg.in/yaml.v3" ) +// os.Create, os.Open, os.DevNull, os.Environ, os.FindProcess are used for +// process spawning and management — no core equivalents for these OS primitives. + // DispatchConfig controls agent dispatch behaviour. type DispatchConfig struct { DefaultAgent string `yaml:"default_agent"` @@ -43,7 +44,7 @@ type AgentsConfig struct { // loadAgentsConfig reads config/agents.yaml from the code path. func (s *PrepSubsystem) loadAgentsConfig() *AgentsConfig { paths := []string{ - filepath.Join(s.codePath, ".core", "agents.yaml"), + core.Path(s.codePath, ".core", "agents.yaml"), } for _, path := range paths { @@ -79,9 +80,16 @@ func (s *PrepSubsystem) delayForAgent(agent string) time.Duration { return 0 } - // Parse reset time + // Parse reset time (format: "HH:MM") resetHour, resetMin := 6, 0 - fmt.Sscanf(rate.ResetUTC, "%d:%d", &resetHour, &resetMin) + if parts := core.Split(rate.ResetUTC, ":"); len(parts) == 2 { + if h, ok := parseSimpleInt(parts[0]); ok { + resetHour = h + } + if m, ok := parseSimpleInt(parts[1]); ok { + resetMin = m + } + } now := time.Now().UTC() resetToday := time.Date(now.Year(), now.Month(), now.Day(), resetHour, resetMin, 0, 0, time.UTC) @@ -115,9 +123,9 @@ func (s *PrepSubsystem) listWorkspaceDirs() []string { if !entry.IsDir() { continue } - path := filepath.Join(wsRoot, entry.Name()) + path := core.Path(wsRoot, entry.Name()) // Check if this dir has a status.json (it's a workspace) - if coreio.Local.IsFile(filepath.Join(path, "status.json")) { + if coreio.Local.IsFile(core.Path(path, "status.json")) { dirs = append(dirs, path) continue } @@ -128,8 +136,8 @@ func (s *PrepSubsystem) listWorkspaceDirs() []string { } for _, sub := range subEntries { if sub.IsDir() { - subPath := filepath.Join(path, sub.Name()) - if coreio.Local.IsFile(filepath.Join(subPath, "status.json")) { + subPath := core.Path(path, sub.Name()) + if coreio.Local.IsFile(core.Path(subPath, "status.json")) { dirs = append(dirs, subPath) } } @@ -146,7 +154,7 @@ func (s *PrepSubsystem) countRunningByAgent(agent string) int { if err != nil || st.Status != "running" { continue } - stBase := strings.SplitN(st.Agent, ":", 2)[0] + stBase := core.SplitN(st.Agent, ":", 2)[0] if stBase != agent { continue } @@ -162,7 +170,7 @@ func (s *PrepSubsystem) countRunningByAgent(agent string) int { // baseAgent strips the model variant (gemini:flash → gemini). func baseAgent(agent string) string { - return strings.SplitN(agent, ":", 2)[0] + return core.SplitN(agent, ":", 2)[0] } // canDispatchAgent checks if we're under the concurrency limit for a specific agent type. @@ -176,6 +184,23 @@ func (s *PrepSubsystem) canDispatchAgent(agent string) bool { return s.countRunningByAgent(base) < limit } +// parseSimpleInt parses a small non-negative integer from a string. +// Returns (value, true) on success, (0, false) on failure. +func parseSimpleInt(s string) (int, bool) { + s = core.Trim(s) + if s == "" { + return 0, false + } + n := 0 + for _, r := range s { + if r < '0' || r > '9' { + return 0, false + } + n = n*10 + int(r-'0') + } + return n, true +} + // canDispatch is kept for backwards compat. func (s *PrepSubsystem) canDispatch() bool { return true @@ -205,7 +230,7 @@ func (s *PrepSubsystem) drainQueue() { continue } - srcDir := filepath.Join(wsDir, "src") + srcDir := core.Path(wsDir, "src") prompt := "Read PROMPT.md for instructions. All context files (CLAUDE.md, TODO.md, CONTEXT.md, CONSUMERS.md, RECENT.md) are in the parent directory. Work in this directory." command, args, err := agentCommand(st.Agent, prompt) @@ -213,7 +238,7 @@ func (s *PrepSubsystem) drainQueue() { continue } - outputFile := filepath.Join(wsDir, fmt.Sprintf("agent-%s.log", st.Agent)) + outputFile := core.Path(wsDir, core.Sprintf("agent-%s.log", st.Agent)) outFile, err := os.Create(outputFile) if err != nil { continue diff --git a/pkg/mcp/agentic/repo_helpers.go b/pkg/mcp/agentic/repo_helpers.go index d50c1b8..cb03de0 100644 --- a/pkg/mcp/agentic/repo_helpers.go +++ b/pkg/mcp/agentic/repo_helpers.go @@ -5,19 +5,18 @@ package agentic import ( "context" "encoding/json" - "os" "os/exec" - "path/filepath" "regexp" "strconv" - "strings" "time" + core "dappco.re/go/core" + coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" ) func listLocalRepos(basePath string) []string { - entries, err := os.ReadDir(basePath) + entries, err := coreio.Local.List(basePath) if err != nil { return nil } @@ -35,7 +34,7 @@ func hasRemote(repoDir, remote string) bool { cmd := exec.Command("git", "remote", "get-url", remote) cmd.Dir = repoDir if out, err := cmd.Output(); err == nil { - return strings.TrimSpace(string(out)) != "" + return core.Trim(string(out)) != "" } return false } @@ -48,7 +47,7 @@ func commitsAhead(repoDir, baseRef, headRef string) int { return 0 } - count, err := parsePositiveInt(strings.TrimSpace(string(out))) + count, err := parsePositiveInt(core.Trim(string(out))) if err != nil { return 0 } @@ -64,8 +63,8 @@ func filesChanged(repoDir, baseRef, headRef string) int { } count := 0 - for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { - if strings.TrimSpace(line) != "" { + for _, line := range core.Split(core.Trim(string(out)), "\n") { + if core.Trim(line) != "" { count++ } } @@ -79,11 +78,11 @@ func gitOutput(repoDir string, args ...string) (string, error) { if err != nil { return "", coreerr.E("gitOutput", string(out), err) } - return strings.TrimSpace(string(out)), nil + return core.Trim(string(out)), nil } func parsePositiveInt(value string) (int, error) { - value = strings.TrimSpace(value) + value = core.Trim(value) if value == "" { return 0, coreerr.E("parsePositiveInt", "empty value", nil) } @@ -148,11 +147,11 @@ func createGitHubPR(ctx context.Context, repoDir, repo string, commits, files in return "", coreerr.E("createGitHubPR", string(out), err) } - lines := strings.Split(strings.TrimSpace(string(out)), "\n") + lines := core.Split(core.Trim(string(out)), "\n") if len(lines) == 0 { return "", nil } - return strings.TrimSpace(lines[len(lines)-1]), nil + return core.Trim(lines[len(lines)-1]), nil } func ensureDevBranch(repoDir string) error { @@ -194,7 +193,7 @@ func parseRetryAfter(detail string) time.Duration { return 5 * time.Minute } - switch strings.ToLower(match[2]) { + switch core.Lower(match[2]) { case "hour", "hours": return time.Duration(n) * time.Hour case "second", "seconds": @@ -205,5 +204,5 @@ func parseRetryAfter(detail string) time.Duration { } func repoRootFromCodePath(codePath string) string { - return filepath.Join(codePath, "core") + return core.Path(codePath, "core") } diff --git a/pkg/mcp/agentic/resume.go b/pkg/mcp/agentic/resume.go index 8528da6..ac5160f 100644 --- a/pkg/mcp/agentic/resume.go +++ b/pkg/mcp/agentic/resume.go @@ -4,16 +4,14 @@ package agentic import ( "context" - "fmt" "os" "os/exec" - "path/filepath" - "strings" "syscall" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -52,8 +50,8 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu return nil, ResumeOutput{}, coreerr.E("resume", "workspace is required", nil) } - wsDir := filepath.Join(s.workspaceRoot(), input.Workspace) - srcDir := filepath.Join(wsDir, "src") + wsDir := core.Path(s.workspaceRoot(), input.Workspace) + srcDir := core.Path(wsDir, "src") // Verify workspace exists if _, err := coreio.Local.List(srcDir); err != nil { @@ -78,8 +76,8 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu // Write ANSWER.md if answer provided if input.Answer != "" { - answerPath := filepath.Join(srcDir, "ANSWER.md") - content := fmt.Sprintf("# Answer\n\n%s\n", input.Answer) + answerPath := core.Path(srcDir, "ANSWER.md") + content := core.Sprintf("# Answer\n\n%s\n", input.Answer) if err := writeAtomic(answerPath, content); err != nil { return nil, ResumeOutput{}, coreerr.E("resume", "failed to write ANSWER.md", err) } @@ -102,7 +100,7 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu } // Spawn agent as detached process (survives parent death) - outputFile := filepath.Join(wsDir, fmt.Sprintf("agent-%s-run%d.log", agent, st.Runs+1)) + outputFile := core.Path(wsDir, core.Sprintf("agent-%s-run%d.log", agent, st.Runs+1)) command, args, err := agentCommand(agent, prompt) if err != nil { @@ -154,10 +152,10 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu "branch": st.Branch, } - if data, err := coreio.Local.Read(filepath.Join(srcDir, "BLOCKED.md")); err == nil { + if data, err := coreio.Local.Read(core.Path(srcDir, "BLOCKED.md")); err == nil { status = "blocked" channel = coremcp.ChannelAgentBlocked - st.Question = strings.TrimSpace(data) + st.Question = core.Trim(data) if st.Question != "" { payload["question"] = st.Question } diff --git a/pkg/mcp/agentic/review_queue.go b/pkg/mcp/agentic/review_queue.go index df8c354..050df04 100644 --- a/pkg/mcp/agentic/review_queue.go +++ b/pkg/mcp/agentic/review_queue.go @@ -5,16 +5,14 @@ package agentic import ( "context" "encoding/json" - "fmt" "os" "os/exec" - "path/filepath" "regexp" - "strings" "time" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -93,7 +91,7 @@ func (s *PrepSubsystem) reviewQueue(ctx context.Context, _ *mcp.CallToolRequest, continue } - repoDir := filepath.Join(basePath, repo) + repoDir := core.Path(basePath, repo) reviewer := input.Reviewer if reviewer == "" { reviewer = "coderabbit" @@ -137,7 +135,7 @@ func (s *PrepSubsystem) findReviewCandidates(basePath string) []string { if !entry.IsDir() { continue } - repoDir := filepath.Join(basePath, entry.Name()) + repoDir := core.Path(basePath, entry.Name()) if !hasRemote(repoDir, "github") { continue } @@ -154,22 +152,22 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer if rl := s.loadRateLimitState(); rl != nil && rl.Limited && time.Now().Before(rl.RetryAt) { result.Verdict = "rate_limited" - result.Detail = fmt.Sprintf("retry after %s", rl.RetryAt.Format(time.RFC3339)) + result.Detail = core.Sprintf("retry after %s", rl.RetryAt.Format(time.RFC3339)) return result } cmd := reviewerCommand(ctx, repoDir, reviewer) cmd.Dir = repoDir out, err := cmd.CombinedOutput() - output := strings.TrimSpace(string(out)) + output := core.Trim(string(out)) - if strings.Contains(strings.ToLower(output), "rate limit") { + if core.Contains(core.Lower(output), "rate limit") { result.Verdict = "rate_limited" result.Detail = output return result } - if err != nil && !strings.Contains(output, "No findings") && !strings.Contains(output, "no issues") { + if err != nil && !core.Contains(output, "No findings") && !core.Contains(output, "no issues") { result.Verdict = "error" if output != "" { result.Detail = output @@ -182,7 +180,7 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer s.storeReviewOutput(repoDir, repo, reviewer, output) result.Findings = countFindingHints(output) - if strings.Contains(output, "No findings") || strings.Contains(output, "no issues") || strings.Contains(output, "LGTM") { + if core.Contains(output, "No findings") || core.Contains(output, "no issues") || core.Contains(output, "LGTM") { result.Verdict = "clean" if dryRun { result.Action = "skipped (dry run)" @@ -198,7 +196,7 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer mergeCmd.Dir = repoDir if mergeOut, err := mergeCmd.CombinedOutput(); err == nil { result.Action = "merged" - result.Detail = strings.TrimSpace(string(mergeOut)) + result.Detail = core.Trim(string(mergeOut)) return result } } @@ -219,7 +217,7 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string) { home := reviewQueueHomeDir() - dataDir := filepath.Join(home, ".core", "training", "reviews") + dataDir := core.Path(home, ".core", "training", "reviews") if err := coreio.Local.EnsureDir(dataDir); err != nil { return } @@ -235,13 +233,13 @@ func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string return } - name := fmt.Sprintf("%s-%s-%d.json", repo, reviewer, time.Now().Unix()) - _ = writeAtomic(filepath.Join(dataDir, name), string(data)) + name := core.Sprintf("%s-%s-%d.json", repo, reviewer, time.Now().Unix()) + _ = writeAtomic(core.Path(dataDir, name), string(data)) } func (s *PrepSubsystem) saveRateLimitState(info *RateLimitInfo) { home := reviewQueueHomeDir() - path := filepath.Join(home, ".core", "coderabbit-ratelimit.json") + path := core.Path(home, ".core", "coderabbit-ratelimit.json") data, err := json.Marshal(info) if err != nil { return @@ -251,7 +249,7 @@ func (s *PrepSubsystem) saveRateLimitState(info *RateLimitInfo) { func (s *PrepSubsystem) loadRateLimitState() *RateLimitInfo { home := reviewQueueHomeDir() - path := filepath.Join(home, ".core", "coderabbit-ratelimit.json") + path := core.Path(home, ".core", "coderabbit-ratelimit.json") data, err := coreio.Local.Read(path) if err != nil { return nil diff --git a/pkg/mcp/agentic/scan.go b/pkg/mcp/agentic/scan.go index 564962f..7e40f47 100644 --- a/pkg/mcp/agentic/scan.go +++ b/pkg/mcp/agentic/scan.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "net/http" - "strings" core "dappco.re/go/core" coreerr "dappco.re/go/core/log" @@ -170,7 +169,7 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str Title: issue.Title, Labels: labels, Assignee: assignee, - URL: strings.Replace(issue.HTMLURL, "https://forge.lthn.ai", s.forgeURL, 1), + URL: core.Replace(issue.HTMLURL, "https://forge.lthn.ai", s.forgeURL), }) } diff --git a/pkg/mcp/agentic/status.go b/pkg/mcp/agentic/status.go index 4f3b28b..d789ece 100644 --- a/pkg/mcp/agentic/status.go +++ b/pkg/mcp/agentic/status.go @@ -15,6 +15,9 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) +// os.Stat and os.FindProcess are used for workspace age detection and PID +// liveness checks — these are OS-level queries with no core equivalent. + // Workspace status file convention: // // {workspace}/status.json — current state of the workspace diff --git a/pkg/mcp/agentic/write_atomic.go b/pkg/mcp/agentic/write_atomic.go index 72bb9a7..8f46118 100644 --- a/pkg/mcp/agentic/write_atomic.go +++ b/pkg/mcp/agentic/write_atomic.go @@ -9,6 +9,9 @@ import ( coreio "dappco.re/go/core/io" ) +// os.CreateTemp, os.Remove, os.Rename are framework-boundary calls for +// atomic file writes — no core equivalent exists for temp file creation. + // writeAtomic writes content to path by staging it in a temporary file and // renaming it into place. // diff --git a/pkg/mcp/brain/direct.go b/pkg/mcp/brain/direct.go index acfb829..cb59d3a 100644 --- a/pkg/mcp/brain/direct.go +++ b/pkg/mcp/brain/direct.go @@ -3,20 +3,15 @@ package brain import ( - "bytes" "context" - "encoding/json" - "fmt" - goio "io" "net/http" "net/url" - "os" - "strings" "time" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -58,15 +53,16 @@ func (s *DirectSubsystem) OnChannel(fn func(ctx context.Context, channel string, // Reads CORE_BRAIN_URL and CORE_BRAIN_KEY from environment, or falls back // to ~/.claude/brain.key for the API key. func NewDirect() *DirectSubsystem { - apiURL := os.Getenv("CORE_BRAIN_URL") + apiURL := core.Env("CORE_BRAIN_URL") if apiURL == "" { apiURL = "https://api.lthn.sh" } - apiKey := os.Getenv("CORE_BRAIN_KEY") + apiKey := core.Env("CORE_BRAIN_KEY") if apiKey == "" { - if data, err := coreio.Local.Read(os.ExpandEnv("$HOME/.claude/brain.key")); err == nil { - apiKey = strings.TrimSpace(data) + home := core.Env("HOME") + if data, err := coreio.Local.Read(core.Path(home, ".claude", "brain.key")); err == nil { + apiKey = core.Trim(data) } } @@ -112,16 +108,12 @@ func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body return nil, coreerr.E("brain.apiCall", "no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)", nil) } - var reqBody goio.Reader + var bodyStr string if body != nil { - data, err := json.Marshal(body) - if err != nil { - return nil, coreerr.E("brain.apiCall", "marshal request", err) - } - reqBody = bytes.NewReader(data) + bodyStr = core.JSONMarshalString(body) } - req, err := http.NewRequestWithContext(ctx, method, s.apiURL+path, reqBody) + req, err := http.NewRequestWithContext(ctx, method, s.apiURL+path, core.NewReader(bodyStr)) if err != nil { return nil, coreerr.E("brain.apiCall", "create request", err) } @@ -135,18 +127,22 @@ func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body } defer resp.Body.Close() - respData, err := goio.ReadAll(resp.Body) - if err != nil { - return nil, coreerr.E("brain.apiCall", "read response", err) + r := core.ReadAll(resp.Body) + if !r.OK { + if readErr, ok := r.Value.(error); ok { + return nil, coreerr.E("brain.apiCall", "read response", readErr) + } + return nil, coreerr.E("brain.apiCall", "read response failed", nil) } + respData := r.Value.(string) if resp.StatusCode >= 400 { - return nil, coreerr.E("brain.apiCall", "API returned "+string(respData), nil) + return nil, coreerr.E("brain.apiCall", "API returned "+respData, nil) } var result map[string]any - if err := json.Unmarshal(respData, &result); err != nil { - return nil, coreerr.E("brain.apiCall", "parse response", err) + if ur := core.JSONUnmarshal([]byte(respData), &result); !ur.OK { + return nil, coreerr.E("brain.apiCall", "parse response", nil) } return result, nil @@ -200,30 +196,7 @@ func (s *DirectSubsystem) recall(ctx context.Context, _ *mcp.CallToolRequest, in return nil, RecallOutput{}, err } - var memories []Memory - if mems, ok := result["memories"].([]any); ok { - for _, m := range mems { - if mm, ok := m.(map[string]any); ok { - mem := Memory{ - Content: fmt.Sprintf("%v", mm["content"]), - Type: fmt.Sprintf("%v", mm["type"]), - Project: fmt.Sprintf("%v", mm["project"]), - AgentID: fmt.Sprintf("%v", mm["agent_id"]), - CreatedAt: fmt.Sprintf("%v", mm["created_at"]), - } - if id, ok := mm["id"].(string); ok { - mem.ID = id - } - if score, ok := mm["score"].(float64); ok { - mem.Confidence = score - } - if source, ok := mm["source"].(string); ok { - mem.Tags = append(mem.Tags, "source:"+source) - } - memories = append(memories, mem) - } - } - } + memories := memoriesFromResult(result) if s.onChannel != nil { s.onChannel(ctx, coremcp.ChannelBrainRecallDone, map[string]any{ @@ -274,37 +247,14 @@ func (s *DirectSubsystem) list(ctx context.Context, _ *mcp.CallToolRequest, inpu if input.AgentID != "" { values.Set("agent_id", input.AgentID) } - values.Set("limit", fmt.Sprintf("%d", limit)) + values.Set("limit", core.Sprintf("%d", limit)) result, err := s.apiCall(ctx, http.MethodGet, "/v1/brain/list?"+values.Encode(), nil) if err != nil { return nil, ListOutput{}, err } - var memories []Memory - if mems, ok := result["memories"].([]any); ok { - for _, m := range mems { - if mm, ok := m.(map[string]any); ok { - mem := Memory{ - Content: fmt.Sprintf("%v", mm["content"]), - Type: fmt.Sprintf("%v", mm["type"]), - Project: fmt.Sprintf("%v", mm["project"]), - AgentID: fmt.Sprintf("%v", mm["agent_id"]), - CreatedAt: fmt.Sprintf("%v", mm["created_at"]), - } - if id, ok := mm["id"].(string); ok { - mem.ID = id - } - if score, ok := mm["score"].(float64); ok { - mem.Confidence = score - } - if source, ok := mm["source"].(string); ok { - mem.Tags = append(mem.Tags, "source:"+source) - } - memories = append(memories, mem) - } - } - } + memories := memoriesFromResult(result) if s.onChannel != nil { s.onChannel(ctx, coremcp.ChannelBrainListDone, map[string]any{ @@ -321,3 +271,49 @@ func (s *DirectSubsystem) list(ctx context.Context, _ *mcp.CallToolRequest, inpu Memories: memories, }, nil } + +// memoriesFromResult extracts Memory entries from an API response map. +func memoriesFromResult(result map[string]any) []Memory { + var memories []Memory + mems, ok := result["memories"].([]any) + if !ok { + return memories + } + for _, m := range mems { + mm, ok := m.(map[string]any) + if !ok { + continue + } + mem := Memory{ + Content: stringFromMap(mm, "content"), + Type: stringFromMap(mm, "type"), + Project: stringFromMap(mm, "project"), + AgentID: stringFromMap(mm, "agent_id"), + CreatedAt: stringFromMap(mm, "created_at"), + } + if id, ok := mm["id"].(string); ok { + mem.ID = id + } + if score, ok := mm["score"].(float64); ok { + mem.Confidence = score + } + if source, ok := mm["source"].(string); ok { + mem.Tags = append(mem.Tags, "source:"+source) + } + memories = append(memories, mem) + } + return memories +} + +// stringFromMap extracts a string value from a map, returning "" if missing or wrong type. +func stringFromMap(m map[string]any, key string) string { + v, ok := m[key] + if !ok || v == nil { + return "" + } + s, ok := v.(string) + if !ok { + return core.Sprintf("%v", v) + } + return s +} diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index fb2ff83..dafed1d 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -5,13 +5,13 @@ package mcp import ( + "cmp" "context" "iter" "net/http" "os" "path/filepath" "slices" - "sort" "sync" core "dappco.re/go/core" @@ -542,8 +542,8 @@ func (s *Service) listDirectory(ctx context.Context, req *mcp.CallToolRequest, i if err != nil { return nil, ListDirectoryOutput{}, log.E("mcp.listDirectory", "failed to list directory", err) } - sort.Slice(entries, func(i, j int) bool { - return entries[i].Name() < entries[j].Name() + slices.SortFunc(entries, func(a, b os.DirEntry) int { + return cmp.Compare(a.Name(), b.Name()) }) result := make([]DirectoryEntry, 0, len(entries)) for _, e := range entries { diff --git a/pkg/mcp/notify.go b/pkg/mcp/notify.go index 78275ba..17c9eeb 100644 --- a/pkg/mcp/notify.go +++ b/pkg/mcp/notify.go @@ -7,13 +7,13 @@ package mcp import ( + "cmp" "context" "io" "iter" "os" "reflect" "slices" - "sort" "sync" "unsafe" @@ -362,8 +362,8 @@ func snapshotSessions(server *mcp.Server) []*mcp.ServerSession { } } - sort.Slice(sessions, func(i, j int) bool { - return sessions[i].ID() < sessions[j].ID() + slices.SortFunc(sessions, func(a, b *mcp.ServerSession) int { + return cmp.Compare(a.ID(), b.ID()) }) return sessions diff --git a/pkg/mcp/process_notifications.go b/pkg/mcp/process_notifications.go index 19c2679..1f2daf4 100644 --- a/pkg/mcp/process_notifications.go +++ b/pkg/mcp/process_notifications.go @@ -4,7 +4,6 @@ package mcp import ( "context" - "strings" "time" core "dappco.re/go/core" @@ -58,12 +57,13 @@ func isTestProcess(command string, args []string) bool { switch base { case "go": - return len(args) > 0 && strings.EqualFold(args[0], "test") + return len(args) > 0 && core.Lower(args[0]) == "test" case "cargo": - return len(args) > 0 && strings.EqualFold(args[0], "test") + return len(args) > 0 && core.Lower(args[0]) == "test" case "npm", "pnpm", "yarn", "bun": for _, arg := range args { - if strings.EqualFold(arg, "test") || core.HasPrefix(core.Lower(arg), "test:") { + lower := core.Lower(arg) + if lower == "test" || core.HasPrefix(lower, "test:") { return true } } diff --git a/pkg/mcp/transport_http.go b/pkg/mcp/transport_http.go index 9a2e4b0..49a5a11 100644 --- a/pkg/mcp/transport_http.go +++ b/pkg/mcp/transport_http.go @@ -7,7 +7,6 @@ import ( "crypto/subtle" "net" "net/http" - "os" "time" core "dappco.re/go/core" @@ -37,7 +36,7 @@ func (s *Service) ServeHTTP(ctx context.Context, addr string) error { addr = DefaultHTTPAddr } - authToken := os.Getenv("MCP_AUTH_TOKEN") + authToken := core.Env("MCP_AUTH_TOKEN") handler := mcp.NewStreamableHTTPHandler( func(r *http.Request) *mcp.Server {