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 {