diff --git a/core-agent b/core-agent index 579736b..7b86f68 100755 Binary files a/core-agent and b/core-agent differ diff --git a/pkg/agentic/auto_pr.go b/pkg/agentic/auto_pr.go new file mode 100644 index 0000000..2771bfd --- /dev/null +++ b/pkg/agentic/auto_pr.go @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "context" + "fmt" + "os/exec" + "path/filepath" + "strings" + "time" +) + +// autoCreatePR pushes the agent's branch and creates a PR on Forge +// if the agent made any commits beyond the initial clone. +func (s *PrepSubsystem) autoCreatePR(wsDir string) { + st, err := readStatus(wsDir) + if err != nil || st.Branch == "" || st.Repo == "" { + return + } + + srcDir := filepath.Join(wsDir, "src") + + // Check if there are commits on the branch beyond origin/main + diffCmd := exec.Command("git", "log", "--oneline", "origin/main..HEAD") + diffCmd.Dir = srcDir + out, err := diffCmd.Output() + if err != nil || len(strings.TrimSpace(string(out))) == 0 { + // No commits — nothing to PR + return + } + + commitCount := len(strings.Split(strings.TrimSpace(string(out)), "\n")) + + // Get the repo's forge remote URL to extract org/repo + org := st.Org + if org == "" { + org = "core" + } + + // Push the branch to forge + forgeRemote := fmt.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, st.Repo) + pushCmd := exec.Command("git", "push", forgeRemote, st.Branch) + pushCmd.Dir = srcDir + if pushErr := pushCmd.Run(); pushErr != nil { + // Push failed — update status with error but don't block + if st2, err := readStatus(wsDir); err == nil { + st2.Question = fmt.Sprintf("PR push failed: %v", pushErr) + writeStatus(wsDir, st2) + } + return + } + + // Create PR via Forge API + title := fmt.Sprintf("[agent/%s] %s", st.Agent, truncate(st.Task, 60)) + body := s.buildAutoPRBody(st, commitCount) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + prURL, _, err := s.forgeCreatePR(ctx, org, st.Repo, st.Branch, "main", title, body) + if err != nil { + if st2, err := readStatus(wsDir); err == nil { + st2.Question = fmt.Sprintf("PR creation failed: %v", err) + writeStatus(wsDir, st2) + } + return + } + + // Update status with PR URL + if st2, err := readStatus(wsDir); err == nil { + st2.PRURL = prURL + writeStatus(wsDir, st2) + } +} + +func (s *PrepSubsystem) buildAutoPRBody(st *WorkspaceStatus, commits int) string { + var b strings.Builder + b.WriteString("## Task\n\n") + b.WriteString(st.Task) + b.WriteString("\n\n") + b.WriteString(fmt.Sprintf("**Agent:** %s\n", st.Agent)) + b.WriteString(fmt.Sprintf("**Commits:** %d\n", commits)) + b.WriteString(fmt.Sprintf("**Branch:** `%s`\n", st.Branch)) + b.WriteString("\n---\n") + b.WriteString("Auto-created by core-agent dispatch system.\n") + b.WriteString("Co-Authored-By: Virgil \n") + return b.String() +} + +func truncate(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max] + "..." +} diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index af183fa..85e3e33 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -132,6 +132,9 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st // Emit completion event emitCompletionEvent(agent, filepath.Base(wsDir)) + // Auto-create PR if agent made commits + s.autoCreatePR(wsDir) + // Ingest scan findings as issues s.ingestFindings(wsDir) @@ -202,6 +205,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, Repo: input.Repo, Org: input.Org, Task: input.Task, + Branch: prepOut.Branch, StartedAt: time.Now(), Runs: 0, }) @@ -226,6 +230,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, Repo: input.Repo, Org: input.Org, Task: input.Task, + Branch: prepOut.Branch, PID: pid, StartedAt: time.Now(), Runs: 1, diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 9584b8c..438fe76 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -114,6 +114,7 @@ type PrepInput struct { type PrepOutput struct { Success bool `json:"success"` WorkspaceDir string `json:"workspace_dir"` + Branch string `json:"branch"` WikiPages int `json:"wiki_pages"` SpecFiles int `json:"spec_files"` Memories int `json:"memories"` @@ -171,6 +172,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques branchCmd := exec.CommandContext(ctx, "git", "checkout", "-b", branchName) branchCmd.Dir = srcDir branchCmd.Run() + out.Branch = branchName // Create context dirs inside src/ coreio.Local.EnsureDir(filepath.Join(srcDir, "kb"))