refactor: replace fmt.Errorf/os.* with go-io/go-log conventions
Replace all fmt.Errorf and errors.New in production code with
coreerr.E("caller.Method", "message", err) from go-log. Replace
all os.ReadFile/os.WriteFile/os.MkdirAll/os.Remove with coreio.Local
equivalents from go-io. Test files are intentionally untouched.
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
56397d7377
commit
5eb26f90fc
31 changed files with 258 additions and 231 deletions
|
|
@ -1,7 +1,6 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
|
@ -11,6 +10,7 @@ import (
|
|||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
agentic "forge.lthn.ai/core/agent/pkg/lifecycle"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-scm/agentci"
|
||||
"forge.lthn.ai/core/config"
|
||||
)
|
||||
|
|
@ -80,18 +80,18 @@ func agentAddCmd() *cli.Command {
|
|||
keys, err := scanCmd.Output()
|
||||
if err != nil {
|
||||
fmt.Println(errorStyle.Render("FAILED"))
|
||||
return fmt.Errorf("failed to scan host keys: %w", err)
|
||||
return coreerr.E("agent.add", "failed to scan host keys", err)
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
knownHostsPath := filepath.Join(home, ".ssh", "known_hosts")
|
||||
f, err := os.OpenFile(knownHostsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open known_hosts: %w", err)
|
||||
return coreerr.E("agent.add", "failed to open known_hosts", err)
|
||||
}
|
||||
if _, err := f.Write(keys); err != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("failed to write known_hosts: %w", err)
|
||||
return coreerr.E("agent.add", "failed to write known_hosts", err)
|
||||
}
|
||||
f.Close()
|
||||
fmt.Println(successStyle.Render("OK"))
|
||||
|
|
@ -102,7 +102,7 @@ func agentAddCmd() *cli.Command {
|
|||
out, err := testCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Println(errorStyle.Render("FAILED"))
|
||||
return fmt.Errorf("SSH failed: %s", strings.TrimSpace(string(out)))
|
||||
return coreerr.E("agent.add", "SSH failed: "+strings.TrimSpace(string(out)), nil)
|
||||
}
|
||||
fmt.Println(successStyle.Render("OK"))
|
||||
|
||||
|
|
@ -204,7 +204,7 @@ func agentStatusCmd() *cli.Command {
|
|||
}
|
||||
ac, ok := agents[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("agent %q not found", name)
|
||||
return coreerr.E("agent.status", "agent not found: "+name, nil)
|
||||
}
|
||||
|
||||
script := `
|
||||
|
|
@ -256,7 +256,7 @@ func agentLogsCmd() *cli.Command {
|
|||
}
|
||||
ac, ok := agents[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("agent %q not found", name)
|
||||
return coreerr.E("agent.status", "agent not found: "+name, nil)
|
||||
}
|
||||
|
||||
remoteCmd := fmt.Sprintf("tail -n %d ~/ai-work/logs/runner.log", lines)
|
||||
|
|
@ -294,13 +294,13 @@ func agentSetupCmd() *cli.Command {
|
|||
}
|
||||
ac, ok := agents[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("agent %q not found — use 'core ai agent add' first", name)
|
||||
return coreerr.E("agent.setup", "agent not found: "+name+" — use 'core ai agent add' first", nil)
|
||||
}
|
||||
|
||||
// Find the setup script relative to the binary or in known locations.
|
||||
scriptPath := findSetupScript()
|
||||
if scriptPath == "" {
|
||||
return errors.New("agent-setup.sh not found — expected in scripts/ directory")
|
||||
return coreerr.E("agent.setup", "agent-setup.sh not found — expected in scripts/ directory", nil)
|
||||
}
|
||||
|
||||
fmt.Printf("Setting up %s on %s...\n", name, ac.Host)
|
||||
|
|
@ -308,7 +308,7 @@ func agentSetupCmd() *cli.Command {
|
|||
setupCmd.Stdout = os.Stdout
|
||||
setupCmd.Stderr = os.Stderr
|
||||
if err := setupCmd.Run(); err != nil {
|
||||
return fmt.Errorf("setup failed: %w", err)
|
||||
return coreerr.E("agent.setup", "setup failed", err)
|
||||
}
|
||||
|
||||
fmt.Println(successStyle.Render("Setup complete!"))
|
||||
|
|
@ -358,7 +358,7 @@ func agentFleetCmd() *cli.Command {
|
|||
|
||||
registry, err := agentic.NewSQLiteRegistry(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open registry: %w", err)
|
||||
return coreerr.E("agent.fleet", "failed to open registry", err)
|
||||
}
|
||||
defer registry.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"time"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
"forge.lthn.ai/core/go-log"
|
||||
|
||||
agentic "forge.lthn.ai/core/agent/pkg/lifecycle"
|
||||
|
|
@ -159,7 +160,7 @@ func dispatchWatchCmd() *cli.Command {
|
|||
defer cancel()
|
||||
|
||||
if err := client.Ping(ctx); err != nil {
|
||||
return fmt.Errorf("API ping failed (url=%s): %w", apiURL, err)
|
||||
return log.E("dispatch.watch", "API ping failed (url="+apiURL+")", err)
|
||||
}
|
||||
log.Info("Connected to agentic API", "url", apiURL, "agent", agentID)
|
||||
|
||||
|
|
@ -348,7 +349,7 @@ func executePhaseWork(ctx context.Context, client *agentic.Client, plan *agentic
|
|||
// Prepare the repository.
|
||||
jobDir := filepath.Join(paths.jobs, fmt.Sprintf("%s-%s-%d", t.RepoOwner, t.RepoName, t.IssueNumber))
|
||||
repoDir := filepath.Join(jobDir, t.RepoName)
|
||||
if err := os.MkdirAll(jobDir, 0755); err != nil {
|
||||
if err := coreio.Local.EnsureDir(jobDir); err != nil {
|
||||
log.Error("Failed to create job dir", "error", err)
|
||||
_ = client.EndSession(ctx, sessionID, string(agentic.SessionFailed), fmt.Sprintf("mkdir failed: %v", err))
|
||||
return false
|
||||
|
|
@ -507,8 +508,8 @@ func dispatchStatusCmd() *cli.Command {
|
|||
paths := getPaths(workDir)
|
||||
|
||||
lockStatus := "IDLE"
|
||||
if data, err := os.ReadFile(paths.lock); err == nil {
|
||||
pidStr := strings.TrimSpace(string(data))
|
||||
if data, err := coreio.Local.Read(paths.lock); err == nil {
|
||||
pidStr := strings.TrimSpace(data)
|
||||
pid, _ := strconv.Atoi(pidStr)
|
||||
if isProcessAlive(pid) {
|
||||
lockStatus = fmt.Sprintf("RUNNING (PID %d)", pid)
|
||||
|
|
@ -587,21 +588,21 @@ func processTicket(paths runnerPaths, ticketPath string) (bool, error) {
|
|||
|
||||
activePath := filepath.Join(paths.active, fileName)
|
||||
if err := os.Rename(ticketPath, activePath); err != nil {
|
||||
return false, fmt.Errorf("failed to move ticket to active: %w", err)
|
||||
return false, log.E("processTicket", "failed to move ticket to active", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(activePath)
|
||||
data, err := coreio.Local.Read(activePath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to read ticket: %w", err)
|
||||
return false, log.E("processTicket", "failed to read ticket", err)
|
||||
}
|
||||
var t dispatchTicket
|
||||
if err := json.Unmarshal(data, &t); err != nil {
|
||||
return false, fmt.Errorf("failed to unmarshal ticket: %w", err)
|
||||
if err := json.Unmarshal([]byte(data), &t); err != nil {
|
||||
return false, log.E("processTicket", "failed to unmarshal ticket", err)
|
||||
}
|
||||
|
||||
jobDir := filepath.Join(paths.jobs, fmt.Sprintf("%s-%s-%d", t.RepoOwner, t.RepoName, t.IssueNumber))
|
||||
repoDir := filepath.Join(jobDir, t.RepoName)
|
||||
if err := os.MkdirAll(jobDir, 0755); err != nil {
|
||||
if err := coreio.Local.EnsureDir(jobDir); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
|
@ -673,14 +674,14 @@ func prepareRepo(t dispatchTicket, repoDir string) error {
|
|||
continue
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("git command %v failed: %s", args, string(out))
|
||||
return log.E("prepareRepo", "git command failed: "+string(out), err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Info("Cloning repo", "url", t.RepoOwner+"/"+t.RepoName)
|
||||
cmd := exec.Command("git", "clone", "-b", t.TargetBranch, cloneURL, repoDir)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("git clone failed: %s", string(out))
|
||||
return log.E("prepareRepo", "git clone failed: "+string(out), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -818,29 +819,29 @@ func moveToDone(paths runnerPaths, activePath, fileName string) {
|
|||
func ensureDispatchDirs(p runnerPaths) error {
|
||||
dirs := []string{p.queue, p.active, p.done, p.logs, p.jobs}
|
||||
for _, d := range dirs {
|
||||
if err := os.MkdirAll(d, 0755); err != nil {
|
||||
return fmt.Errorf("mkdir %s failed: %w", d, err)
|
||||
if err := coreio.Local.EnsureDir(d); err != nil {
|
||||
return log.E("ensureDispatchDirs", "mkdir "+d+" failed", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func acquireLock(lockPath string) error {
|
||||
if data, err := os.ReadFile(lockPath); err == nil {
|
||||
pidStr := strings.TrimSpace(string(data))
|
||||
if data, err := coreio.Local.Read(lockPath); err == nil {
|
||||
pidStr := strings.TrimSpace(data)
|
||||
pid, _ := strconv.Atoi(pidStr)
|
||||
if isProcessAlive(pid) {
|
||||
return fmt.Errorf("locked by PID %d", pid)
|
||||
return log.E("acquireLock", fmt.Sprintf("locked by PID %d", pid), nil)
|
||||
}
|
||||
log.Info("Removing stale lock", "pid", pid)
|
||||
_ = os.Remove(lockPath)
|
||||
_ = coreio.Local.Delete(lockPath)
|
||||
}
|
||||
|
||||
return os.WriteFile(lockPath, []byte(fmt.Sprintf("%d", os.Getpid())), 0644)
|
||||
return coreio.Local.Write(lockPath, fmt.Sprintf("%d", os.Getpid()))
|
||||
}
|
||||
|
||||
func releaseLock(lockPath string) {
|
||||
_ = os.Remove(lockPath)
|
||||
_ = coreio.Local.Delete(lockPath)
|
||||
}
|
||||
|
||||
func isProcessAlive(pid int) bool {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ package workspace
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -31,6 +30,7 @@ import (
|
|||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -92,7 +92,7 @@ func agentContextPath(wsPath, provider, name string) string {
|
|||
func parseAgentID(id string) (provider, name string, err error) {
|
||||
parts := strings.SplitN(id, "/", 2)
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return "", "", errors.New("agent ID must be provider/agent-name (e.g. claude-opus/qa)")
|
||||
return "", "", coreerr.E("parseAgentID", "agent ID must be provider/agent-name (e.g. claude-opus/qa)", nil)
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
|
@ -134,10 +134,10 @@ func runAgentInit(cmd *cli.Command, args []string) error {
|
|||
|
||||
// Create directory structure
|
||||
if err := coreio.Local.EnsureDir(agentDir); err != nil {
|
||||
return fmt.Errorf("failed to create agent directory: %w", err)
|
||||
return coreerr.E("agentInit", "failed to create agent directory", err)
|
||||
}
|
||||
if err := coreio.Local.EnsureDir(filepath.Join(agentDir, "artifacts")); err != nil {
|
||||
return fmt.Errorf("failed to create artifacts directory: %w", err)
|
||||
return coreerr.E("agentInit", "failed to create artifacts directory", err)
|
||||
}
|
||||
|
||||
// Create initial memory.md
|
||||
|
|
@ -153,7 +153,7 @@ func runAgentInit(cmd *cli.Command, args []string) error {
|
|||
`, provider, name, taskIssue, taskEpic, taskEpic, taskIssue, time.Now().Format(time.RFC3339))
|
||||
|
||||
if err := coreio.Local.Write(filepath.Join(agentDir, "memory.md"), memoryContent); err != nil {
|
||||
return fmt.Errorf("failed to create memory.md: %w", err)
|
||||
return coreerr.E("agentInit", "failed to create memory.md", err)
|
||||
}
|
||||
|
||||
// Write manifest
|
||||
|
|
@ -184,7 +184,7 @@ func runAgentList(cmd *cli.Command, args []string) error {
|
|||
|
||||
providers, err := coreio.Local.List(agentsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list agents: %w", err)
|
||||
return coreerr.E("agentList", "failed to list agents", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ package workspace
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -19,6 +18,7 @@ import (
|
|||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-scm/repos"
|
||||
)
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ func runTaskCreate(cmd *cli.Command, args []string) error {
|
|||
if len(repoNames) == 0 {
|
||||
repoNames, err = registryRepoNames(root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load registry: %w", err)
|
||||
return coreerr.E("taskCreate", "failed to load registry", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ func runTaskCreate(cmd *cli.Command, args []string) error {
|
|||
}
|
||||
|
||||
if err := coreio.Local.EnsureDir(wsPath); err != nil {
|
||||
return fmt.Errorf("failed to create workspace directory: %w", err)
|
||||
return coreerr.E("taskCreate", "failed to create workspace directory", err)
|
||||
}
|
||||
|
||||
cli.Print("Creating task workspace: %s\n", cli.ValueStyle.Render(fmt.Sprintf("p%d/i%d", taskEpic, taskIssue)))
|
||||
|
|
@ -190,14 +190,14 @@ func runTaskRemove(cmd *cli.Command, args []string) error {
|
|||
cli.Print(" %s %s\n", cli.ErrorStyle.Render("·"), r)
|
||||
}
|
||||
cli.Print("\nUse --force to override or resolve the issues first.\n")
|
||||
return errors.New("workspace has unresolved changes")
|
||||
return coreerr.E("taskRemove", "workspace has unresolved changes", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove worktrees first (so git knows they're gone)
|
||||
entries, err := coreio.Local.List(wsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list workspace: %w", err)
|
||||
return coreerr.E("taskRemove", "failed to list workspace", err)
|
||||
}
|
||||
|
||||
config, _ := LoadConfig(root)
|
||||
|
|
@ -224,7 +224,7 @@ func runTaskRemove(cmd *cli.Command, args []string) error {
|
|||
|
||||
// Remove the workspace directory
|
||||
if err := coreio.Local.DeleteAll(wsPath); err != nil {
|
||||
return fmt.Errorf("failed to remove workspace directory: %w", err)
|
||||
return coreerr.E("taskRemove", "failed to remove workspace directory", err)
|
||||
}
|
||||
|
||||
// Clean up empty parent (p{epic}/) if it's now empty
|
||||
|
|
@ -251,7 +251,7 @@ func runTaskList(cmd *cli.Command, args []string) error {
|
|||
|
||||
epics, err := coreio.Local.List(wsRoot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list workspaces: %w", err)
|
||||
return coreerr.E("taskList", "failed to list workspaces", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
|
|
@ -316,7 +316,7 @@ func runTaskStatus(cmd *cli.Command, args []string) error {
|
|||
|
||||
entries, err := coreio.Local.List(wsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list workspace: %w", err)
|
||||
return coreerr.E("taskStatus", "failed to list workspace", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
|
|
@ -369,11 +369,11 @@ func createWorktree(ctx context.Context, repoPath, worktreePath, branch string)
|
|||
cmd.Dir = repoPath
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s", strings.TrimSpace(string(output)))
|
||||
return coreerr.E("createWorktree", strings.TrimSpace(string(output)), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s", errStr)
|
||||
return coreerr.E("createWorktree", errStr, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
package workspace
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -46,16 +45,16 @@ func LoadConfig(dir string) (*WorkspaceConfig, error) {
|
|||
// No workspace.yaml found anywhere - return nil to indicate no config
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read workspace config: %w", err)
|
||||
return nil, coreerr.E("LoadConfig", "failed to read workspace config", err)
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
if err := yaml.Unmarshal([]byte(data), config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse workspace config: %w", err)
|
||||
return nil, coreerr.E("LoadConfig", "failed to parse workspace config", err)
|
||||
}
|
||||
|
||||
if config.Version != 1 {
|
||||
return nil, fmt.Errorf("unsupported workspace config version: %d", config.Version)
|
||||
return nil, coreerr.E("LoadConfig", "unsupported workspace config version", nil)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
|
|
@ -65,17 +64,17 @@ func LoadConfig(dir string) (*WorkspaceConfig, error) {
|
|||
func SaveConfig(dir string, config *WorkspaceConfig) error {
|
||||
coreDir := filepath.Join(dir, ".core")
|
||||
if err := coreio.Local.EnsureDir(coreDir); err != nil {
|
||||
return fmt.Errorf("failed to create .core directory: %w", err)
|
||||
return coreerr.E("SaveConfig", "failed to create .core directory", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(coreDir, "workspace.yaml")
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal workspace config: %w", err)
|
||||
return coreerr.E("SaveConfig", "failed to marshal workspace config", err)
|
||||
}
|
||||
|
||||
if err := coreio.Local.Write(path, string(data)); err != nil {
|
||||
return fmt.Errorf("failed to write workspace config: %w", err)
|
||||
return coreerr.E("SaveConfig", "failed to write workspace config", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -100,5 +99,5 @@ func FindWorkspaceRoot() (string, error) {
|
|||
dir = parent
|
||||
}
|
||||
|
||||
return "", errors.New("not in a workspace")
|
||||
return "", coreerr.E("FindWorkspaceRoot", "not in a workspace", nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-process"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
|
@ -76,7 +78,7 @@ func agentCommand(agent, prompt string) (string, []string, error) {
|
|||
script := filepath.Join(home, "Code", "core", "agent", "scripts", "local-agent.sh")
|
||||
return "bash", []string{script, prompt}, nil
|
||||
default:
|
||||
return "", nil, fmt.Errorf("unknown agent: %s", agent)
|
||||
return "", nil, coreerr.E("agentCommand", "unknown agent: "+agent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st
|
|||
Detach: true,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("failed to spawn %s: %w", agent, err)
|
||||
return 0, "", coreerr.E("dispatch.spawnAgent", "failed to spawn "+agent, err)
|
||||
}
|
||||
|
||||
pid := proc.Info().PID
|
||||
|
|
@ -109,7 +111,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st
|
|||
|
||||
// Write captured output to log file
|
||||
if output := proc.Output(); output != "" {
|
||||
os.WriteFile(outputFile, []byte(output), 0644)
|
||||
coreio.Local.Write(outputFile, output)
|
||||
}
|
||||
|
||||
// Update status to completed
|
||||
|
|
@ -131,10 +133,10 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st
|
|||
|
||||
func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, input DispatchInput) (*mcp.CallToolResult, DispatchOutput, error) {
|
||||
if input.Repo == "" {
|
||||
return nil, DispatchOutput{}, fmt.Errorf("repo is required")
|
||||
return nil, DispatchOutput{}, coreerr.E("dispatch", "repo is required", nil)
|
||||
}
|
||||
if input.Task == "" {
|
||||
return nil, DispatchOutput{}, fmt.Errorf("task is required")
|
||||
return nil, DispatchOutput{}, coreerr.E("dispatch", "task is required", nil)
|
||||
}
|
||||
if input.Org == "" {
|
||||
input.Org = "core"
|
||||
|
|
@ -159,7 +161,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
|
|||
}
|
||||
_, prepOut, err := s.prepWorkspace(ctx, req, prepInput)
|
||||
if err != nil {
|
||||
return nil, DispatchOutput{}, fmt.Errorf("prep workspace failed: %w", err)
|
||||
return nil, DispatchOutput{}, coreerr.E("dispatch", "prep workspace failed", err)
|
||||
}
|
||||
|
||||
wsDir := prepOut.WorkspaceDir
|
||||
|
|
@ -170,13 +172,13 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
|
|||
|
||||
if input.DryRun {
|
||||
// Read PROMPT.md for the dry run output
|
||||
promptContent, _ := os.ReadFile(filepath.Join(wsDir, "PROMPT.md"))
|
||||
promptContent, _ := coreio.Local.Read(filepath.Join(wsDir, "PROMPT.md"))
|
||||
return nil, DispatchOutput{
|
||||
Success: true,
|
||||
Agent: input.Agent,
|
||||
Repo: input.Repo,
|
||||
WorkspaceDir: wsDir,
|
||||
Prompt: string(promptContent),
|
||||
Prompt: promptContent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -53,13 +54,13 @@ func (s *PrepSubsystem) registerEpicTool(server *mcp.Server) {
|
|||
|
||||
func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest, input EpicInput) (*mcp.CallToolResult, EpicOutput, error) {
|
||||
if input.Title == "" {
|
||||
return nil, EpicOutput{}, fmt.Errorf("title is required")
|
||||
return nil, EpicOutput{}, coreerr.E("createEpic", "title is required", nil)
|
||||
}
|
||||
if len(input.Tasks) == 0 {
|
||||
return nil, EpicOutput{}, fmt.Errorf("at least one task is required")
|
||||
return nil, EpicOutput{}, coreerr.E("createEpic", "at least one task is required", nil)
|
||||
}
|
||||
if s.forgeToken == "" {
|
||||
return nil, EpicOutput{}, fmt.Errorf("no Forge token configured")
|
||||
return nil, EpicOutput{}, coreerr.E("createEpic", "no Forge token configured", nil)
|
||||
}
|
||||
if input.Org == "" {
|
||||
input.Org = "core"
|
||||
|
|
@ -112,7 +113,7 @@ func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest
|
|||
epicLabels := append(labelIDs, s.resolveLabelIDs(ctx, input.Org, input.Repo, []string{"epic"})...)
|
||||
epic, err := s.createIssue(ctx, input.Org, input.Repo, input.Title, body.String(), epicLabels)
|
||||
if err != nil {
|
||||
return nil, EpicOutput{}, fmt.Errorf("failed to create epic: %w", err)
|
||||
return nil, EpicOutput{}, coreerr.E("createEpic", "failed to create epic", err)
|
||||
}
|
||||
|
||||
out := EpicOutput{
|
||||
|
|
@ -162,12 +163,12 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body
|
|||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return ChildRef{}, fmt.Errorf("create issue request failed: %w", err)
|
||||
return ChildRef{}, coreerr.E("createIssue", "create issue request failed", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 201 {
|
||||
return ChildRef{}, fmt.Errorf("create issue returned %d", resp.StatusCode)
|
||||
return ChildRef{}, coreerr.E("createIssue", fmt.Sprintf("create issue returned %d", resp.StatusCode), nil)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
)
|
||||
|
||||
// ingestFindings reads the agent output log and creates issues via the API
|
||||
|
|
@ -26,12 +28,12 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) {
|
|||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(logFiles[0])
|
||||
if err != nil || len(content) < 100 {
|
||||
contentStr, err := coreio.Local.Read(logFiles[0])
|
||||
if err != nil || len(contentStr) < 100 {
|
||||
return
|
||||
}
|
||||
|
||||
body := string(content)
|
||||
body := contentStr
|
||||
|
||||
// Skip quota errors
|
||||
if strings.Contains(body, "QUOTA_EXHAUSTED") || strings.Contains(body, "QuotaError") {
|
||||
|
|
@ -93,11 +95,11 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p
|
|||
|
||||
// Read the agent API key from file
|
||||
home, _ := os.UserHomeDir()
|
||||
apiKeyData, err := os.ReadFile(filepath.Join(home, ".claude", "agent-api.key"))
|
||||
apiKeyStr, err := coreio.Local.Read(filepath.Join(home, ".claude", "agent-api.key"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
apiKey := strings.TrimSpace(string(apiKeyData))
|
||||
apiKey := strings.TrimSpace(apiKeyStr)
|
||||
|
||||
payload, _ := json.Marshal(map[string]string{
|
||||
"title": title,
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import (
|
|||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -145,10 +146,10 @@ func (s *PrepSubsystem) registerPlanTools(server *mcp.Server) {
|
|||
|
||||
func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, input PlanCreateInput) (*mcp.CallToolResult, PlanCreateOutput, error) {
|
||||
if input.Title == "" {
|
||||
return nil, PlanCreateOutput{}, fmt.Errorf("title is required")
|
||||
return nil, PlanCreateOutput{}, coreerr.E("planCreate", "title is required", nil)
|
||||
}
|
||||
if input.Objective == "" {
|
||||
return nil, PlanCreateOutput{}, fmt.Errorf("objective is required")
|
||||
return nil, PlanCreateOutput{}, coreerr.E("planCreate", "objective is required", nil)
|
||||
}
|
||||
|
||||
id := generatePlanID(input.Title)
|
||||
|
|
@ -177,7 +178,7 @@ func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, in
|
|||
|
||||
path, err := writePlan(s.plansDir(), &plan)
|
||||
if err != nil {
|
||||
return nil, PlanCreateOutput{}, fmt.Errorf("failed to write plan: %w", err)
|
||||
return nil, PlanCreateOutput{}, coreerr.E("planCreate", "failed to write plan", err)
|
||||
}
|
||||
|
||||
return nil, PlanCreateOutput{
|
||||
|
|
@ -189,7 +190,7 @@ func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, in
|
|||
|
||||
func (s *PrepSubsystem) planRead(_ context.Context, _ *mcp.CallToolRequest, input PlanReadInput) (*mcp.CallToolResult, PlanReadOutput, error) {
|
||||
if input.ID == "" {
|
||||
return nil, PlanReadOutput{}, fmt.Errorf("id is required")
|
||||
return nil, PlanReadOutput{}, coreerr.E("planRead", "id is required", nil)
|
||||
}
|
||||
|
||||
plan, err := readPlan(s.plansDir(), input.ID)
|
||||
|
|
@ -205,7 +206,7 @@ func (s *PrepSubsystem) planRead(_ context.Context, _ *mcp.CallToolRequest, inpu
|
|||
|
||||
func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, input PlanUpdateInput) (*mcp.CallToolResult, PlanUpdateOutput, error) {
|
||||
if input.ID == "" {
|
||||
return nil, PlanUpdateOutput{}, fmt.Errorf("id is required")
|
||||
return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "id is required", nil)
|
||||
}
|
||||
|
||||
plan, err := readPlan(s.plansDir(), input.ID)
|
||||
|
|
@ -216,7 +217,7 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in
|
|||
// Apply partial updates
|
||||
if input.Status != "" {
|
||||
if !validPlanStatus(input.Status) {
|
||||
return nil, PlanUpdateOutput{}, fmt.Errorf("invalid status: %s (valid: draft, ready, in_progress, needs_verification, verified, approved)", input.Status)
|
||||
return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "invalid status: "+input.Status+" (valid: draft, ready, in_progress, needs_verification, verified, approved)", nil)
|
||||
}
|
||||
plan.Status = input.Status
|
||||
}
|
||||
|
|
@ -239,7 +240,7 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in
|
|||
plan.UpdatedAt = time.Now()
|
||||
|
||||
if _, err := writePlan(s.plansDir(), plan); err != nil {
|
||||
return nil, PlanUpdateOutput{}, fmt.Errorf("failed to write plan: %w", err)
|
||||
return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "failed to write plan", err)
|
||||
}
|
||||
|
||||
return nil, PlanUpdateOutput{
|
||||
|
|
@ -250,16 +251,16 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in
|
|||
|
||||
func (s *PrepSubsystem) planDelete(_ context.Context, _ *mcp.CallToolRequest, input PlanDeleteInput) (*mcp.CallToolResult, PlanDeleteOutput, error) {
|
||||
if input.ID == "" {
|
||||
return nil, PlanDeleteOutput{}, fmt.Errorf("id is required")
|
||||
return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "id is required", nil)
|
||||
}
|
||||
|
||||
path := planPath(s.plansDir(), input.ID)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil, PlanDeleteOutput{}, fmt.Errorf("plan not found: %s", input.ID)
|
||||
return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "plan not found: "+input.ID, nil)
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
return nil, PlanDeleteOutput{}, fmt.Errorf("failed to delete plan: %w", err)
|
||||
if err := coreio.Local.Delete(path); err != nil {
|
||||
return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "failed to delete plan", err)
|
||||
}
|
||||
|
||||
return nil, PlanDeleteOutput{
|
||||
|
|
@ -270,13 +271,13 @@ func (s *PrepSubsystem) planDelete(_ context.Context, _ *mcp.CallToolRequest, in
|
|||
|
||||
func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, input PlanListInput) (*mcp.CallToolResult, PlanListOutput, error) {
|
||||
dir := s.plansDir()
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, PlanListOutput{}, fmt.Errorf("failed to access plans directory: %w", err)
|
||||
if err := coreio.Local.EnsureDir(dir); err != nil {
|
||||
return nil, PlanListOutput{}, coreerr.E("planList", "failed to access plans directory", err)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, PlanListOutput{}, fmt.Errorf("failed to read plans directory: %w", err)
|
||||
return nil, PlanListOutput{}, coreerr.E("planList", "failed to read plans directory", err)
|
||||
}
|
||||
|
||||
var plans []Plan
|
||||
|
|
@ -351,21 +352,21 @@ func generatePlanID(title string) string {
|
|||
}
|
||||
|
||||
func readPlan(dir, id string) (*Plan, error) {
|
||||
data, err := os.ReadFile(planPath(dir, id))
|
||||
data, err := coreio.Local.Read(planPath(dir, id))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plan not found: %s", id)
|
||||
return nil, coreerr.E("readPlan", "plan not found: "+id, nil)
|
||||
}
|
||||
|
||||
var plan Plan
|
||||
if err := json.Unmarshal(data, &plan); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse plan %s: %w", id, err)
|
||||
if err := json.Unmarshal([]byte(data), &plan); err != nil {
|
||||
return nil, coreerr.E("readPlan", "failed to parse plan "+id, err)
|
||||
}
|
||||
return &plan, nil
|
||||
}
|
||||
|
||||
func writePlan(dir string, plan *Plan) (string, error) {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create plans directory: %w", err)
|
||||
if err := coreio.Local.EnsureDir(dir); err != nil {
|
||||
return "", coreerr.E("writePlan", "failed to create plans directory", err)
|
||||
}
|
||||
|
||||
path := planPath(dir, plan.ID)
|
||||
|
|
@ -374,7 +375,7 @@ func writePlan(dir string, plan *Plan) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
return path, os.WriteFile(path, data, 0644)
|
||||
return path, coreio.Local.Write(path, string(data))
|
||||
}
|
||||
|
||||
func validPlanStatus(status string) bool {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -47,10 +48,10 @@ func (s *PrepSubsystem) registerCreatePRTool(server *mcp.Server) {
|
|||
|
||||
func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, input CreatePRInput) (*mcp.CallToolResult, CreatePROutput, error) {
|
||||
if input.Workspace == "" {
|
||||
return nil, CreatePROutput{}, fmt.Errorf("workspace is required")
|
||||
return nil, CreatePROutput{}, coreerr.E("createPR", "workspace is required", nil)
|
||||
}
|
||||
if s.forgeToken == "" {
|
||||
return nil, CreatePROutput{}, fmt.Errorf("no Forge token configured")
|
||||
return nil, CreatePROutput{}, coreerr.E("createPR", "no Forge token configured", nil)
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
|
|
@ -58,13 +59,13 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in
|
|||
srcDir := filepath.Join(wsDir, "src")
|
||||
|
||||
if _, err := os.Stat(srcDir); err != nil {
|
||||
return nil, CreatePROutput{}, fmt.Errorf("workspace not found: %s", input.Workspace)
|
||||
return nil, CreatePROutput{}, coreerr.E("createPR", "workspace not found: "+input.Workspace, nil)
|
||||
}
|
||||
|
||||
// Read workspace status for repo, branch, issue context
|
||||
st, err := readStatus(wsDir)
|
||||
if err != nil {
|
||||
return nil, CreatePROutput{}, fmt.Errorf("no status.json: %w", err)
|
||||
return nil, CreatePROutput{}, coreerr.E("createPR", "no status.json", err)
|
||||
}
|
||||
|
||||
if st.Branch == "" {
|
||||
|
|
@ -73,7 +74,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in
|
|||
branchCmd.Dir = srcDir
|
||||
out, err := branchCmd.Output()
|
||||
if err != nil {
|
||||
return nil, CreatePROutput{}, fmt.Errorf("failed to detect branch: %w", err)
|
||||
return nil, CreatePROutput{}, coreerr.E("createPR", "failed to detect branch", err)
|
||||
}
|
||||
st.Branch = strings.TrimSpace(string(out))
|
||||
}
|
||||
|
|
@ -116,13 +117,13 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in
|
|||
pushCmd.Dir = srcDir
|
||||
pushOut, err := pushCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, CreatePROutput{}, fmt.Errorf("git push failed: %s: %w", string(pushOut), err)
|
||||
return nil, CreatePROutput{}, coreerr.E("createPR", "git push failed: "+string(pushOut), err)
|
||||
}
|
||||
|
||||
// Create PR via Forge API
|
||||
prURL, prNum, err := s.forgeCreatePR(ctx, org, st.Repo, st.Branch, base, title, body)
|
||||
if err != nil {
|
||||
return nil, CreatePROutput{}, fmt.Errorf("failed to create PR: %w", err)
|
||||
return nil, CreatePROutput{}, coreerr.E("createPR", "failed to create PR", err)
|
||||
}
|
||||
|
||||
// Update status with PR URL
|
||||
|
|
@ -177,7 +178,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base
|
|||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("request failed: %w", err)
|
||||
return "", 0, coreerr.E("forgeCreatePR", "request failed", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
|
@ -185,7 +186,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base
|
|||
var errBody map[string]any
|
||||
json.NewDecoder(resp.Body).Decode(&errBody)
|
||||
msg, _ := errBody["message"].(string)
|
||||
return "", 0, fmt.Errorf("HTTP %d: %s", resp.StatusCode, msg)
|
||||
return "", 0, coreerr.E("forgeCreatePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil)
|
||||
}
|
||||
|
||||
var pr struct {
|
||||
|
|
@ -252,7 +253,7 @@ func (s *PrepSubsystem) registerListPRsTool(server *mcp.Server) {
|
|||
|
||||
func (s *PrepSubsystem) listPRs(ctx context.Context, _ *mcp.CallToolRequest, input ListPRsInput) (*mcp.CallToolResult, ListPRsOutput, error) {
|
||||
if s.forgeToken == "" {
|
||||
return nil, ListPRsOutput{}, fmt.Errorf("no Forge token configured")
|
||||
return nil, ListPRsOutput{}, coreerr.E("listPRs", "no Forge token configured", nil)
|
||||
}
|
||||
|
||||
if input.Org == "" {
|
||||
|
|
@ -309,7 +310,7 @@ func (s *PrepSubsystem) listRepoPRs(ctx context.Context, org, repo, state string
|
|||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("failed to list PRs for %s: %v", repo, err)
|
||||
return nil, coreerr.E("listRepoPRs", "failed to list PRs for "+repo, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
@ -43,8 +45,8 @@ func NewPrep() *PrepSubsystem {
|
|||
|
||||
brainKey := os.Getenv("CORE_BRAIN_KEY")
|
||||
if brainKey == "" {
|
||||
if data, err := os.ReadFile(filepath.Join(home, ".claude", "brain.key")); err == nil {
|
||||
brainKey = strings.TrimSpace(string(data))
|
||||
if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil {
|
||||
brainKey = strings.TrimSpace(data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +124,7 @@ type PrepOutput struct {
|
|||
|
||||
func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolRequest, input PrepInput) (*mcp.CallToolResult, PrepOutput, error) {
|
||||
if input.Repo == "" {
|
||||
return nil, PrepOutput{}, fmt.Errorf("repo is required")
|
||||
return nil, PrepOutput{}, coreerr.E("prepWorkspace", "repo is required", nil)
|
||||
}
|
||||
if input.Org == "" {
|
||||
input.Org = "core"
|
||||
|
|
@ -171,29 +173,29 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
|||
branchCmd.Run()
|
||||
|
||||
// Create context dirs inside src/
|
||||
os.MkdirAll(filepath.Join(srcDir, "kb"), 0755)
|
||||
os.MkdirAll(filepath.Join(srcDir, "specs"), 0755)
|
||||
coreio.Local.EnsureDir(filepath.Join(srcDir, "kb"))
|
||||
coreio.Local.EnsureDir(filepath.Join(srcDir, "specs"))
|
||||
|
||||
// Remote stays as local clone origin — agent cannot push to forge.
|
||||
// Reviewer pulls changes from workspace and pushes after verification.
|
||||
|
||||
// 2. Copy CLAUDE.md and GEMINI.md to workspace
|
||||
claudeMdPath := filepath.Join(repoPath, "CLAUDE.md")
|
||||
if data, err := os.ReadFile(claudeMdPath); err == nil {
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "CLAUDE.md"), data, 0644)
|
||||
if data, err := coreio.Local.Read(claudeMdPath); err == nil {
|
||||
coreio.Local.Write(filepath.Join(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")
|
||||
if data, err := os.ReadFile(agentGeminiMd); err == nil {
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "GEMINI.md"), data, 0644)
|
||||
if data, err := coreio.Local.Read(agentGeminiMd); err == nil {
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "GEMINI.md"), data)
|
||||
}
|
||||
|
||||
// Copy persona if specified
|
||||
if input.Persona != "" {
|
||||
personaPath := filepath.Join(s.codePath, "core", "agent", "prompts", "personas", input.Persona+".md")
|
||||
if data, err := os.ReadFile(personaPath); err == nil {
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "PERSONA.md"), data, 0644)
|
||||
if data, err := coreio.Local.Read(personaPath); err == nil {
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "PERSONA.md"), data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +205,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
|||
} else if input.Task != "" {
|
||||
todo := fmt.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)
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "TODO.md"), []byte(todo), 0644)
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "TODO.md"), todo)
|
||||
}
|
||||
|
||||
// 4. Generate CONTEXT.md from OpenBrain
|
||||
|
|
@ -300,7 +302,7 @@ Do NOT push. Commit only — a reviewer will verify and push.
|
|||
prompt = "Read TODO.md and complete the task. Work in src/.\n"
|
||||
}
|
||||
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "PROMPT.md"), []byte(prompt), 0644)
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "PROMPT.md"), prompt)
|
||||
}
|
||||
|
||||
// --- Plan template rendering ---
|
||||
|
|
@ -310,17 +312,17 @@ Do NOT push. Commit only — a reviewer will verify and push.
|
|||
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")
|
||||
data, err := os.ReadFile(templatePath)
|
||||
data, err := coreio.Local.Read(templatePath)
|
||||
if err != nil {
|
||||
// Try .yml extension
|
||||
templatePath = filepath.Join(s.codePath, "core", "agent", "prompts", "templates", templateSlug+".yml")
|
||||
data, err = os.ReadFile(templatePath)
|
||||
data, err = coreio.Local.Read(templatePath)
|
||||
if err != nil {
|
||||
return // Template not found, skip silently
|
||||
}
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
content := data
|
||||
|
||||
// Substitute variables ({{variable_name}} → value)
|
||||
for key, value := range variables {
|
||||
|
|
@ -380,7 +382,7 @@ func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map
|
|||
plan.WriteString("\n**Commit after completing this phase.**\n\n---\n\n")
|
||||
}
|
||||
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "PLAN.md"), []byte(plan.String()), 0644)
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "PLAN.md"), plan.String())
|
||||
}
|
||||
|
||||
// --- Helpers (unchanged) ---
|
||||
|
|
@ -440,7 +442,7 @@ func (s *PrepSubsystem) pullWiki(ctx context.Context, org, repo, wsDir string) i
|
|||
return '-'
|
||||
}, page.Title) + ".md"
|
||||
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "kb", filename), content, 0644)
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "kb", filename), string(content))
|
||||
count++
|
||||
}
|
||||
|
||||
|
|
@ -453,8 +455,8 @@ func (s *PrepSubsystem) copySpecs(wsDir string) int {
|
|||
|
||||
for _, file := range specFiles {
|
||||
src := filepath.Join(s.specsPath, file)
|
||||
if data, err := os.ReadFile(src); err == nil {
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "specs", file), data, 0644)
|
||||
if data, err := coreio.Local.Read(src); err == nil {
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "specs", file), data)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
|
@ -503,7 +505,7 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string)
|
|||
content.WriteString(fmt.Sprintf("### %d. %s [%s] (score: %.3f)\n\n%s\n\n", i+1, memProject, memType, score, memContent))
|
||||
}
|
||||
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "CONTEXT.md"), []byte(content.String()), 0644)
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "CONTEXT.md"), content.String())
|
||||
return len(result.Memories)
|
||||
}
|
||||
|
||||
|
|
@ -511,24 +513,24 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int {
|
|||
goWorkPath := filepath.Join(s.codePath, "go.work")
|
||||
modulePath := "forge.lthn.ai/core/" + repo
|
||||
|
||||
workData, err := os.ReadFile(goWorkPath)
|
||||
workData, err := coreio.Local.Read(goWorkPath)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var consumers []string
|
||||
for _, line := range strings.Split(string(workData), "\n") {
|
||||
for _, line := range strings.Split(workData, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "./") {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(s.codePath, strings.TrimPrefix(line, "./"))
|
||||
goMod := filepath.Join(dir, "go.mod")
|
||||
modData, err := os.ReadFile(goMod)
|
||||
modData, err := coreio.Local.Read(goMod)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(string(modData), modulePath) && !strings.HasPrefix(string(modData), "module "+modulePath) {
|
||||
if strings.Contains(modData, modulePath) && !strings.HasPrefix(modData, "module "+modulePath) {
|
||||
consumers = append(consumers, filepath.Base(dir))
|
||||
}
|
||||
}
|
||||
|
|
@ -540,7 +542,7 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int {
|
|||
content += "- " + c + "\n"
|
||||
}
|
||||
content += fmt.Sprintf("\n**Breaking change risk: %d consumers.**\n", len(consumers))
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "CONSUMERS.md"), []byte(content), 0644)
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "CONSUMERS.md"), content)
|
||||
}
|
||||
|
||||
return len(consumers)
|
||||
|
|
@ -557,7 +559,7 @@ func (s *PrepSubsystem) gitLog(repoPath, wsDir string) int {
|
|||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
if len(lines) > 0 && lines[0] != "" {
|
||||
content := "# Recent Changes\n\n```\n" + string(output) + "```\n"
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "RECENT.md"), []byte(content), 0644)
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "RECENT.md"), content)
|
||||
}
|
||||
|
||||
return len(lines)
|
||||
|
|
@ -590,5 +592,5 @@ func (s *PrepSubsystem) generateTodo(ctx context.Context, org, repo string, issu
|
|||
content += fmt.Sprintf("**Repo:** %s/%s\n\n---\n\n", org, repo)
|
||||
content += "## Objective\n\n" + issueData.Body + "\n"
|
||||
|
||||
os.WriteFile(filepath.Join(wsDir, "src", "TODO.md"), []byte(content), 0644)
|
||||
coreio.Local.Write(filepath.Join(wsDir, "src", "TODO.md"), content)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -47,12 +48,12 @@ func (s *PrepSubsystem) loadAgentsConfig() *AgentsConfig {
|
|||
}
|
||||
|
||||
for _, path := range paths {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := coreio.Local.Read(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var cfg AgentsConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
if err := yaml.Unmarshal([]byte(data), &cfg); err != nil {
|
||||
continue
|
||||
}
|
||||
return &cfg
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -38,7 +40,7 @@ func (s *PrepSubsystem) registerResumeTool(server *mcp.Server) {
|
|||
|
||||
func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, input ResumeInput) (*mcp.CallToolResult, ResumeOutput, error) {
|
||||
if input.Workspace == "" {
|
||||
return nil, ResumeOutput{}, fmt.Errorf("workspace is required")
|
||||
return nil, ResumeOutput{}, coreerr.E("resume", "workspace is required", nil)
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
|
|
@ -47,17 +49,17 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
|||
|
||||
// Verify workspace exists
|
||||
if _, err := os.Stat(srcDir); err != nil {
|
||||
return nil, ResumeOutput{}, fmt.Errorf("workspace not found: %s", input.Workspace)
|
||||
return nil, ResumeOutput{}, coreerr.E("resume", "workspace not found: "+input.Workspace, nil)
|
||||
}
|
||||
|
||||
// Read current status
|
||||
st, err := readStatus(wsDir)
|
||||
if err != nil {
|
||||
return nil, ResumeOutput{}, fmt.Errorf("no status.json in workspace: %w", err)
|
||||
return nil, ResumeOutput{}, coreerr.E("resume", "no status.json in workspace", err)
|
||||
}
|
||||
|
||||
if st.Status != "blocked" && st.Status != "failed" && st.Status != "completed" {
|
||||
return nil, ResumeOutput{}, fmt.Errorf("workspace is %s, not resumable (must be blocked, failed, or completed)", st.Status)
|
||||
return nil, ResumeOutput{}, coreerr.E("resume", "workspace is "+st.Status+", not resumable (must be blocked, failed, or completed)", nil)
|
||||
}
|
||||
|
||||
// Determine agent
|
||||
|
|
@ -70,8 +72,8 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
|||
if input.Answer != "" {
|
||||
answerPath := filepath.Join(srcDir, "ANSWER.md")
|
||||
content := fmt.Sprintf("# Answer\n\n%s\n", input.Answer)
|
||||
if err := os.WriteFile(answerPath, []byte(content), 0644); err != nil {
|
||||
return nil, ResumeOutput{}, fmt.Errorf("failed to write ANSWER.md: %w", err)
|
||||
if err := coreio.Local.Write(answerPath, content); err != nil {
|
||||
return nil, ResumeOutput{}, coreerr.E("resume", "failed to write ANSWER.md", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ type ScanIssue struct {
|
|||
|
||||
func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input ScanInput) (*mcp.CallToolResult, ScanOutput, error) {
|
||||
if s.forgeToken == "" {
|
||||
return nil, ScanOutput{}, fmt.Errorf("no Forge token configured")
|
||||
return nil, ScanOutput{}, coreerr.E("scan", "no Forge token configured", nil)
|
||||
}
|
||||
|
||||
if input.Org == "" {
|
||||
|
|
@ -105,7 +106,7 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string,
|
|||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("failed to list repos: %v", err)
|
||||
return nil, coreerr.E("scan.listOrgRepos", "failed to list repos", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
|
@ -129,7 +130,7 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str
|
|||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("failed to list issues for %s: %v", repo, err)
|
||||
return nil, coreerr.E("scan.listRepoIssues", "failed to list issues for "+repo, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ package agentic
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -49,16 +50,16 @@ func writeStatus(wsDir string, status *WorkspaceStatus) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0644)
|
||||
return coreio.Local.Write(filepath.Join(wsDir, "status.json"), string(data))
|
||||
}
|
||||
|
||||
func readStatus(wsDir string) (*WorkspaceStatus, error) {
|
||||
data, err := os.ReadFile(filepath.Join(wsDir, "status.json"))
|
||||
data, err := coreio.Local.Read(filepath.Join(wsDir, "status.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var s WorkspaceStatus
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
if err := json.Unmarshal([]byte(data), &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
|
|
@ -99,7 +100,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
|||
|
||||
entries, err := os.ReadDir(wsRoot)
|
||||
if err != nil {
|
||||
return nil, StatusOutput{}, fmt.Errorf("no workspaces found: %w", err)
|
||||
return nil, StatusOutput{}, coreerr.E("status", "no workspaces found", err)
|
||||
}
|
||||
|
||||
var workspaces []WorkspaceInfo
|
||||
|
|
@ -150,9 +151,9 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
|||
if err != nil || proc.Signal(nil) != nil {
|
||||
// Process died — check for BLOCKED.md
|
||||
blockedPath := filepath.Join(wsDir, "src", "BLOCKED.md")
|
||||
if data, err := os.ReadFile(blockedPath); err == nil {
|
||||
if data, err := coreio.Local.Read(blockedPath); err == nil {
|
||||
info.Status = "blocked"
|
||||
info.Question = strings.TrimSpace(string(data))
|
||||
info.Question = strings.TrimSpace(data)
|
||||
st.Status = "blocked"
|
||||
st.Question = info.Question
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@ package brain
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/mcp/pkg/mcp/ide"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// errBridgeNotAvailable is returned when a tool requires the Laravel bridge
|
||||
// but it has not been initialised (headless mode).
|
||||
var errBridgeNotAvailable = errors.New("brain: bridge not available")
|
||||
var errBridgeNotAvailable = coreerr.E("brain", "bridge not available", nil)
|
||||
|
||||
// Subsystem implements mcp.Subsystem for OpenBrain knowledge store operations.
|
||||
// It proxies brain_* tool calls to the Laravel backend via the shared IDE bridge.
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -49,8 +52,9 @@ func NewDirect() *DirectSubsystem {
|
|||
|
||||
apiKey := os.Getenv("CORE_BRAIN_KEY")
|
||||
if apiKey == "" {
|
||||
if data, err := os.ReadFile(os.ExpandEnv("$HOME/.claude/brain.key")); err == nil {
|
||||
apiKey = strings.TrimSpace(string(data))
|
||||
home, _ := os.UserHomeDir()
|
||||
if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil {
|
||||
apiKey = strings.TrimSpace(data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,21 +94,21 @@ func (s *DirectSubsystem) Shutdown(_ context.Context) error { return nil }
|
|||
|
||||
func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body any) (map[string]any, error) {
|
||||
if s.apiKey == "" {
|
||||
return nil, fmt.Errorf("brain: no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)")
|
||||
return nil, coreerr.E("brain.apiCall", "no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)", nil)
|
||||
}
|
||||
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("brain: marshal request: %w", err)
|
||||
return nil, coreerr.E("brain.apiCall", "marshal request", err)
|
||||
}
|
||||
reqBody = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, s.apiURL+path, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("brain: create request: %w", err)
|
||||
return nil, coreerr.E("brain.apiCall", "create request", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
|
@ -112,22 +116,22 @@ func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body
|
|||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("brain: API call failed: %w", err)
|
||||
return nil, coreerr.E("brain.apiCall", "API call failed", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("brain: read response: %w", err)
|
||||
return nil, coreerr.E("brain.apiCall", "read response", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("brain: API returned %d: %s", resp.StatusCode, string(respData))
|
||||
return nil, coreerr.E("brain.apiCall", fmt.Sprintf("API returned %d: %s", resp.StatusCode, string(respData)), nil)
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(respData, &result); err != nil {
|
||||
return nil, fmt.Errorf("brain: parse response: %w", err)
|
||||
return nil, coreerr.E("brain.apiCall", "parse response", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -73,7 +74,7 @@ type ConversationOutput struct {
|
|||
|
||||
func (s *DirectSubsystem) sendMessage(ctx context.Context, _ *mcp.CallToolRequest, input SendInput) (*mcp.CallToolResult, SendOutput, error) {
|
||||
if input.To == "" || input.Content == "" {
|
||||
return nil, SendOutput{}, fmt.Errorf("to and content are required")
|
||||
return nil, SendOutput{}, coreerr.E("brain.sendMessage", "to and content are required", nil)
|
||||
}
|
||||
|
||||
result, err := s.apiCall(ctx, "POST", "/v1/messages/send", map[string]any{
|
||||
|
|
@ -114,7 +115,7 @@ func (s *DirectSubsystem) inbox(ctx context.Context, _ *mcp.CallToolRequest, inp
|
|||
|
||||
func (s *DirectSubsystem) conversation(ctx context.Context, _ *mcp.CallToolRequest, input ConversationInput) (*mcp.CallToolResult, ConversationOutput, error) {
|
||||
if input.Agent == "" {
|
||||
return nil, ConversationOutput{}, fmt.Errorf("agent is required")
|
||||
return nil, ConversationOutput{}, coreerr.E("brain.conversation", "agent is required", nil)
|
||||
}
|
||||
|
||||
result, err := s.apiCall(ctx, "GET", "/v1/messages/conversation/"+input.Agent+"?me="+agentName(), nil)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ package brain
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/mcp/pkg/mcp/ide"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
|
@ -140,7 +140,7 @@ func (s *Subsystem) brainRemember(_ context.Context, _ *mcp.CallToolRequest, inp
|
|||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, RememberOutput{}, fmt.Errorf("failed to send brain_remember: %w", err)
|
||||
return nil, RememberOutput{}, coreerr.E("brain.remember", "failed to send brain_remember", err)
|
||||
}
|
||||
|
||||
return nil, RememberOutput{
|
||||
|
|
@ -163,7 +163,7 @@ func (s *Subsystem) brainRecall(_ context.Context, _ *mcp.CallToolRequest, input
|
|||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, RecallOutput{}, fmt.Errorf("failed to send brain_recall: %w", err)
|
||||
return nil, RecallOutput{}, coreerr.E("brain.recall", "failed to send brain_recall", err)
|
||||
}
|
||||
|
||||
return nil, RecallOutput{
|
||||
|
|
@ -185,7 +185,7 @@ func (s *Subsystem) brainForget(_ context.Context, _ *mcp.CallToolRequest, input
|
|||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ForgetOutput{}, fmt.Errorf("failed to send brain_forget: %w", err)
|
||||
return nil, ForgetOutput{}, coreerr.E("brain.forget", "failed to send brain_forget", err)
|
||||
}
|
||||
|
||||
return nil, ForgetOutput{
|
||||
|
|
@ -210,7 +210,7 @@ func (s *Subsystem) brainList(_ context.Context, _ *mcp.CallToolRequest, input L
|
|||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ListOutput{}, fmt.Errorf("failed to send brain_list: %w", err)
|
||||
return nil, ListOutput{}, coreerr.E("brain.list", "failed to send brain_list", err)
|
||||
}
|
||||
|
||||
return nil, ListOutput{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-scm/forge"
|
||||
"forge.lthn.ai/core/agent/pkg/jobrunner"
|
||||
)
|
||||
|
|
@ -47,11 +48,11 @@ func (h *CompletionHandler) Execute(ctx context.Context, signal *jobrunner.Pipel
|
|||
if signal.Success {
|
||||
completeLabel, err := h.forge.EnsureLabel(signal.RepoOwner, signal.RepoName, LabelAgentComplete, ColorAgentComplete)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ensure label %s: %w", LabelAgentComplete, err)
|
||||
return nil, coreerr.E("completion.Execute", "ensure label "+LabelAgentComplete, err)
|
||||
}
|
||||
|
||||
if err := h.forge.AddIssueLabels(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), []int64{completeLabel.ID}); err != nil {
|
||||
return nil, fmt.Errorf("add completed label: %w", err)
|
||||
return nil, coreerr.E("completion.Execute", "add completed label", err)
|
||||
}
|
||||
|
||||
if signal.Message != "" {
|
||||
|
|
@ -60,11 +61,11 @@ func (h *CompletionHandler) Execute(ctx context.Context, signal *jobrunner.Pipel
|
|||
} else {
|
||||
failedLabel, err := h.forge.EnsureLabel(signal.RepoOwner, signal.RepoName, LabelAgentFailed, ColorAgentFailed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ensure label %s: %w", LabelAgentFailed, err)
|
||||
return nil, coreerr.E("completion.Execute", "ensure label "+LabelAgentFailed, err)
|
||||
}
|
||||
|
||||
if err := h.forge.AddIssueLabels(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), []int64{failedLabel.ID}); err != nil {
|
||||
return nil, fmt.Errorf("add failed label: %w", err)
|
||||
return nil, coreerr.E("completion.Execute", "add failed label", err)
|
||||
}
|
||||
|
||||
msg := "Agent reported failure."
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
agentci "forge.lthn.ai/core/agent/pkg/orchestrator"
|
||||
"forge.lthn.ai/core/go-scm/forge"
|
||||
"forge.lthn.ai/core/agent/pkg/jobrunner"
|
||||
"forge.lthn.ai/core/go-log"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -83,23 +83,23 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
|
|||
|
||||
agentName, agent, ok := h.spinner.FindByForgejoUser(signal.Assignee)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("handlers.Dispatch.Execute: unknown agent: %s", signal.Assignee)
|
||||
return nil, coreerr.E("handlers.Dispatch.Execute", "unknown agent: "+signal.Assignee, nil)
|
||||
}
|
||||
|
||||
// Sanitize inputs to prevent path traversal.
|
||||
safeOwner, err := agentci.SanitizePath(signal.RepoOwner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid repo owner: %w", err)
|
||||
return nil, coreerr.E("handlers.Dispatch.Execute", "invalid repo owner", err)
|
||||
}
|
||||
safeRepo, err := agentci.SanitizePath(signal.RepoName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid repo name: %w", err)
|
||||
return nil, coreerr.E("handlers.Dispatch.Execute", "invalid repo name", err)
|
||||
}
|
||||
|
||||
// Ensure in-progress label exists on repo.
|
||||
inProgressLabel, err := h.forge.EnsureLabel(safeOwner, safeRepo, LabelInProgress, ColorInProgress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ensure label %s: %w", LabelInProgress, err)
|
||||
return nil, coreerr.E("handlers.Dispatch.Execute", "ensure label "+LabelInProgress, err)
|
||||
}
|
||||
|
||||
// Check if already in progress to prevent double-dispatch.
|
||||
|
|
@ -107,7 +107,7 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
|
|||
if err == nil {
|
||||
for _, l := range issue.Labels {
|
||||
if l.Name == LabelInProgress || l.Name == LabelAgentComplete {
|
||||
log.Info("issue already processed, skipping", "issue", signal.ChildNumber)
|
||||
coreerr.Info("issue already processed, skipping", "issue", signal.ChildNumber)
|
||||
return &jobrunner.ActionResult{
|
||||
Action: "dispatch",
|
||||
Success: true,
|
||||
|
|
@ -120,11 +120,11 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
|
|||
|
||||
// Assign agent and add in-progress label.
|
||||
if err := h.forge.AssignIssue(safeOwner, safeRepo, int64(signal.ChildNumber), []string{signal.Assignee}); err != nil {
|
||||
log.Warn("failed to assign agent, continuing", "err", err)
|
||||
coreerr.Warn("failed to assign agent, continuing", "err", err)
|
||||
}
|
||||
|
||||
if err := h.forge.AddIssueLabels(safeOwner, safeRepo, int64(signal.ChildNumber), []int64{inProgressLabel.ID}); err != nil {
|
||||
return nil, fmt.Errorf("add in-progress label: %w", err)
|
||||
return nil, coreerr.E("handlers.Dispatch.Execute", "add in-progress label", err)
|
||||
}
|
||||
|
||||
// Remove agent-ready label if present.
|
||||
|
|
@ -164,13 +164,13 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
|
|||
ticketJSON, err := json.MarshalIndent(ticket, "", " ")
|
||||
if err != nil {
|
||||
h.failDispatch(signal, "Failed to marshal ticket JSON")
|
||||
return nil, fmt.Errorf("marshal ticket: %w", err)
|
||||
return nil, coreerr.E("handlers.Dispatch.Execute", "marshal ticket", err)
|
||||
}
|
||||
|
||||
// Check if ticket already exists on agent (dedup).
|
||||
ticketName := fmt.Sprintf("ticket-%s-%s-%d.json", safeOwner, safeRepo, signal.ChildNumber)
|
||||
if h.ticketExists(ctx, agent, ticketName) {
|
||||
log.Info("ticket already queued, skipping", "ticket", ticketName, "agent", signal.Assignee)
|
||||
coreerr.Info("ticket already queued, skipping", "ticket", ticketName, "agent", signal.Assignee)
|
||||
return &jobrunner.ActionResult{
|
||||
Action: "dispatch",
|
||||
RepoOwner: safeOwner,
|
||||
|
|
@ -263,7 +263,7 @@ func (h *DispatchHandler) secureTransfer(ctx context.Context, agent agentci.Agen
|
|||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return log.E("dispatch.transfer", fmt.Sprintf("ssh to %s failed: %s", agent.Host, string(output)), err)
|
||||
return coreerr.E("dispatch.transfer", fmt.Sprintf("ssh to %s failed: %s", agent.Host, string(output)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
forgejosdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-scm/forge"
|
||||
"forge.lthn.ai/core/agent/pkg/jobrunner"
|
||||
)
|
||||
|
|
@ -39,7 +40,7 @@ func (h *DismissReviewsHandler) Execute(ctx context.Context, signal *jobrunner.P
|
|||
|
||||
reviews, err := h.forge.ListPRReviews(signal.RepoOwner, signal.RepoName, int64(signal.PRNumber))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dismiss_reviews: list reviews: %w", err)
|
||||
return nil, coreerr.E("dismiss_reviews.Execute", "list reviews", err)
|
||||
}
|
||||
|
||||
var dismissErrors []string
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
forgejosdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-scm/forge"
|
||||
"forge.lthn.ai/core/agent/pkg/jobrunner"
|
||||
)
|
||||
|
|
@ -41,7 +42,7 @@ func (h *TickParentHandler) Execute(ctx context.Context, signal *jobrunner.Pipel
|
|||
// Fetch the epic issue body.
|
||||
epic, err := h.forge.GetIssue(signal.RepoOwner, signal.RepoName, int64(signal.EpicNumber))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tick_parent: fetch epic: %w", err)
|
||||
return nil, coreerr.E("tick_parent.Execute", "fetch epic", err)
|
||||
}
|
||||
|
||||
oldBody := epic.Body
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@ package jobrunner
|
|||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
// validPathComponent matches safe repo owner/name characters (alphanumeric, hyphen, underscore, dot).
|
||||
|
|
@ -55,7 +56,7 @@ type Journal struct {
|
|||
// NewJournal creates a new Journal rooted at baseDir.
|
||||
func NewJournal(baseDir string) (*Journal, error) {
|
||||
if baseDir == "" {
|
||||
return nil, errors.New("journal.NewJournal: base directory is required")
|
||||
return nil, coreerr.E("journal.NewJournal", "base directory is required", nil)
|
||||
}
|
||||
return &Journal{baseDir: baseDir}, nil
|
||||
}
|
||||
|
|
@ -66,12 +67,12 @@ func NewJournal(baseDir string) (*Journal, error) {
|
|||
func sanitizePathComponent(name string) (string, error) {
|
||||
// Reject empty or whitespace-only values.
|
||||
if name == "" || strings.TrimSpace(name) == "" {
|
||||
return "", fmt.Errorf("journal.sanitizePathComponent: invalid path component: %q", name)
|
||||
return "", coreerr.E("journal.sanitizePathComponent", "invalid path component: "+name, nil)
|
||||
}
|
||||
|
||||
// Reject inputs containing path separators (directory traversal attempt).
|
||||
if strings.ContainsAny(name, `/\`) {
|
||||
return "", fmt.Errorf("journal.sanitizePathComponent: path component contains directory separator: %q", name)
|
||||
return "", coreerr.E("journal.sanitizePathComponent", "path component contains directory separator: "+name, nil)
|
||||
}
|
||||
|
||||
// Use filepath.Clean to normalize (e.g., collapse redundant dots).
|
||||
|
|
@ -79,12 +80,12 @@ func sanitizePathComponent(name string) (string, error) {
|
|||
|
||||
// Reject traversal components.
|
||||
if clean == "." || clean == ".." {
|
||||
return "", fmt.Errorf("journal.sanitizePathComponent: invalid path component: %q", name)
|
||||
return "", coreerr.E("journal.sanitizePathComponent", "invalid path component: "+name, nil)
|
||||
}
|
||||
|
||||
// Validate against the safe character set.
|
||||
if !validPathComponent.MatchString(clean) {
|
||||
return "", fmt.Errorf("journal.sanitizePathComponent: path component contains invalid characters: %q", name)
|
||||
return "", coreerr.E("journal.sanitizePathComponent", "path component contains invalid characters: "+name, nil)
|
||||
}
|
||||
|
||||
return clean, nil
|
||||
|
|
@ -122,10 +123,10 @@ func (j *Journal) ReadEntries(path string) iter.Seq2[JournalEntry, error] {
|
|||
// Append writes a journal entry for the given signal and result.
|
||||
func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error {
|
||||
if signal == nil {
|
||||
return errors.New("journal.Append: signal is required")
|
||||
return coreerr.E("journal.Append", "signal is required", nil)
|
||||
}
|
||||
if result == nil {
|
||||
return errors.New("journal.Append: result is required")
|
||||
return coreerr.E("journal.Append", "result is required", nil)
|
||||
}
|
||||
|
||||
entry := JournalEntry{
|
||||
|
|
@ -153,18 +154,18 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error {
|
|||
|
||||
data, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("journal.Append: marshal entry: %w", err)
|
||||
return coreerr.E("journal.Append", "marshal entry", err)
|
||||
}
|
||||
data = append(data, '\n')
|
||||
|
||||
// Sanitize path components to prevent path traversal (CVE: issue #46).
|
||||
owner, err := sanitizePathComponent(signal.RepoOwner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("journal.Append: invalid repo owner: %w", err)
|
||||
return coreerr.E("journal.Append", "invalid repo owner", err)
|
||||
}
|
||||
repo, err := sanitizePathComponent(signal.RepoName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("journal.Append: invalid repo name: %w", err)
|
||||
return coreerr.E("journal.Append", "invalid repo name", err)
|
||||
}
|
||||
|
||||
date := result.Timestamp.UTC().Format("2006-01-02")
|
||||
|
|
@ -173,27 +174,27 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error {
|
|||
// Resolve to absolute path and verify it stays within baseDir.
|
||||
absBase, err := filepath.Abs(j.baseDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("journal.Append: resolve base directory: %w", err)
|
||||
return coreerr.E("journal.Append", "resolve base directory", err)
|
||||
}
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("journal.Append: resolve journal directory: %w", err)
|
||||
return coreerr.E("journal.Append", "resolve journal directory", err)
|
||||
}
|
||||
if !strings.HasPrefix(absDir, absBase+string(filepath.Separator)) {
|
||||
return fmt.Errorf("journal.Append: path %q escapes base directory %q", absDir, absBase)
|
||||
return coreerr.E("journal.Append", "path escapes base directory: "+absDir, nil)
|
||||
}
|
||||
|
||||
j.mu.Lock()
|
||||
defer j.mu.Unlock()
|
||||
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("journal.Append: create directory: %w", err)
|
||||
if err := coreio.Local.EnsureDir(dir); err != nil {
|
||||
return coreerr.E("journal.Append", "create directory", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, date+".jsonl")
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("journal.Append: open file: %w", err)
|
||||
return coreerr.E("journal.Append", "open file", err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ func runCommandCtx(ctx context.Context, dir string, command string, args ...stri
|
|||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if stderr.Len() > 0 {
|
||||
return "", fmt.Errorf("%w: %s", err, stderr.String())
|
||||
return "", log.E("runCommandCtx", stderr.String(), err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ package lifecycle
|
|||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
// ErrNoEligibleAgent is returned when no agent matches the task requirements.
|
||||
var ErrNoEligibleAgent = errors.New("no eligible agent for task")
|
||||
var ErrNoEligibleAgent = coreerr.E("TaskRouter", "no eligible agent for task", nil)
|
||||
|
||||
// TaskRouter selects an agent for a given task from a list of candidates.
|
||||
type TaskRouter interface {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"forge.lthn.ai/core/go-inference"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
// Engine drives the agent loop: prompt the model, parse tool calls, execute
|
||||
|
|
@ -56,7 +57,7 @@ func New(opts ...Option) *Engine {
|
|||
// until the model produces a response with no tool blocks or maxTurns is hit.
|
||||
func (e *Engine) Run(ctx context.Context, userMessage string) (*Result, error) {
|
||||
if e.model == nil {
|
||||
return nil, fmt.Errorf("loop: no model configured")
|
||||
return nil, coreerr.E("loop.Run", "no model configured", nil)
|
||||
}
|
||||
|
||||
system := e.system
|
||||
|
|
@ -74,7 +75,7 @@ func (e *Engine) Run(ctx context.Context, userMessage string) (*Result, error) {
|
|||
|
||||
for turn := 0; turn < e.maxTurns; turn++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("loop: context cancelled: %w", err)
|
||||
return nil, coreerr.E("loop.Run", "context cancelled", err)
|
||||
}
|
||||
|
||||
prompt := BuildFullPrompt(system, history, "")
|
||||
|
|
@ -83,7 +84,7 @@ func (e *Engine) Run(ctx context.Context, userMessage string) (*Result, error) {
|
|||
response.WriteString(tok.Text)
|
||||
}
|
||||
if err := e.model.Err(); err != nil {
|
||||
return nil, fmt.Errorf("loop: inference error: %w", err)
|
||||
return nil, coreerr.E("loop.Run", "inference error", err)
|
||||
}
|
||||
|
||||
fullResponse := response.String()
|
||||
|
|
@ -127,5 +128,5 @@ func (e *Engine) Run(ctx context.Context, userMessage string) (*Result, error) {
|
|||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("loop: max turns (%d) exceeded", e.maxTurns)
|
||||
return nil, coreerr.E("loop.Run", fmt.Sprintf("max turns (%d) exceeded", e.maxTurns), nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
var eaasClient = &http.Client{Timeout: 30 * time.Second}
|
||||
|
|
@ -59,28 +61,28 @@ func eaasPostHandler(baseURL, path string) func(context.Context, map[string]any)
|
|||
return func(ctx context.Context, args map[string]any) (string, error) {
|
||||
body, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal args: %w", err)
|
||||
return "", coreerr.E("eaas.handler", "marshal args", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", baseURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create request: %w", err)
|
||||
return "", coreerr.E("eaas.handler", "create request", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := eaasClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("eaas request: %w", err)
|
||||
return "", coreerr.E("eaas.handler", "eaas request", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
result, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read response: %w", err)
|
||||
return "", coreerr.E("eaas.handler", "read response", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("eaas returned %d: %s", resp.StatusCode, string(result))
|
||||
return "", coreerr.E("eaas.handler", fmt.Sprintf("eaas returned %d: %s", resp.StatusCode, string(result)), nil)
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package loop
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
aimcp "forge.lthn.ai/core/mcp/pkg/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ func WrapRESTHandler(handler RESTHandlerFunc) func(context.Context, map[string]a
|
|||
return func(ctx context.Context, args map[string]any) (string, error) {
|
||||
body, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal args: %w", err)
|
||||
return "", coreerr.E("mcp.handler", "marshal args", err)
|
||||
}
|
||||
|
||||
result, err := handler(ctx, body)
|
||||
|
|
@ -40,7 +40,7 @@ func WrapRESTHandler(handler RESTHandlerFunc) func(context.Context, map[string]a
|
|||
|
||||
out, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal result: %w", err)
|
||||
return "", coreerr.E("mcp.handler", "marshal result", err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
package orchestrator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/config"
|
||||
)
|
||||
|
||||
|
|
@ -46,7 +45,7 @@ func LoadAgents(cfg *config.Config) (map[string]AgentConfig, error) {
|
|||
continue
|
||||
}
|
||||
if ac.Host == "" {
|
||||
return nil, fmt.Errorf("agentci.LoadAgents: agent %q: host is required", name)
|
||||
return nil, coreerr.E("agentci.LoadAgents", "agent "+name+": host is required", nil)
|
||||
}
|
||||
if ac.QueueDir == "" {
|
||||
ac.QueueDir = "/home/claude/ai-work/queue"
|
||||
|
|
@ -96,7 +95,7 @@ func LoadClothoConfig(cfg *config.Config) (ClothoConfig, error) {
|
|||
|
||||
// SaveAgent writes an agent config entry to the config file.
|
||||
func SaveAgent(cfg *config.Config, name string, ac AgentConfig) error {
|
||||
key := fmt.Sprintf("agentci.agents.%s", name)
|
||||
key := "agentci.agents." + name
|
||||
data := map[string]any{
|
||||
"host": ac.Host,
|
||||
"queue_dir": ac.QueueDir,
|
||||
|
|
@ -126,10 +125,10 @@ func SaveAgent(cfg *config.Config, name string, ac AgentConfig) error {
|
|||
func RemoveAgent(cfg *config.Config, name string) error {
|
||||
var agents map[string]AgentConfig
|
||||
if err := cfg.Get("agentci.agents", &agents); err != nil {
|
||||
return errors.New("agentci.RemoveAgent: no agents configured")
|
||||
return coreerr.E("agentci.RemoveAgent", "no agents configured", nil)
|
||||
}
|
||||
if _, ok := agents[name]; !ok {
|
||||
return fmt.Errorf("agentci.RemoveAgent: agent %q not found", name)
|
||||
return coreerr.E("agentci.RemoveAgent", "agent "+name+" not found", nil)
|
||||
}
|
||||
delete(agents, name)
|
||||
return cfg.Set("agentci.agents", agents)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package orchestrator
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
||||
var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`)
|
||||
|
|
@ -16,10 +17,10 @@ var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`)
|
|||
func SanitizePath(input string) (string, error) {
|
||||
base := filepath.Base(input)
|
||||
if !safeNameRegex.MatchString(base) {
|
||||
return "", fmt.Errorf("agentci.SanitizePath: invalid characters in path element: %s", input)
|
||||
return "", coreerr.E("agentci.SanitizePath", "invalid characters in path element: "+input, nil)
|
||||
}
|
||||
if base == "." || base == ".." || base == "/" {
|
||||
return "", fmt.Errorf("agentci.SanitizePath: invalid path element: %s", base)
|
||||
return "", coreerr.E("agentci.SanitizePath", "invalid path element: "+base, nil)
|
||||
}
|
||||
return base, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue