feat: auto-create PR on Forge after agent completion
When a dispatched agent completes with commits: 1. Branch name threaded through PrepOutput → status.json 2. Completion goroutine pushes branch to forge 3. Auto-creates PR via Forge API with task description 4. PR URL stored in status.json for review Agents now create PRs instead of committing to main. Combined with sandbox restrictions, this closes the loop on controlled agent contributions. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
da1c45b4df
commit
71decc26b2
4 changed files with 103 additions and 0 deletions
BIN
core-agent
BIN
core-agent
Binary file not shown.
96
pkg/agentic/auto_pr.go
Normal file
96
pkg/agentic/auto_pr.go
Normal file
|
|
@ -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 <virgil@lethean.io>\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max] + "..."
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue