feat(daemon): agent-driven repo checkout with bare clone cache
Agents now decide which repo to use based on issue context and check out repos on demand via `multica repo checkout <url>`. Workspace repos are cached locally as bare clones for fast worktree creation. Key changes: - Add repocache package for bare clone management (clone, fetch, worktree) - Add `multica repo checkout` CLI command that talks to local daemon - Add POST /repo/checkout endpoint on daemon health server - Pass workspace repos metadata through register + task claim responses - Remove pre-created worktrees from execenv (workdir starts empty) - Update CLAUDE.md template to instruct agents to use `multica repo checkout` - Pass MULTICA_DAEMON_PORT, WORKSPACE_ID, AGENT_NAME, TASK_ID env vars to agent
This commit is contained in:
parent
ab4058b1e4
commit
cdc1ac708e
15 changed files with 1064 additions and 255 deletions
92
server/cmd/multica/cmd_repo.go
Normal file
92
server/cmd/multica/cmd_repo.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var repoCmd = &cobra.Command{
|
||||
Use: "repo",
|
||||
Short: "Manage repositories",
|
||||
}
|
||||
|
||||
var repoCheckoutCmd = &cobra.Command{
|
||||
Use: "checkout <url>",
|
||||
Short: "Check out a repository into the working directory",
|
||||
Long: "Creates a git worktree from the daemon's bare clone cache. Used by agents to check out repos on demand.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runRepoCheckout,
|
||||
}
|
||||
|
||||
func init() {
|
||||
repoCmd.AddCommand(repoCheckoutCmd)
|
||||
}
|
||||
|
||||
func runRepoCheckout(cmd *cobra.Command, args []string) error {
|
||||
repoURL := args[0]
|
||||
|
||||
daemonPort := os.Getenv("MULTICA_DAEMON_PORT")
|
||||
if daemonPort == "" {
|
||||
return fmt.Errorf("MULTICA_DAEMON_PORT not set (this command is intended to be run by an agent inside a daemon task)")
|
||||
}
|
||||
|
||||
workspaceID := os.Getenv("MULTICA_WORKSPACE_ID")
|
||||
agentName := os.Getenv("MULTICA_AGENT_NAME")
|
||||
taskID := os.Getenv("MULTICA_TASK_ID")
|
||||
|
||||
// Use current working directory as the checkout target.
|
||||
workDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get working directory: %w", err)
|
||||
}
|
||||
|
||||
reqBody := map[string]string{
|
||||
"url": repoURL,
|
||||
"workspace_id": workspaceID,
|
||||
"workdir": workDir,
|
||||
"agent_name": agentName,
|
||||
"task_id": taskID,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode request: %w", err)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Minute}
|
||||
resp, err := client.Post(
|
||||
fmt.Sprintf("http://127.0.0.1:%s/repo/checkout", daemonPort),
|
||||
"application/json",
|
||||
bytes.NewReader(data),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to daemon: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("checkout failed: %s", string(body))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Path string `json:"path"`
|
||||
BranchName string `json:"branch_name"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return fmt.Errorf("parse response: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stdout, "%s\n", result.Path)
|
||||
fmt.Fprintf(os.Stderr, "Checked out %s → %s (branch: %s)\n", repoURL, result.Path, result.BranchName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ func init() {
|
|||
rootCmd.AddCommand(workspaceCmd)
|
||||
rootCmd.AddCommand(configCmd)
|
||||
rootCmd.AddCommand(issueCmd)
|
||||
rootCmd.AddCommand(repoCmd)
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue