diff --git a/pkg/agentic/auto_pr.go b/pkg/agentic/auto_pr.go index 124b05d..a0f0305 100644 --- a/pkg/agentic/auto_pr.go +++ b/pkg/agentic/auto_pr.go @@ -4,11 +4,11 @@ package agentic import ( "context" - "fmt" "os/exec" "path/filepath" - "strings" "time" + + core "dappco.re/go/core" ) // autoCreatePR pushes the agent's branch and creates a PR on Forge @@ -28,12 +28,12 @@ func (s *PrepSubsystem) autoCreatePR(wsDir string) { diffCmd := exec.Command("git", "log", "--oneline", "origin/"+base+"..HEAD") diffCmd.Dir = srcDir out, err := diffCmd.Output() - if err != nil || len(strings.TrimSpace(string(out))) == 0 { + if err != nil || len(core.Trim(string(out))) == 0 { // No commits — nothing to PR return } - commitCount := len(strings.Split(strings.TrimSpace(string(out)), "\n")) + commitCount := len(core.Split(core.Trim(string(out)), "\n")) // Get the repo's forge remote URL to extract org/repo org := st.Org @@ -42,20 +42,20 @@ func (s *PrepSubsystem) autoCreatePR(wsDir string) { } // Push the branch to forge - forgeRemote := fmt.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo) + forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo) pushCmd := exec.Command("git", "push", forgeRemote, st.Branch) pushCmd.Dir = srcDir if pushErr := pushCmd.Run(); pushErr != nil { // Push failed — update status with error but don't block if st2, err := readStatus(wsDir); err == nil { - st2.Question = fmt.Sprintf("PR push failed: %v", pushErr) + st2.Question = core.Sprintf("PR push failed: %v", pushErr) writeStatus(wsDir, st2) } return } // Create PR via Forge API - title := fmt.Sprintf("[agent/%s] %s", st.Agent, truncate(st.Task, 60)) + title := core.Sprintf("[agent/%s] %s", st.Agent, truncate(st.Task, 60)) body := s.buildAutoPRBody(st, commitCount) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -64,7 +64,7 @@ func (s *PrepSubsystem) autoCreatePR(wsDir string) { prURL, _, err := s.forgeCreatePR(ctx, org, st.Repo, st.Branch, base, title, body) if err != nil { if st2, err := readStatus(wsDir); err == nil { - st2.Question = fmt.Sprintf("PR creation failed: %v", err) + st2.Question = core.Sprintf("PR creation failed: %v", err) writeStatus(wsDir, st2) } return @@ -78,13 +78,13 @@ func (s *PrepSubsystem) autoCreatePR(wsDir string) { } func (s *PrepSubsystem) buildAutoPRBody(st *WorkspaceStatus, commits int) string { - var b strings.Builder + b := core.NewBuilder() b.WriteString("## Task\n\n") b.WriteString(st.Task) b.WriteString("\n\n") - b.WriteString(fmt.Sprintf("**Agent:** %s\n", st.Agent)) - b.WriteString(fmt.Sprintf("**Commits:** %d\n", commits)) - b.WriteString(fmt.Sprintf("**Branch:** `%s`\n", st.Branch)) + b.WriteString(core.Sprintf("**Agent:** %s\n", st.Agent)) + b.WriteString(core.Sprintf("**Commits:** %d\n", commits)) + b.WriteString(core.Sprintf("**Branch:** `%s`\n", st.Branch)) b.WriteString("\n---\n") b.WriteString("Auto-created by core-agent dispatch system.\n") b.WriteString("Co-Authored-By: Virgil \n") diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index 2100edf..08fee61 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -4,13 +4,12 @@ package agentic import ( "context" - "fmt" "os" "path/filepath" - "strings" "syscall" "time" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "dappco.re/go/core/process" @@ -52,7 +51,7 @@ func (s *PrepSubsystem) registerDispatchTool(server *mcp.Server) { // 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 { @@ -120,7 +119,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st return 0, "", err } - outputFile := filepath.Join(wsDir, fmt.Sprintf("agent-%s.log", agent)) + outputFile := filepath.Join(wsDir, core.Sprintf("agent-%s.log", agent)) // Clean up stale BLOCKED.md from previous runs so it doesn't // prevent this run from completing @@ -172,13 +171,13 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st question := "" blockedPath := filepath.Join(wsDir, "src", "BLOCKED.md") - if blockedContent, err := coreio.Local.Read(blockedPath); err == nil && strings.TrimSpace(blockedContent) != "" { + if blockedContent, err := coreio.Local.Read(blockedPath); err == nil && core.Trim(blockedContent) != "" { finalStatus = "blocked" - question = strings.TrimSpace(blockedContent) + question = core.Trim(blockedContent) } else if exitCode != 0 || procStatus == "failed" || procStatus == "killed" { finalStatus = "failed" if exitCode != 0 { - question = fmt.Sprintf("Agent exited with code %d", exitCode) + question = core.Sprintf("Agent exited with code %d", exitCode) } } diff --git a/pkg/agentic/epic.go b/pkg/agentic/epic.go index 4668b35..6c624c5 100644 --- a/pkg/agentic/epic.go +++ b/pkg/agentic/epic.go @@ -6,10 +6,9 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" - "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -99,14 +98,14 @@ func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest } // Step 2: Build epic body with checklist - var body strings.Builder + body := core.NewBuilder() if input.Body != "" { body.WriteString(input.Body) body.WriteString("\n\n") } body.WriteString("## Tasks\n\n") for _, child := range children { - body.WriteString(fmt.Sprintf("- [ ] #%d %s\n", child.Number, child.Title)) + body.WriteString(core.Sprintf("- [ ] #%d %s\n", child.Number, child.Title)) } // Step 3: Create epic issue @@ -156,7 +155,7 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body } data, _ := json.Marshal(payload) - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues", s.forgeURL, org, repo) + url := core.Sprintf("%s/api/v1/repos/%s/%s/issues", s.forgeURL, org, repo) req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(data)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "token "+s.forgeToken) @@ -168,7 +167,7 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body defer resp.Body.Close() if resp.StatusCode != 201 { - return ChildRef{}, coreerr.E("createIssue", fmt.Sprintf("create issue returned %d", resp.StatusCode), nil) + return ChildRef{}, coreerr.E("createIssue", core.Sprintf("create issue returned %d", resp.StatusCode), nil) } var result struct { @@ -191,7 +190,7 @@ func (s *PrepSubsystem) resolveLabelIDs(ctx context.Context, org, repo string, n } // Fetch existing labels - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels?limit=50", s.forgeURL, org, repo) + url := core.Sprintf("%s/api/v1/repos/%s/%s/labels?limit=50", s.forgeURL, org, repo) req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req.Header.Set("Authorization", "token "+s.forgeToken) @@ -250,7 +249,7 @@ func (s *PrepSubsystem) createLabel(ctx context.Context, org, repo, name string) "color": colour, }) - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo) + url := core.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo) req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "token "+s.forgeToken) diff --git a/pkg/agentic/ingest.go b/pkg/agentic/ingest.go index fee8452..84b11ab 100644 --- a/pkg/agentic/ingest.go +++ b/pkg/agentic/ingest.go @@ -5,12 +5,11 @@ package agentic import ( "bytes" "encoding/json" - "fmt" "net/http" "os" "path/filepath" - "strings" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" ) @@ -36,7 +35,7 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) { body := contentStr // Skip quota errors - if strings.Contains(body, "QUOTA_EXHAUSTED") || strings.Contains(body, "QuotaError") { + if core.Contains(body, "QUOTA_EXHAUSTED") || core.Contains(body, "QuotaError") { return } @@ -49,13 +48,13 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) { // Determine issue type from the template used issueType := "task" priority := "normal" - if strings.Contains(body, "security") || strings.Contains(body, "Security") { + if core.Contains(body, "security") || core.Contains(body, "Security") { issueType = "bug" priority = "high" } // Create a single issue per repo with all findings in the body - title := fmt.Sprintf("Scan findings for %s (%d items)", st.Repo, findings) + title := core.Sprintf("Scan findings for %s (%d items)", st.Repo, findings) // Truncate body to reasonable size for issue description description := body @@ -78,7 +77,7 @@ func countFileRefs(body string) int { } if j < len(body) && body[j] == '`' { ref := body[i+1 : j] - if strings.Contains(ref, ".go:") || strings.Contains(ref, ".php:") { + if core.Contains(ref, ".go:") || core.Contains(ref, ".php:") { count++ } } @@ -99,7 +98,7 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p if err != nil { return } - apiKey := strings.TrimSpace(apiKeyStr) + apiKey := core.Trim(apiKeyStr) payload, _ := json.Marshal(map[string]string{ "title": title, diff --git a/pkg/agentic/mirror.go b/pkg/agentic/mirror.go index f56423f..480cb2c 100644 --- a/pkg/agentic/mirror.go +++ b/pkg/agentic/mirror.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -105,7 +106,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu // Skip if too many files for one PR 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 } @@ -124,7 +125,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu pushCmd := exec.CommandContext(ctx, "git", "push", "github", base+":refs/heads/dev", "--force") pushCmd.Dir = repoDir if err := pushCmd.Run(); err != nil { - sync.Skipped = fmt.Sprintf("push failed: %v", err) + sync.Skipped = core.Sprintf("push failed: %v", err) synced = append(synced, sync) continue } @@ -133,7 +134,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu // Create PR: dev → main on GitHub prURL, err := s.createGitHubPR(ctx, repoDir, repo, ahead, files) if err != nil { - sync.Skipped = fmt.Sprintf("PR creation failed: %v", err) + sync.Skipped = core.Sprintf("PR creation failed: %v", err) } else { sync.PRURL = prURL } @@ -152,11 +153,11 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu // createGitHubPR creates a PR from dev → main using the gh CLI. func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string, commits, files int) (string, error) { // Check if there's already an open PR from dev - ghRepo := fmt.Sprintf("%s/%s", GitHubOrg(), repo) + ghRepo := core.Sprintf("%s/%s", GitHubOrg(), repo) checkCmd := exec.CommandContext(ctx, "gh", "pr", "list", "--repo", ghRepo, "--head", "dev", "--state", "open", "--json", "url", "--limit", "1") checkCmd.Dir = repoDir out, err := checkCmd.Output() - if err == nil && strings.Contains(string(out), "url") { + if err == nil && core.Contains(string(out), "url") { // PR already exists — extract URL // Format: [{"url":"https://..."}] url := extractJSONField(string(out), "url") @@ -166,7 +167,7 @@ func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string } // Build PR body - body := fmt.Sprintf("## Forge → GitHub Sync\n\n"+ + body := core.Sprintf("## Forge → GitHub Sync\n\n"+ "**Commits:** %d\n"+ "**Files changed:** %d\n\n"+ "Automated sync from Forge (forge.lthn.ai) to GitHub mirror.\n"+ @@ -175,7 +176,7 @@ func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string "Co-Authored-By: Virgil ", commits, files) - title := fmt.Sprintf("[sync] %s: %d commits, %d files", repo, commits, files) + title := core.Sprintf("[sync] %s: %d commits, %d files", repo, commits, files) prCmd := exec.CommandContext(ctx, "gh", "pr", "create", "--repo", ghRepo, @@ -191,7 +192,7 @@ func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string } // gh pr create outputs the PR URL on the last line - lines := strings.Split(strings.TrimSpace(string(prOut)), "\n") + lines := core.Split(core.Trim(string(prOut)), "\n") if len(lines) > 0 { return lines[len(lines)-1], nil } @@ -223,7 +224,7 @@ func commitsAhead(repoDir, base, head string) int { return 0 } var n int - fmt.Sscanf(strings.TrimSpace(string(out)), "%d", &n) + fmt.Sscanf(core.Trim(string(out)), "%d", &n) return n } @@ -235,7 +236,7 @@ func filesChanged(repoDir, base, head string) int { if err != nil { return 0 } - lines := strings.Split(strings.TrimSpace(string(out)), "\n") + lines := core.Split(core.Trim(string(out)), "\n") if len(lines) == 1 && lines[0] == "" { return 0 } @@ -264,7 +265,7 @@ func (s *PrepSubsystem) listLocalRepos(basePath string) []string { // extractJSONField extracts a simple string field from JSON array output. func extractJSONField(jsonStr, field string) string { // Quick and dirty — works for gh CLI output like [{"url":"https://..."}] - key := fmt.Sprintf(`"%s":"`, field) + key := core.Sprintf(`"%s":"`, field) idx := strings.Index(jsonStr, key) if idx < 0 { return "" diff --git a/pkg/agentic/paths.go b/pkg/agentic/paths.go index 737878d..6e88d27 100644 --- a/pkg/agentic/paths.go +++ b/pkg/agentic/paths.go @@ -6,7 +6,8 @@ import ( "os" "os/exec" "path/filepath" - "strings" + + core "dappco.re/go/core" ) // WorkspaceRoot returns the root directory for agent workspaces. @@ -37,8 +38,8 @@ func AgentName() string { return name } hostname, _ := os.Hostname() - h := strings.ToLower(hostname) - if strings.Contains(h, "snider") || strings.Contains(h, "studio") || strings.Contains(h, "mac") { + h := core.Lower(hostname) + if core.Contains(h, "snider") || core.Contains(h, "studio") || core.Contains(h, "mac") { return "cladius" } return "charon" @@ -49,9 +50,9 @@ func gitDefaultBranch(repoDir string) string { cmd := exec.Command("git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short") cmd.Dir = repoDir if out, err := cmd.Output(); err == nil { - ref := strings.TrimSpace(string(out)) - if strings.HasPrefix(ref, "origin/") { - return strings.TrimPrefix(ref, "origin/") + ref := core.Trim(string(out)) + if core.HasPrefix(ref, "origin/") { + return core.TrimPrefix(ref, "origin/") } return ref } diff --git a/pkg/agentic/paths_test.go b/pkg/agentic/paths_test.go index 4735d5b..a1ca5e3 100644 --- a/pkg/agentic/paths_test.go +++ b/pkg/agentic/paths_test.go @@ -4,9 +4,9 @@ package agentic import ( "os" - "strings" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" ) @@ -130,5 +130,5 @@ func TestValidPlanStatus_Bad(t *testing.T) { func TestGeneratePlanID_Good(t *testing.T) { id := generatePlanID("Fix the login bug in auth service") assert.True(t, len(id) > 0) - assert.True(t, strings.Contains(id, "fix-the-login-bug")) + assert.True(t, core.Contains(id, "fix-the-login-bug")) } diff --git a/pkg/agentic/plan.go b/pkg/agentic/plan.go index 9980c6c..1104aa6 100644 --- a/pkg/agentic/plan.go +++ b/pkg/agentic/plan.go @@ -12,6 +12,7 @@ import ( "strings" "time" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -282,11 +283,11 @@ func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, inpu var plans []Plan for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") { + if entry.IsDir() || !core.HasSuffix(entry.Name(), ".json") { continue } - id := strings.TrimSuffix(entry.Name(), ".json") + id := core.TrimSuffix(entry.Name(), ".json") plan, err := readPlan(dir, id) if err != nil { continue @@ -336,8 +337,8 @@ func generatePlanID(title string) string { }, title) // Trim consecutive dashes and cap length - for strings.Contains(slug, "--") { - slug = strings.ReplaceAll(slug, "--", "-") + for core.Contains(slug, "--") { + slug = core.Replace(slug, "--", "-") } slug = strings.Trim(slug, "-") if len(slug) > 30 { diff --git a/pkg/agentic/plan_test.go b/pkg/agentic/plan_test.go index 3f59669..2c6750f 100644 --- a/pkg/agentic/plan_test.go +++ b/pkg/agentic/plan_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -137,9 +138,9 @@ func TestWriteReadPlan_Good_Roundtrip(t *testing.T) { func TestGeneratePlanID_Good_Slugifies(t *testing.T) { id := generatePlanID("Add Unit Tests for Agentic") - assert.True(t, strings.HasPrefix(id, "add-unit-tests-for-agentic"), "got: %s", id) + assert.True(t, core.HasPrefix(id, "add-unit-tests-for-agentic"), "got: %s", id) // Should have random suffix - parts := strings.Split(id, "-") + parts := core.Split(id, "-") assert.True(t, len(parts) >= 5, "expected slug with random suffix, got: %s", id) } @@ -153,7 +154,7 @@ func TestGeneratePlanID_Good_TruncatesLong(t *testing.T) { func TestGeneratePlanID_Good_HandlesSpecialChars(t *testing.T) { id := generatePlanID("Fix bug #123: auth & session!") - assert.True(t, strings.Contains(id, "fix-bug"), "got: %s", id) + assert.True(t, core.Contains(id, "fix-bug"), "got: %s", id) assert.NotContains(t, id, "#") assert.NotContains(t, id, "!") assert.NotContains(t, id, "&") diff --git a/pkg/agentic/pr.go b/pkg/agentic/pr.go index e1dcbd1..7149034 100644 --- a/pkg/agentic/pr.go +++ b/pkg/agentic/pr.go @@ -6,13 +6,12 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" "os" "os/exec" "path/filepath" - "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -75,7 +74,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 @@ -93,7 +92,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 @@ -112,7 +111,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in } // Push branch to Forge (origin is the local clone, not Forge) - forgeRemote := fmt.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo) + forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo) pushCmd := exec.CommandContext(ctx, "git", "push", forgeRemote, st.Branch) pushCmd.Dir = srcDir pushOut, err := pushCmd.CombinedOutput() @@ -132,7 +131,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) } @@ -148,17 +147,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() } @@ -171,7 +170,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base "base": base, }) - 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, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "token "+s.forgeToken) @@ -186,7 +185,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base var errBody map[string]any json.NewDecoder(resp.Body).Decode(&errBody) msg, _ := errBody["message"].(string) - return "", 0, coreerr.E("forgeCreatePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) + return "", 0, coreerr.E("forgeCreatePR", core.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) } var pr struct { @@ -201,7 +200,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base func (s *PrepSubsystem) commentOnIssue(ctx context.Context, org, repo string, issue int, comment string) { payload, _ := json.Marshal(map[string]string{"body": comment}) - 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, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "token "+s.forgeToken) @@ -303,7 +302,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) @@ -315,7 +314,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 listing PRs for %s", resp.StatusCode, repo), nil) + return nil, coreerr.E("listRepoPRs", core.Sprintf("HTTP %d listing PRs for %s", resp.StatusCode, repo), nil) } var prs []struct { diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index ddc863b..d96d46a 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -8,7 +8,6 @@ import ( "context" "encoding/base64" "encoding/json" - "fmt" "io" "net/http" "os" @@ -19,6 +18,7 @@ import ( "time" "dappco.re/go/agent/pkg/lib" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -56,7 +56,7 @@ func NewPrep() *PrepSubsystem { brainKey := os.Getenv("CORE_BRAIN_KEY") if brainKey == "" { if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil { - brainKey = strings.TrimSpace(data) + brainKey = core.Trim(data) } } @@ -156,7 +156,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques // Workspace root: .core/workspace/{repo}-{timestamp}/ wsRoot := WorkspaceRoot() - wsName := fmt.Sprintf("%s-%d", input.Repo, time.Now().UnixNano()) + wsName := core.Sprintf("%s-%d", input.Repo, time.Now().UnixNano()) wsDir := filepath.Join(wsRoot, wsName) // Create workspace structure @@ -199,17 +199,17 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques taskSlug = strings.Trim(taskSlug, "-") if taskSlug == "" { // Fallback for issue-only dispatches with no task text - taskSlug = fmt.Sprintf("issue-%d", input.Issue) + taskSlug = core.Sprintf("issue-%d", input.Issue) if input.Issue == 0 { - taskSlug = fmt.Sprintf("work-%d", time.Now().Unix()) + taskSlug = core.Sprintf("work-%d", time.Now().Unix()) } } - branchName := fmt.Sprintf("agent/%s", taskSlug) + branchName := core.Sprintf("agent/%s", taskSlug) branchCmd := exec.CommandContext(ctx, "git", "checkout", "-b", branchName) branchCmd.Dir = srcDir if err := branchCmd.Run(); err != nil { - return nil, PrepOutput{}, coreerr.E("prep.branch", fmt.Sprintf("failed to create branch %q", branchName), err) + return nil, PrepOutput{}, coreerr.E("prep.branch", core.Sprintf("failed to create branch %q", branchName), err) } out.Branch = branchName @@ -324,8 +324,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 @@ -345,7 +345,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") @@ -363,7 +363,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") } @@ -390,7 +390,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, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req.Header.Set("Authorization", "token "+s.forgeToken) @@ -417,7 +417,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, _ := http.NewRequestWithContext(ctx, "GET", pageURL, nil) pageReq.Header.Set("Authorization", "token "+s.forgeToken) @@ -482,7 +482,7 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string) "agent_id": "cladius", }) - req, _ := http.NewRequestWithContext(ctx, "POST", s.brainURL+"/v1/brain/recall", strings.NewReader(string(body))) + req, _ := http.NewRequestWithContext(ctx, "POST", s.brainURL+"/v1/brain/recall", core.NewReader(string(body))) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+s.brainKey) @@ -503,7 +503,7 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string) } json.Unmarshal(respData, &result) - var content strings.Builder + content := core.NewBuilder() content.WriteString("# Context — " + repo + "\n\n") content.WriteString("> Relevant knowledge from OpenBrain.\n\n") @@ -512,7 +512,7 @@ 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)) } coreio.Local.Write(filepath.Join(wsDir, "src", "CONTEXT.md"), content.String()) @@ -529,18 +529,18 @@ 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, "./")) + dir := filepath.Join(s.codePath, core.TrimPrefix(line, "./")) goMod := filepath.Join(dir, "go.mod") modData, err := coreio.Local.Read(goMod) if err != nil { continue } - if strings.Contains(modData, modulePath) && !strings.HasPrefix(modData, "module "+modulePath) { + if core.Contains(modData, modulePath) && !core.HasPrefix(modData, "module "+modulePath) { consumers = append(consumers, filepath.Base(dir)) } } @@ -551,7 +551,7 @@ 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)) + content += core.Sprintf("\n**Breaking change risk: %d consumers.**\n", len(consumers)) coreio.Local.Write(filepath.Join(wsDir, "src", "CONSUMERS.md"), content) } @@ -566,7 +566,7 @@ 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" coreio.Local.Write(filepath.Join(wsDir, "src", "RECENT.md"), content) @@ -580,7 +580,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) @@ -600,11 +600,8 @@ 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 += "## Objective\n\n" + issueData.Body + "\n" + content := core.Sprintf("# TASK: %s\n\n**Status:** ready\n**Source:** %s/%s/%s/issues/%d\n**Repo:** %s/%s\n\n---\n\n## Objective\n\n%s\n", + issueData.Title, s.forgeURL, org, repo, issue, org, repo, issueData.Body) coreio.Local.Write(filepath.Join(wsDir, "src", "TODO.md"), content) } diff --git a/pkg/agentic/queue.go b/pkg/agentic/queue.go index b5dbf50..a0a78d4 100644 --- a/pkg/agentic/queue.go +++ b/pkg/agentic/queue.go @@ -10,6 +10,7 @@ import ( "syscall" "time" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" "gopkg.in/yaml.v3" ) @@ -139,7 +140,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. diff --git a/pkg/agentic/remote.go b/pkg/agentic/remote.go index 0a2e6e1..3071f5f 100644 --- a/pkg/agentic/remote.go +++ b/pkg/agentic/remote.go @@ -5,12 +5,11 @@ package agentic import ( "context" "encoding/json" - "fmt" "net/http" "os" - "strings" "time" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -96,7 +95,7 @@ func (s *PrepSubsystem) dispatchRemote(ctx context.Context, _ *mcp.CallToolReque }, } - url := fmt.Sprintf("http://%s/mcp", addr) + url := core.Sprintf("http://%s/mcp", addr) client := &http.Client{Timeout: 30 * time.Second} // Step 1: Initialize session @@ -104,7 +103,7 @@ func (s *PrepSubsystem) dispatchRemote(ctx context.Context, _ *mcp.CallToolReque if err != nil { return nil, RemoteDispatchOutput{ Host: input.Host, - Error: fmt.Sprintf("init failed: %v", err), + Error: core.Sprintf("init failed: %v", err), }, coreerr.E("dispatchRemote", "MCP initialize failed", err) } @@ -114,7 +113,7 @@ func (s *PrepSubsystem) dispatchRemote(ctx context.Context, _ *mcp.CallToolReque if err != nil { return nil, RemoteDispatchOutput{ Host: input.Host, - Error: fmt.Sprintf("call failed: %v", err), + Error: core.Sprintf("call failed: %v", err), }, coreerr.E("dispatchRemote", "tool call failed", err) } @@ -163,12 +162,12 @@ func resolveHost(host string) string { "local": "127.0.0.1:9101", } - if addr, ok := aliases[strings.ToLower(host)]; ok { + if addr, ok := aliases[core.Lower(host)]; ok { return addr } // If no port specified, add default - if !strings.Contains(host, ":") { + if !core.Contains(host, ":") { return host + ":9101" } @@ -178,7 +177,7 @@ func resolveHost(host string) string { // remoteToken gets the auth token for a remote agent. func remoteToken(host string) string { // Check environment first - envKey := fmt.Sprintf("AGENT_TOKEN_%s", strings.ToUpper(host)) + envKey := core.Sprintf("AGENT_TOKEN_%s", core.Upper(host)) if token := os.Getenv(envKey); token != "" { return token } @@ -191,12 +190,12 @@ func remoteToken(host string) string { // Try reading from file home, _ := os.UserHomeDir() tokenFiles := []string{ - fmt.Sprintf("%s/.core/tokens/%s.token", home, strings.ToLower(host)), - fmt.Sprintf("%s/.core/agent-token", home), + core.Sprintf("%s/.core/tokens/%s.token", home, core.Lower(host)), + core.Sprintf("%s/.core/agent-token", home), } for _, f := range tokenFiles { if data, err := coreio.Local.Read(f); err == nil { - return strings.TrimSpace(data) + return core.Trim(data) } } diff --git a/pkg/agentic/remote_client.go b/pkg/agentic/remote_client.go index f49d024..08ce2e7 100644 --- a/pkg/agentic/remote_client.go +++ b/pkg/agentic/remote_client.go @@ -7,10 +7,9 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" - "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -46,7 +45,7 @@ func mcpInitialize(ctx context.Context, client *http.Client, url, token string) defer resp.Body.Close() if resp.StatusCode != 200 { - return "", coreerr.E("mcpInitialize", fmt.Sprintf("HTTP %d", resp.StatusCode), nil) + return "", coreerr.E("mcpInitialize", core.Sprintf("HTTP %d", resp.StatusCode), nil) } sessionID := resp.Header.Get("Mcp-Session-Id") @@ -88,7 +87,7 @@ func mcpCall(ctx context.Context, client *http.Client, url, token, sessionID str defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, coreerr.E("mcpCall", fmt.Sprintf("HTTP %d", resp.StatusCode), nil) + return nil, coreerr.E("mcpCall", core.Sprintf("HTTP %d", resp.StatusCode), nil) } // Parse SSE response — extract data: lines @@ -100,8 +99,8 @@ func readSSEData(resp *http.Response) ([]byte, error) { scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := scanner.Text() - if strings.HasPrefix(line, "data: ") { - return []byte(strings.TrimPrefix(line, "data: ")), nil + if core.HasPrefix(line, "data: ") { + return []byte(core.TrimPrefix(line, "data: ")), nil } } return nil, coreerr.E("readSSEData", "no data in SSE response", nil) diff --git a/pkg/agentic/resume.go b/pkg/agentic/resume.go index 9422502..9b3c99a 100644 --- a/pkg/agentic/resume.go +++ b/pkg/agentic/resume.go @@ -4,10 +4,10 @@ package agentic import ( "context" - "fmt" "os" "path/filepath" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -70,7 +70,7 @@ 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) + content := core.Sprintf("# Answer\n\n%s\n", input.Answer) if err := coreio.Local.Write(answerPath, content); err != nil { return nil, ResumeOutput{}, coreerr.E("resume", "failed to write ANSWER.md", err) } @@ -110,6 +110,6 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu Workspace: input.Workspace, Agent: agent, PID: pid, - OutputFile: filepath.Join(wsDir, fmt.Sprintf("agent-%s.log", agent)), + OutputFile: filepath.Join(wsDir, core.Sprintf("agent-%s.log", agent)), }, nil } diff --git a/pkg/agentic/review_queue.go b/pkg/agentic/review_queue.go index 515722a..9aae3ee 100644 --- a/pkg/agentic/review_queue.go +++ b/pkg/agentic/review_queue.go @@ -5,15 +5,14 @@ package agentic import ( "context" "encoding/json" - "fmt" "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" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -160,7 +159,7 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer // Check saved rate limit 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 } @@ -173,14 +172,14 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer output := string(out) // Parse rate limit (both reviewers use similar patterns) - if strings.Contains(output, "Rate limit exceeded") || strings.Contains(output, "rate limit") { + if core.Contains(output, "Rate limit exceeded") || core.Contains(output, "rate limit") { result.Verdict = "rate_limited" result.Detail = output return result } // Parse error - 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" result.Detail = output return result @@ -190,7 +189,7 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer s.storeReviewOutput(repoDir, repo, reviewer, output) // Parse verdict - 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" result.Findings = 0 @@ -226,7 +225,7 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer coreio.Local.Write(findingsFile, output) // Dispatch fix agent with the findings - task := fmt.Sprintf("Fix CodeRabbit findings. The review output is in .core/coderabbit-findings.txt. "+ + task := core.Sprintf("Fix CodeRabbit findings. The review output is in .core/coderabbit-findings.txt. "+ "Read it, verify each finding against the code, fix what's valid. Run tests. "+ "Commit: fix(coderabbit): address review findings\n\nFindings summary (%d issues):\n%s", result.Findings, truncate(output, 1500)) @@ -288,15 +287,15 @@ func (s *PrepSubsystem) dispatchFixFromQueue(ctx context.Context, repo, task str func countFindings(output string) int { // Count lines that look like findings count := 0 - for _, line := range strings.Split(output, "\n") { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ") || - strings.Contains(trimmed, "Issue:") || strings.Contains(trimmed, "Finding:") || - strings.Contains(trimmed, "⚠") || strings.Contains(trimmed, "❌") { + for _, line := range core.Split(output, "\n") { + trimmed := core.Trim(line) + if core.HasPrefix(trimmed, "- ") || core.HasPrefix(trimmed, "* ") || + core.Contains(trimmed, "Issue:") || core.Contains(trimmed, "Finding:") || + core.Contains(trimmed, "⚠") || core.Contains(trimmed, "❌") { count++ } } - if count == 0 && !strings.Contains(output, "No findings") { + if count == 0 && !core.Contains(output, "No findings") { count = 1 // At least one finding if not clean } return count @@ -339,7 +338,7 @@ func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string coreio.Local.EnsureDir(dataDir) timestamp := time.Now().Format("2006-01-02T15-04-05") - filename := fmt.Sprintf("%s_%s_%s.txt", repo, reviewer, timestamp) + filename := core.Sprintf("%s_%s_%s.txt", repo, reviewer, timestamp) // Write raw output coreio.Local.Write(filepath.Join(dataDir, filename), output) @@ -352,7 +351,7 @@ func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string "output": output, "verdict": "clean", } - if !strings.Contains(output, "No findings") && !strings.Contains(output, "no issues") { + if !core.Contains(output, "No findings") && !core.Contains(output, "no issues") { entry["verdict"] = "findings" } jsonLine, _ := json.Marshal(entry) diff --git a/pkg/agentic/scan.go b/pkg/agentic/scan.go index c466605..fd8e910 100644 --- a/pkg/agentic/scan.go +++ b/pkg/agentic/scan.go @@ -5,10 +5,10 @@ package agentic import ( "context" "encoding/json" - "fmt" "net/http" "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -81,7 +81,7 @@ func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input seen := make(map[string]bool) var unique []ScanIssue for _, issue := range allIssues { - key := fmt.Sprintf("%s#%d", issue.Repo, issue.Number) + key := core.Sprintf("%s#%d", issue.Repo, issue.Number) if !seen[key] { seen[key] = true unique = append(unique, issue) @@ -104,7 +104,7 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, page := 1 for { - u := fmt.Sprintf("%s/api/v1/orgs/%s/repos?limit=50&page=%d", s.forgeURL, org, page) + u := core.Sprintf("%s/api/v1/orgs/%s/repos?limit=50&page=%d", s.forgeURL, org, page) req, err := http.NewRequestWithContext(ctx, "GET", u, nil) if err != nil { return nil, coreerr.E("scan.listOrgRepos", "failed to create request", err) @@ -118,7 +118,7 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, if resp.StatusCode != 200 { resp.Body.Close() - return nil, coreerr.E("scan.listOrgRepos", fmt.Sprintf("HTTP %d listing repos", resp.StatusCode), nil) + return nil, coreerr.E("scan.listOrgRepos", core.Sprintf("HTTP %d listing repos", resp.StatusCode), nil) } var repos []struct { @@ -141,10 +141,10 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, } func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label string) ([]ScanIssue, error) { - u := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues?state=open&limit=10&type=issues", + u := core.Sprintf("%s/api/v1/repos/%s/%s/issues?state=open&limit=10&type=issues", s.forgeURL, org, repo) if label != "" { - u += "&labels=" + strings.ReplaceAll(strings.ReplaceAll(label, " ", "%20"), "&", "%26") + u += "&labels=" + core.Replace(core.Replace(label, " ", "%20"), "&", "%26") } req, err := http.NewRequestWithContext(ctx, "GET", u, nil) if err != nil { @@ -159,7 +159,7 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, coreerr.E("scan.listRepoIssues", fmt.Sprintf("HTTP %d listing issues for %s", resp.StatusCode, repo), nil) + return nil, coreerr.E("scan.listRepoIssues", core.Sprintf("HTTP %d listing issues for %s", resp.StatusCode, repo), nil) } var issues []struct { diff --git a/pkg/agentic/status.go b/pkg/agentic/status.go index 331aaf0..8349f6c 100644 --- a/pkg/agentic/status.go +++ b/pkg/agentic/status.go @@ -5,13 +5,12 @@ package agentic import ( "context" "encoding/json" - "fmt" "os" "path/filepath" - "strings" "syscall" "time" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -154,13 +153,13 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu blockedPath := filepath.Join(wsDir, "src", "BLOCKED.md") if data, err := coreio.Local.Read(blockedPath); err == nil { info.Status = "blocked" - info.Question = strings.TrimSpace(data) + info.Question = core.Trim(data) st.Status = "blocked" st.Question = info.Question } else { // Dead PID without BLOCKED.md — check exit code from log // If no evidence of success, mark as failed (not completed) - logFile := filepath.Join(wsDir, fmt.Sprintf("agent-%s.log", st.Agent)) + logFile := filepath.Join(wsDir, core.Sprintf("agent-%s.log", st.Agent)) if _, err := coreio.Local.Read(logFile); err != nil { info.Status = "failed" st.Status = "failed" diff --git a/pkg/agentic/verify.go b/pkg/agentic/verify.go index 2a2f2ce..d939e83 100644 --- a/pkg/agentic/verify.go +++ b/pkg/agentic/verify.go @@ -11,9 +11,9 @@ import ( "os" "os/exec" "path/filepath" - "strings" "time" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" ) @@ -89,7 +89,7 @@ func (s *PrepSubsystem) attemptVerifyAndMerge(srcDir, org, repo, branch string, testResult := s.runVerification(srcDir) if !testResult.passed { - comment := fmt.Sprintf("## Verification Failed\n\n**Command:** `%s`\n\n```\n%s\n```\n\n**Exit code:** %d", + comment := core.Sprintf("## Verification Failed\n\n**Command:** `%s`\n\n```\n%s\n```\n\n**Exit code:** %d", testResult.testCmd, truncate(testResult.output, 2000), testResult.exitCode) s.commentOnIssue(context.Background(), org, repo, prNum, comment) return testFailed @@ -100,12 +100,12 @@ func (s *PrepSubsystem) attemptVerifyAndMerge(srcDir, org, repo, branch string, defer cancel() if err := s.forgeMergePR(ctx, org, repo, prNum); err != nil { - comment := fmt.Sprintf("## Tests Passed — Merge Failed\n\n`%s` passed but merge failed: %v", testResult.testCmd, err) + comment := core.Sprintf("## Tests Passed — Merge Failed\n\n`%s` passed but merge failed: %v", testResult.testCmd, err) s.commentOnIssue(context.Background(), org, repo, prNum, comment) return mergeConflict } - comment := fmt.Sprintf("## Auto-Verified & Merged\n\n**Tests:** `%s` — PASS\n\nAuto-merged by core-agent dispatch system.", testResult.testCmd) + comment := core.Sprintf("## Auto-Verified & Merged\n\n**Tests:** `%s` — PASS\n\nAuto-merged by core-agent dispatch system.", testResult.testCmd) s.commentOnIssue(context.Background(), org, repo, prNum, comment) return mergeSuccess } @@ -142,7 +142,7 @@ func (s *PrepSubsystem) rebaseBranch(srcDir, branch string) bool { } repo = st.Repo } - forgeRemote := fmt.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, repo) + forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, repo) push := exec.Command("git", "push", "--force-with-lease", forgeRemote, branch) push.Dir = srcDir return push.Run() == nil @@ -160,7 +160,7 @@ func (s *PrepSubsystem) flagForReview(org, repo string, prNum int, result mergeR payload, _ := json.Marshal(map[string]any{ "labels": []int{s.getLabelID(ctx, org, repo, "needs-review")}, }) - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d/labels", s.forgeURL, org, repo, prNum) + url := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d/labels", s.forgeURL, org, repo, prNum) req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "token "+s.forgeToken) @@ -174,7 +174,7 @@ func (s *PrepSubsystem) flagForReview(org, repo string, prNum int, result mergeR if result == mergeConflict { reason = "Merge conflict persists after rebase" } - comment := fmt.Sprintf("## Needs Review\n\n%s. Auto-merge gave up after retry.\n\nLabelled `needs-review` for human attention.", reason) + comment := core.Sprintf("## Needs Review\n\n%s. Auto-merge gave up after retry.\n\nLabelled `needs-review` for human attention.", reason) s.commentOnIssue(ctx, org, repo, prNum, comment) } @@ -184,7 +184,7 @@ func (s *PrepSubsystem) ensureLabel(ctx context.Context, org, repo, name, colour "name": name, "color": "#" + colour, }) - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo) + url := core.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo) req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "token "+s.forgeToken) @@ -196,7 +196,7 @@ func (s *PrepSubsystem) ensureLabel(ctx context.Context, org, repo, name, colour // getLabelID fetches the ID of a label by name. func (s *PrepSubsystem) getLabelID(ctx context.Context, org, repo, name string) int { - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo) + url := core.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo) req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req.Header.Set("Authorization", "token "+s.forgeToken) resp, err := s.client.Do(req) @@ -318,7 +318,7 @@ func (s *PrepSubsystem) forgeMergePR(ctx context.Context, org, repo string, prNu "delete_branch_after_merge": true, }) - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d/merge", s.forgeURL, org, repo, prNum) + url := core.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d/merge", s.forgeURL, org, repo, prNum) req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "token "+s.forgeToken) @@ -333,7 +333,7 @@ func (s *PrepSubsystem) forgeMergePR(ctx context.Context, org, repo string, prNu var errBody map[string]any json.NewDecoder(resp.Body).Decode(&errBody) msg, _ := errBody["message"].(string) - return coreerr.E("forgeMergePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) + return coreerr.E("forgeMergePR", core.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) } return nil @@ -341,7 +341,7 @@ func (s *PrepSubsystem) forgeMergePR(ctx context.Context, org, repo string, prNu // extractPRNumber gets the PR number from a Forge PR URL. func extractPRNumber(prURL string) int { - parts := strings.Split(prURL, "/") + parts := core.Split(prURL, "/") if len(parts) == 0 { return 0 } diff --git a/pkg/agentic/watch.go b/pkg/agentic/watch.go index 6fc9999..7dc54e5 100644 --- a/pkg/agentic/watch.go +++ b/pkg/agentic/watch.go @@ -4,10 +4,10 @@ package agentic import ( "context" - "fmt" "path/filepath" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -128,7 +128,7 @@ func (s *PrepSubsystem) watch(ctx context.Context, req *mcp.CallToolRequest, inp ProgressToken: progressToken, Progress: progressCount, Total: total, - Message: fmt.Sprintf("%s completed (%s)", st.Repo, st.Agent), + Message: core.Sprintf("%s completed (%s)", st.Repo, st.Agent), }) } @@ -149,7 +149,7 @@ func (s *PrepSubsystem) watch(ctx context.Context, req *mcp.CallToolRequest, inp ProgressToken: progressToken, Progress: progressCount, Total: total, - Message: fmt.Sprintf("%s %s (%s)", st.Repo, st.Status, st.Agent), + Message: core.Sprintf("%s %s (%s)", st.Repo, st.Status, st.Agent), }) } @@ -169,7 +169,7 @@ func (s *PrepSubsystem) watch(ctx context.Context, req *mcp.CallToolRequest, inp ProgressToken: progressToken, Progress: progressCount, Total: total, - Message: fmt.Sprintf("%s %s (%s)", st.Repo, st.Status, st.Agent), + Message: core.Sprintf("%s %s (%s)", st.Repo, st.Status, st.Agent), }) } } diff --git a/pkg/brain/bridge_test.go b/pkg/brain/bridge_test.go index 6e8d3aa..baee24a 100644 --- a/pkg/brain/bridge_test.go +++ b/pkg/brain/bridge_test.go @@ -7,10 +7,10 @@ import ( "encoding/json" "net/http" "net/http/httptest" - "strings" "testing" "time" + core "dappco.re/go/core" ws "dappco.re/go/core/ws" "forge.lthn.ai/core/mcp/pkg/mcp/ide" "github.com/gorilla/websocket" @@ -44,7 +44,7 @@ func testBridge(t *testing.T) *ide.Bridge { t.Helper() srv := testWSServer(t) - wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + wsURL := "ws" + core.TrimPrefix(srv.URL, "http") hub := ws.NewHub() bridge := ide.NewBridge(hub, ide.Config{ LaravelWSURL: wsURL, diff --git a/pkg/brain/direct.go b/pkg/brain/direct.go index bb5a195..be418c3 100644 --- a/pkg/brain/direct.go +++ b/pkg/brain/direct.go @@ -6,25 +6,19 @@ import ( "bytes" "context" "encoding/json" - "fmt" "io" "net/http" "os" "path/filepath" - "strings" "time" "dappco.re/go/agent/pkg/agentic" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) -// agentName returns the identity of this agent. -func agentName() string { - return agentic.AgentName() -} - // DirectSubsystem implements mcp.Subsystem for OpenBrain via direct HTTP calls. // Unlike Subsystem (which uses the IDE WebSocket bridge), this calls the // Laravel API directly — suitable for standalone core-mcp usage. @@ -47,7 +41,7 @@ func NewDirect() *DirectSubsystem { if apiKey == "" { home, _ := os.UserHomeDir() if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil { - apiKey = strings.TrimSpace(data) + apiKey = core.Trim(data) } } @@ -119,7 +113,7 @@ func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body } if resp.StatusCode >= 400 { - return nil, coreerr.E("brain.apiCall", fmt.Sprintf("API returned %d: %s", resp.StatusCode, string(respData)), nil) + return nil, coreerr.E("brain.apiCall", core.Sprintf("API returned %d: %s", resp.StatusCode, string(respData)), nil) } var result map[string]any @@ -136,7 +130,7 @@ func (s *DirectSubsystem) remember(ctx context.Context, _ *mcp.CallToolRequest, "type": input.Type, "tags": input.Tags, "project": input.Project, - "agent_id": agentName(), + "agent_id": agentic.AgentName(), }) if err != nil { return nil, RememberOutput{}, err @@ -179,11 +173,11 @@ func (s *DirectSubsystem) recall(ctx context.Context, _ *mcp.CallToolRequest, in 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"]), + Content: core.Sprintf("%v", mm["content"]), + Type: core.Sprintf("%v", mm["type"]), + Project: core.Sprintf("%v", mm["project"]), + AgentID: core.Sprintf("%v", mm["agent_id"]), + CreatedAt: core.Sprintf("%v", mm["created_at"]), } if id, ok := mm["id"].(string); ok { mem.ID = id diff --git a/pkg/brain/messaging.go b/pkg/brain/messaging.go index ad5e3a6..99aa648 100644 --- a/pkg/brain/messaging.go +++ b/pkg/brain/messaging.go @@ -4,9 +4,10 @@ package brain import ( "context" - "fmt" "net/url" + "dappco.re/go/agent/pkg/agentic" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -80,7 +81,7 @@ func (s *DirectSubsystem) sendMessage(ctx context.Context, _ *mcp.CallToolReques result, err := s.apiCall(ctx, "POST", "/v1/messages/send", map[string]any{ "to": input.To, - "from": agentName(), + "from": agentic.AgentName(), "content": input.Content, "subject": input.Subject, }) @@ -101,7 +102,7 @@ func (s *DirectSubsystem) sendMessage(ctx context.Context, _ *mcp.CallToolReques func (s *DirectSubsystem) inbox(ctx context.Context, _ *mcp.CallToolRequest, input InboxInput) (*mcp.CallToolResult, InboxOutput, error) { agent := input.Agent if agent == "" { - agent = agentName() + agent = agentic.AgentName() } result, err := s.apiCall(ctx, "GET", "/v1/messages/inbox?agent="+url.QueryEscape(agent), nil) if err != nil { @@ -119,7 +120,7 @@ func (s *DirectSubsystem) conversation(ctx context.Context, _ *mcp.CallToolReque return nil, ConversationOutput{}, coreerr.E("brain.conversation", "agent is required", nil) } - result, err := s.apiCall(ctx, "GET", "/v1/messages/conversation/"+url.PathEscape(input.Agent)+"?me="+url.QueryEscape(agentName()), nil) + result, err := s.apiCall(ctx, "GET", "/v1/messages/conversation/"+url.PathEscape(input.Agent)+"?me="+url.QueryEscape(agentic.AgentName()), nil) if err != nil { return nil, ConversationOutput{}, err } @@ -137,12 +138,12 @@ func parseMessages(result map[string]any) []MessageItem { mm, _ := m.(map[string]any) messages = append(messages, MessageItem{ ID: toInt(mm["id"]), - From: fmt.Sprintf("%v", mm["from"]), - To: fmt.Sprintf("%v", mm["to"]), - Subject: fmt.Sprintf("%v", mm["subject"]), - Content: fmt.Sprintf("%v", mm["content"]), + From: core.Sprintf("%v", mm["from"]), + To: core.Sprintf("%v", mm["to"]), + Subject: core.Sprintf("%v", mm["subject"]), + Content: core.Sprintf("%v", mm["content"]), Read: mm["read"] == true, - CreatedAt: fmt.Sprintf("%v", mm["created_at"]), + CreatedAt: core.Sprintf("%v", mm["created_at"]), }) } return messages diff --git a/pkg/lib/lib.go b/pkg/lib/lib.go index dc12398..b2b6bcb 100644 --- a/pkg/lib/lib.go +++ b/pkg/lib/lib.go @@ -27,8 +27,9 @@ import ( "io/fs" "os" "path/filepath" - "strings" "text/template" + + core "dappco.re/go/core" ) //go:embed prompt/*.md @@ -160,8 +161,8 @@ func ExtractWorkspace(tmplName, targetDir string, data *WorkspaceData) error { // Process .tmpl files through text/template outputName := name - if strings.HasSuffix(name, ".tmpl") { - outputName = strings.TrimSuffix(name, ".tmpl") + if core.HasSuffix(name, ".tmpl") { + outputName = core.TrimSuffix(name, ".tmpl") tmpl, err := template.New(name).Parse(string(content)) if err != nil { return err @@ -193,9 +194,9 @@ func ListTasks() []string { if err != nil || d.IsDir() { return nil } - rel := strings.TrimPrefix(path, "task/") + rel := core.TrimPrefix(path, "task/") ext := filepath.Ext(rel) - slugs = append(slugs, strings.TrimSuffix(rel, ext)) + slugs = append(slugs, core.TrimSuffix(rel, ext)) return nil }) return slugs @@ -207,9 +208,9 @@ func ListPersonas() []string { if err != nil || d.IsDir() { return nil } - if strings.HasSuffix(path, ".md") { - rel := strings.TrimPrefix(path, "persona/") - rel = strings.TrimSuffix(rel, ".md") + if core.HasSuffix(path, ".md") { + rel := core.TrimPrefix(path, "persona/") + rel = core.TrimSuffix(rel, ".md") paths = append(paths, rel) } return nil @@ -224,14 +225,12 @@ func listDir(fsys embed.FS, dir string) []string { } var slugs []string for _, e := range entries { + name := e.Name() if e.IsDir() { - name := e.Name() slugs = append(slugs, name) continue } - name := e.Name() - ext := filepath.Ext(name) - slugs = append(slugs, strings.TrimSuffix(name, ext)) + slugs = append(slugs, core.TrimSuffix(name, filepath.Ext(name))) } return slugs } diff --git a/pkg/monitor/harvest.go b/pkg/monitor/harvest.go index 642b2d4..c353342 100644 --- a/pkg/monitor/harvest.go +++ b/pkg/monitor/harvest.go @@ -16,9 +16,9 @@ import ( "os" "os/exec" "path/filepath" - "strings" "dappco.re/go/agent/pkg/agentic" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" ) @@ -57,7 +57,7 @@ func (m *Subsystem) harvestCompleted() string { var parts []string for _, h := range harvested { if h.rejected != "" { - parts = append(parts, fmt.Sprintf("%s: REJECTED (%s)", h.repo, h.rejected)) + parts = append(parts, core.Sprintf("%s: REJECTED (%s)", h.repo, h.rejected)) if m.notifier != nil { m.notifier.ChannelSend(context.Background(), "harvest.rejected", map[string]any{ "repo": h.repo, @@ -66,7 +66,7 @@ func (m *Subsystem) harvestCompleted() string { }) } } else { - parts = append(parts, fmt.Sprintf("%s: ready-for-review %s (%d files)", h.repo, h.branch, h.files)) + parts = append(parts, core.Sprintf("%s: ready-for-review %s (%d files)", h.repo, h.branch, h.files)) if m.notifier != nil { m.notifier.ChannelSend(context.Background(), "harvest.complete", map[string]any{ "repo": h.repo, @@ -76,7 +76,7 @@ func (m *Subsystem) harvestCompleted() string { } } } - return "Harvested: " + strings.Join(parts, ", ") + return "Harvested: " + core.Join(", ", parts...) } // harvestWorkspace checks a single workspace and pushes if ready. @@ -146,7 +146,7 @@ func detectBranch(srcDir string) string { if err != nil { return "" } - return strings.TrimSpace(string(out)) + return core.Trim(string(out)) } // defaultBranch detects the default branch of the repo (main, master, etc.). @@ -155,10 +155,10 @@ func defaultBranch(srcDir string) string { cmd := exec.Command("git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short") cmd.Dir = srcDir if out, err := cmd.Output(); err == nil { - ref := strings.TrimSpace(string(out)) + ref := core.Trim(string(out)) // returns "origin/main" — strip prefix - if strings.HasPrefix(ref, "origin/") { - return strings.TrimPrefix(ref, "origin/") + if core.HasPrefix(ref, "origin/") { + return core.TrimPrefix(ref, "origin/") } return ref } @@ -186,14 +186,14 @@ func countUnpushed(srcDir, branch string) int { if err2 != nil { return 0 } - lines := strings.Split(strings.TrimSpace(string(out2)), "\n") + lines := core.Split(core.Trim(string(out2)), "\n") if len(lines) == 1 && lines[0] == "" { return 0 } return len(lines) } var count int - fmt.Sscanf(strings.TrimSpace(string(out)), "%d", &count) + fmt.Sscanf(core.Trim(string(out)), "%d", &count) return count } @@ -220,20 +220,20 @@ func checkSafety(srcDir string) string { ".db": true, ".sqlite": true, ".sqlite3": true, } - for _, file := range strings.Split(strings.TrimSpace(string(out)), "\n") { + for _, file := range core.Split(core.Trim(string(out)), "\n") { if file == "" { continue } - ext := strings.ToLower(filepath.Ext(file)) + ext := core.Lower(filepath.Ext(file)) if binaryExts[ext] { - return fmt.Sprintf("binary file added: %s", file) + return core.Sprintf("binary file added: %s", file) } // Check file size (reject > 1MB) fullPath := filepath.Join(srcDir, file) info, err := os.Stat(fullPath) if err == nil && info.Size() > 1024*1024 { - return fmt.Sprintf("large file: %s (%d bytes)", file, info.Size()) + return core.Sprintf("large file: %s (%d bytes)", file, info.Size()) } } @@ -249,7 +249,7 @@ func countChangedFiles(srcDir string) int { if err != nil { return 0 } - lines := strings.Split(strings.TrimSpace(string(out)), "\n") + lines := core.Split(core.Trim(string(out)), "\n") if len(lines) == 1 && lines[0] == "" { return 0 } @@ -262,7 +262,7 @@ func pushBranch(srcDir, branch string) error { cmd.Dir = srcDir out, err := cmd.CombinedOutput() if err != nil { - return coreerr.E("harvest.pushBranch", strings.TrimSpace(string(out)), err) + return coreerr.E("harvest.pushBranch", core.Trim(string(out)), err) } return nil } diff --git a/pkg/monitor/monitor.go b/pkg/monitor/monitor.go index 4f14c14..458b57c 100644 --- a/pkg/monitor/monitor.go +++ b/pkg/monitor/monitor.go @@ -17,11 +17,11 @@ import ( "net/url" "os" "path/filepath" - "strings" "sync" "time" "dappco.re/go/agent/pkg/agentic" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -83,6 +83,19 @@ func New(opts ...Options) *Subsystem { } } +// loadBrainKey returns the API key from CORE_BRAIN_KEY env or ~/.claude/brain.key. +func loadBrainKey() string { + if key := os.Getenv("CORE_BRAIN_KEY"); key != "" { + return key + } + home, _ := os.UserHomeDir() + data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")) + if err != nil { + return "" + } + return core.Trim(data) +} + // debugChannel sends a debug message via the notifier so it arrives as a channel event. func (m *Subsystem) debugChannel(msg string) { if m.notifier != nil { @@ -194,7 +207,7 @@ func (m *Subsystem) check(ctx context.Context) { return } - combined := strings.Join(messages, "\n") + combined := core.Join("\n", messages...) m.notify(ctx, combined) // Notify resource subscribers that agent status changed @@ -242,7 +255,7 @@ func (m *Subsystem) checkCompletions() string { if !m.seenCompleted[wsName] { m.seenCompleted[wsName] = true if seeded { - newlyCompleted = append(newlyCompleted, fmt.Sprintf("%s (%s)", st.Repo, st.Agent)) + newlyCompleted = append(newlyCompleted, core.Sprintf("%s (%s)", st.Repo, st.Agent)) } } case "running": @@ -253,7 +266,7 @@ func (m *Subsystem) checkCompletions() string { if !m.seenCompleted[wsName] { m.seenCompleted[wsName] = true if seeded { - newlyCompleted = append(newlyCompleted, fmt.Sprintf("%s (%s) [%s]", st.Repo, st.Agent, st.Status)) + newlyCompleted = append(newlyCompleted, core.Sprintf("%s (%s) [%s]", st.Repo, st.Agent, st.Status)) } } } @@ -275,27 +288,21 @@ func (m *Subsystem) checkCompletions() string { }) } - msg := fmt.Sprintf("%d agent(s) completed", len(newlyCompleted)) + msg := core.Sprintf("%d agent(s) completed", len(newlyCompleted)) if running > 0 { - msg += fmt.Sprintf(", %d still running", running) + msg += core.Sprintf(", %d still running", running) } if queued > 0 { - msg += fmt.Sprintf(", %d queued", queued) + msg += core.Sprintf(", %d queued", queued) } return msg } // checkInbox checks for unread messages. func (m *Subsystem) checkInbox() string { - apiKeyStr := os.Getenv("CORE_BRAIN_KEY") + apiKeyStr := loadBrainKey() if apiKeyStr == "" { - home, _ := os.UserHomeDir() - keyFile := filepath.Join(home, ".claude", "brain.key") - data, err := coreio.Local.Read(keyFile) - if err != nil { - return "" - } - apiKeyStr = data + return "" } // Call the API to check inbox @@ -307,7 +314,7 @@ func (m *Subsystem) checkInbox() string { if err != nil { return "" } - req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(apiKeyStr)) + req.Header.Set("Authorization", "Bearer "+core.Trim(apiKeyStr)) client := &http.Client{Timeout: 10 * time.Second} httpResp, err := client.Do(req) @@ -397,7 +404,7 @@ func (m *Subsystem) checkInbox() string { }) } - return fmt.Sprintf("%d unread message(s) in inbox", unread) + return core.Sprintf("%d unread message(s) in inbox", unread) } // notify sends a log notification to all connected MCP sessions. diff --git a/pkg/monitor/monitor_test.go b/pkg/monitor/monitor_test.go index 9cfc4ab..aed6480 100644 --- a/pkg/monitor/monitor_test.go +++ b/pkg/monitor/monitor_test.go @@ -5,7 +5,6 @@ package monitor import ( "context" "encoding/json" - "fmt" "net/http" "net/http/httptest" "os" @@ -13,6 +12,7 @@ import ( "testing" "time" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -126,9 +126,9 @@ func TestCheckCompletions_Good_NewCompletions(t *testing.T) { t.Setenv("CORE_WORKSPACE", wsRoot) for i := 0; i < 2; i++ { - writeWorkspaceStatus(t, wsRoot, fmt.Sprintf("ws-%d", i), map[string]any{ + writeWorkspaceStatus(t, wsRoot, core.Sprintf("ws-%d", i), map[string]any{ "status": "completed", - "repo": fmt.Sprintf("repo-%d", i), + "repo": core.Sprintf("repo-%d", i), "agent": "claude:sonnet", }) } @@ -152,9 +152,9 @@ func TestCheckCompletions_Good_MixedStatuses(t *testing.T) { t.Setenv("CORE_WORKSPACE", wsRoot) for i, status := range []string{"completed", "running", "queued"} { - writeWorkspaceStatus(t, wsRoot, fmt.Sprintf("ws-%d", i), map[string]any{ + writeWorkspaceStatus(t, wsRoot, core.Sprintf("ws-%d", i), map[string]any{ "status": status, - "repo": fmt.Sprintf("repo-%d", i), + "repo": core.Sprintf("repo-%d", i), "agent": "claude:sonnet", }) } @@ -563,9 +563,9 @@ func TestAgentStatusResource_Good(t *testing.T) { t.Setenv("CORE_WORKSPACE", wsRoot) for i, status := range []string{"completed", "running"} { - writeWorkspaceStatus(t, wsRoot, fmt.Sprintf("ws-%d", i), map[string]any{ + writeWorkspaceStatus(t, wsRoot, core.Sprintf("ws-%d", i), map[string]any{ "status": status, - "repo": fmt.Sprintf("repo-%d", i), + "repo": core.Sprintf("repo-%d", i), "agent": "claude:sonnet", }) } @@ -767,10 +767,10 @@ func TestHarvestCompleted_Good_MultipleWorkspaces(t *testing.T) { t.Setenv("CORE_WORKSPACE", wsRoot) for i := 0; i < 2; i++ { - name := fmt.Sprintf("ws-%d", i) + name := core.Sprintf("ws-%d", i) wsDir := filepath.Join(wsRoot, "workspace", name) - sourceDir := filepath.Join(wsRoot, fmt.Sprintf("source-%d", i)) + sourceDir := filepath.Join(wsRoot, core.Sprintf("source-%d", i)) require.NoError(t, os.MkdirAll(sourceDir, 0755)) run(t, sourceDir, "git", "init") run(t, sourceDir, "git", "checkout", "-b", "main") @@ -786,7 +786,7 @@ func TestHarvestCompleted_Good_MultipleWorkspaces(t *testing.T) { run(t, srcDir, "git", "add", ".") run(t, srcDir, "git", "commit", "-m", "agent work") - writeStatus(t, wsDir, "completed", fmt.Sprintf("repo-%d", i), "agent/test-task") + writeStatus(t, wsDir, "completed", core.Sprintf("repo-%d", i), "agent/test-task") } mon := New() diff --git a/pkg/monitor/sync.go b/pkg/monitor/sync.go index ae5a964..9b60d52 100644 --- a/pkg/monitor/sync.go +++ b/pkg/monitor/sync.go @@ -4,17 +4,15 @@ package monitor import ( "encoding/json" - "fmt" "net/http" neturl "net/url" "os" "os/exec" "path/filepath" - "strings" "time" "dappco.re/go/agent/pkg/agentic" - coreio "dappco.re/go/core/io" + core "dappco.re/go/core" ) // CheckinResponse is what the API returns for an agent checkin. @@ -42,7 +40,7 @@ func (m *Subsystem) syncRepos() string { agentName := agentic.AgentName() - checkinURL := fmt.Sprintf("%s/v1/agent/checkin?agent=%s&since=%d", apiURL, neturl.QueryEscape(agentName), m.lastSyncTimestamp) + checkinURL := core.Sprintf("%s/v1/agent/checkin?agent=%s&since=%d", apiURL, neturl.QueryEscape(agentName), m.lastSyncTimestamp) req, err := http.NewRequest("GET", checkinURL, nil) if err != nil { @@ -50,14 +48,7 @@ func (m *Subsystem) syncRepos() string { } // Use brain key for auth - brainKey := os.Getenv("CORE_BRAIN_KEY") - if brainKey == "" { - home, _ := os.UserHomeDir() - if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil { - brainKey = strings.TrimSpace(data) - } - } - if brainKey != "" { + if brainKey := loadBrainKey(); brainKey != "" { req.Header.Set("Authorization", "Bearer "+brainKey) } @@ -111,7 +102,7 @@ func (m *Subsystem) syncRepos() string { if err != nil { continue } - current := strings.TrimSpace(string(currentBranch)) + current := core.Trim(string(currentBranch)) // Determine which branch to pull — use server-reported branch, // fall back to current if server didn't specify @@ -128,7 +119,7 @@ func (m *Subsystem) syncRepos() string { statusCmd := exec.Command("git", "status", "--porcelain") statusCmd.Dir = repoDir status, _ := statusCmd.Output() - if len(strings.TrimSpace(string(status))) > 0 { + if len(core.Trim(string(status))) > 0 { continue // Don't pull if dirty } @@ -154,7 +145,7 @@ func (m *Subsystem) syncRepos() string { return "" } - return fmt.Sprintf("Synced %d repo(s): %s", len(pulled), strings.Join(pulled, ", ")) + return core.Sprintf("Synced %d repo(s): %s", len(pulled), core.Join(", ", pulled...)) } // lastSyncTimestamp is stored on the subsystem — add it via the check cycle. diff --git a/pkg/setup/config.go b/pkg/setup/config.go index fd7d349..25d4df3 100644 --- a/pkg/setup/config.go +++ b/pkg/setup/config.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + core "dappco.re/go/core" "dappco.re/go/agent/pkg/lib" ) @@ -138,17 +139,17 @@ func detectGitRemote() string { if err != nil { return "" } - url := strings.TrimSpace(string(output)) + url := core.Trim(string(output)) // SSH: git@github.com:owner/repo.git or ssh://git@forge.lthn.ai:2223/core/agent.git - if strings.Contains(url, ":") { - parts := strings.SplitN(url, ":", 2) + if core.Contains(url, ":") { + parts := core.SplitN(url, ":", 2) if len(parts) == 2 { repo := parts[1] - repo = strings.TrimSuffix(repo, ".git") + repo = core.TrimSuffix(repo, ".git") // Handle port in SSH URL (ssh://git@host:port/path) - if strings.Contains(repo, "/") { - segments := strings.SplitN(repo, "/", 2) + if core.Contains(repo, "/") { + segments := core.SplitN(repo, "/", 2) if len(segments) == 2 && strings.ContainsAny(segments[0], "0123456789") { repo = segments[1] } @@ -161,7 +162,7 @@ func detectGitRemote() string { for _, host := range []string{"github.com/", "forge.lthn.ai/"} { if idx := strings.Index(url, host); idx >= 0 { repo := url[idx+len(host):] - return strings.TrimSuffix(repo, ".git") + return core.TrimSuffix(repo, ".git") } }