diff options
| author | Claudomator Agent <agent@claudomator.local> | 2026-06-05 09:42:56 +0000 |
|---|---|---|
| committer | Claudomator Agent <agent@claudomator.local> | 2026-06-05 09:42:56 +0000 |
| commit | 3d286974cdc28c68c5ee536ce9303899dae9540e (patch) | |
| tree | c24d1729bebfee142e5521cdc1c91d00bd90c7fd /internal/executor/local.go | |
| parent | 6ee7bd83433eacabfbf948ab9ecacb22bcd705ae (diff) | |
feat(local-runner): add file I/O tools and git sandbox
Add read_file, write_file, run_bash, and glob tool definitions to
agentToolDefs() in localtools.go, with dispatch in dispatchAgentTool()
(signature now includes workDir). File and bash tools return an error
when workDir is empty.
LocalRunner.Run() clones t.Agent.ProjectDir into a temp dir when set,
passes workDir to dispatchAgentTool, pushes commits back on success,
preserves the sandbox in e.SandboxDir on BLOCKED, and cleans up on
completion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/executor/local.go')
| -rw-r--r-- | internal/executor/local.go | 42 |
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. |
