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:
Jiayuan 2026-03-29 19:37:48 +08:00
parent ab4058b1e4
commit cdc1ac708e
15 changed files with 1064 additions and 255 deletions

View file

@ -81,6 +81,13 @@ func agentToResponse(a db.Agent) AgentResponse {
}
}
// RepoData holds repository information included in claim responses so the
// daemon can set up worktrees for each workspace repo.
type RepoData struct {
URL string `json:"url"`
Description string `json:"description"`
}
type AgentTaskResponse struct {
ID string `json:"id"`
AgentID string `json:"agent_id"`
@ -94,6 +101,7 @@ type AgentTaskResponse struct {
Result any `json:"result"`
Error *string `json:"error"`
Agent *TaskAgentData `json:"agent,omitempty"`
Repos []RepoData `json:"repos,omitempty"`
CreatedAt string `json:"created_at"`
PriorSessionID string `json:"prior_session_id,omitempty"` // session ID from a previous task on same issue
}

View file

@ -51,7 +51,8 @@ func (h *Handler) DaemonRegister(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "at least one runtime is required")
return
}
if _, err := h.Queries.GetWorkspace(r.Context(), parseUUID(req.WorkspaceID)); err != nil {
ws, err := h.Queries.GetWorkspace(r.Context(), parseUUID(req.WorkspaceID))
if err != nil {
writeError(w, http.StatusNotFound, "workspace not found")
return
}
@ -106,7 +107,16 @@ func (h *Handler) DaemonRegister(w http.ResponseWriter, r *http.Request) {
"runtimes": resp,
})
writeJSON(w, http.StatusOK, map[string]any{"runtimes": resp})
// Include workspace repos so the daemon can cache them locally.
var repos []RepoData
if ws.Repos != nil {
json.Unmarshal(ws.Repos, &repos)
}
if repos == nil {
repos = []RepoData{}
}
writeJSON(w, http.StatusOK, map[string]any{"runtimes": resp, "repos": repos})
}
// DaemonDeregister marks runtimes as offline when the daemon shuts down.
@ -217,6 +227,16 @@ func (h *Handler) ClaimTaskByRuntime(w http.ResponseWriter, r *http.Request) {
}
}
// Include workspace repos so the daemon can set up worktrees.
if issue, err := h.Queries.GetIssue(r.Context(), task.IssueID); err == nil {
if ws, err := h.Queries.GetWorkspace(r.Context(), issue.WorkspaceID); err == nil && ws.Repos != nil {
var repos []RepoData
if json.Unmarshal(ws.Repos, &repos) == nil && len(repos) > 0 {
resp.Repos = repos
}
}
}
// Look up the prior session for this (agent, issue) pair so the daemon
// can resume the Claude Code conversation context.
if prior, err := h.Queries.GetLastTaskSession(r.Context(), db.GetLastTaskSessionParams{