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>
96 lines
2.6 KiB
Go
96 lines
2.6 KiB
Go
// 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] + "..."
|
|
}
|