refactor: AX compliance sweep — replace banned stdlib imports with core primitives
Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath, errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim, core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(), core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives. Framework boundary exceptions preserved where stdlib types are required by external interfaces (Gin, net/http, CGo, Wails, bubbletea). Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
65b686283f
commit
91803e32df
18 changed files with 318 additions and 291 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue