summaryrefslogtreecommitdiff
path: root/internal/executor/local.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/executor/local.go')
-rw-r--r--internal/executor/local.go42
1 files changed, 38 insertions, 4 deletions
diff --git a/internal/executor/local.go b/internal/executor/local.go
index 6a240a4..43e76d3 100644
--- a/internal/executor/local.go
+++ b/internal/executor/local.go
@@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"os"
+ "os/exec"
"path/filepath"
"strings"
"time"
@@ -73,6 +74,27 @@ func (r *LocalRunner) Run(ctx context.Context, t *task.Task, e *storage.Executio
}
defer stdout.Close()
+ // --- Git sandbox setup ---
+ var workDir string
+ if t.Agent.ProjectDir != "" {
+ // Reuse existing sandbox on resume (BLOCKED → QUEUED path).
+ if e.SandboxDir != "" {
+ workDir = e.SandboxDir
+ } else {
+ tmpDir, tmpErr := os.MkdirTemp("", "claudomator-local-*")
+ if tmpErr != nil {
+ return fmt.Errorf("local runner: create sandbox temp dir: %w", tmpErr)
+ }
+ cloneCmd := exec.CommandContext(ctx, "git", "clone", t.Agent.ProjectDir, tmpDir)
+ if cloneOut, cloneErr := cloneCmd.CombinedOutput(); cloneErr != nil {
+ os.RemoveAll(tmpDir)
+ return fmt.Errorf("local runner: git clone: %w\n%s", cloneErr, cloneOut)
+ }
+ workDir = tmpDir
+ e.SandboxDir = workDir
+ }
+ }
+
temperature := t.Agent.Temperature
if temperature == nil && r.DefaultTemperature > 0 {
v := r.DefaultTemperature
@@ -140,7 +162,7 @@ func (r *LocalRunner) Run(ctx context.Context, t *task.Task, e *storage.Executio
// Re-feed: record the assistant's tool-call turn, then each tool result.
messages = append(messages, llm.Message{Role: "assistant", Content: resp.Content, ToolCalls: resp.ToolCalls})
for _, tc := range resp.ToolCalls {
- result, didBlock, toolErr := dispatchAgentTool(ctx, ch, tc.Function.Name, tc.Function.Arguments)
+ result, didBlock, toolErr := dispatchAgentTool(ctx, ch, workDir, tc.Function.Name, tc.Function.Arguments)
if toolErr != nil {
writeResultLine(stdout, "error", toolErr.Error(), totalIn, totalOut)
return fmt.Errorf("local runner: tool %s: %w", tc.Function.Name, toolErr)
@@ -178,13 +200,25 @@ func (r *LocalRunner) Run(ctx context.Context, t *task.Task, e *storage.Executio
)
}
- // If the model asked the user, stop and block. Local resume re-feeds the
- // conversation (per-runner sovereign state) — not yet wired, so the session
- // ID is empty and resume starts fresh for now.
+ // If the model asked the user, stop and block. Preserve the sandbox so
+ // resume can reuse it.
if q, isBlocked := channelPendingQuestion(ch); isBlocked {
+ // workDir is already stored in e.SandboxDir; don't clean up.
return &BlockedError{QuestionJSON: q}
}
+ // Push any commits made inside the sandbox back to the origin (project_dir).
+ if workDir != "" {
+ pushCmd := exec.CommandContext(ctx, "git", "-C", workDir, "push", "origin", "HEAD")
+ if pushOut, pushErr := pushCmd.CombinedOutput(); pushErr != nil {
+ if r.Logger != nil {
+ r.Logger.Warn("local runner: git push failed", "err", pushErr, "output", string(pushOut))
+ }
+ }
+ os.RemoveAll(workDir)
+ e.SandboxDir = ""
+ }
+
// For tool-less models report_summary is never called, so synthesise a
// summary from the raw assistant output so handleRunResult has something to
// store.