feat(daemon): add per-task isolated execution environments

Introduce the `execenv` package that creates isolated working directories
for each agent task. Supports git worktree mode (code tasks) and plain
directory mode (non-code tasks), with `.agent_context/issue_context.md`
injected into the workdir for Claude Code to discover.

Key changes:
- New `server/internal/daemon/execenv/` package (Prepare/Cleanup)
- `runTask()` now creates isolated env instead of using shared reposRoot
- Prompt updated to reference `.agent_context/` files
- Add `WorkspacesRoot` config (default ~/multica_workspaces)
- Add `KeepEnvAfterTask` config for debugging
- Default agent timeout increased from 20min to 2h
- `CompleteTask` now forwards branch name to server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jiayuan Zhang 2026-03-25 12:41:52 +08:00
parent 0ce25597d6
commit 678266ec87
10 changed files with 841 additions and 66 deletions

View file

@ -0,0 +1,69 @@
package execenv
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// writeContextFiles renders and writes .agent_context/issue_context.md into workDir.
func writeContextFiles(workDir string, ctx TaskContextForEnv) error {
contextDir := filepath.Join(workDir, ".agent_context")
if err := os.MkdirAll(contextDir, 0o755); err != nil {
return fmt.Errorf("create .agent_context dir: %w", err)
}
content := renderIssueContext(ctx)
path := filepath.Join(contextDir, "issue_context.md")
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
return fmt.Errorf("write issue_context.md: %w", err)
}
return nil
}
// renderIssueContext builds the markdown content for issue_context.md.
// Sections with empty content are omitted.
func renderIssueContext(ctx TaskContextForEnv) string {
var b strings.Builder
if ctx.IssueTitle != "" {
fmt.Fprintf(&b, "# Issue: %s\n\n", ctx.IssueTitle)
}
if ctx.IssueDescription != "" {
b.WriteString("## Description\n\n")
b.WriteString(ctx.IssueDescription)
b.WriteString("\n\n")
}
if len(ctx.AcceptanceCriteria) > 0 {
b.WriteString("## Acceptance Criteria\n\n")
for _, item := range ctx.AcceptanceCriteria {
fmt.Fprintf(&b, "- %s\n", item)
}
b.WriteString("\n")
}
if len(ctx.ContextRefs) > 0 {
b.WriteString("## Context References\n\n")
for _, ref := range ctx.ContextRefs {
fmt.Fprintf(&b, "- %s\n", ref)
}
b.WriteString("\n")
}
if ctx.WorkspaceContext != "" {
b.WriteString("## Workspace Context\n\n")
b.WriteString(ctx.WorkspaceContext)
b.WriteString("\n\n")
}
if ctx.AgentSkills != "" {
b.WriteString("## Agent Instructions\n\n")
b.WriteString(ctx.AgentSkills)
b.WriteString("\n")
}
return b.String()
}