fix: heartbeat worktree uses stale plan dir and blocks workspace reads

Heartbeat created worktrees against GetPlanWorkDir() which could return
a stale path from a previous plan (e.g. a different repository), causing
gh commands to target the wrong repo. Now always uses agent.Workspace.

Also fix resolveFS to allow reading absolute paths under the original
workspace when operating inside a worktree, preventing "path escapes
workspace" errors for files like HEARTBEAT.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-03-15 17:14:04 +09:00
parent 56d70c6073
commit 2882b91e7c
2 changed files with 13 additions and 2 deletions

View file

@ -2511,10 +2511,11 @@ func (al *AgentLoop) executeToolCalls(
}) })
// Heartbeat lazy worktree: create worktree on first write-tool call // Heartbeat lazy worktree: create worktree on first write-tool call
// Always use ai.Workspace (not GetPlanWorkDir) to avoid creating worktrees
// against stale project paths from previous plans.
if opts.Background && isWriteTool(tc.Name) && !agent.IsInWorktree(opts.SessionKey) { if opts.Background && isWriteTool(tc.Name) && !agent.IsInWorktree(opts.SessionKey) {
taskName := "heartbeat-" + time.Now().Format("20060102") taskName := "heartbeat-" + time.Now().Format("20060102")
hbDir := agent.ContextBuilder.GetPlanWorkDir() if wt, wtErr := agent.ActivateWorktree(opts.SessionKey, taskName, agent.Workspace); wtErr == nil {
if wt, wtErr := agent.ActivateWorktree(opts.SessionKey, taskName, hbDir); wtErr == nil {
logger.InfoCF("agent", "Heartbeat worktree created", map[string]any{"branch": wt.Branch}) logger.InfoCF("agent", "Heartbeat worktree created", map[string]any{"branch": wt.Branch})
} }
} }

View file

@ -61,6 +61,16 @@ func resolveFS(ctx context.Context, fs fileSystem, path string) fileSystem {
return fs return fs
} }
// Absolute paths under the original workspace use original fs
// (e.g. heartbeat reading workspace files while in a worktree)
if filepath.IsAbs(path) {
if sfs, ok := fs.(*sandboxFs); ok {
if strings.HasPrefix(path, sfs.workspace+"/") || path == sfs.workspace {
return fs
}
}
}
// Only sandboxFs supports workspace override // Only sandboxFs supports workspace override
if sfs, ok := fs.(*sandboxFs); ok { if sfs, ok := fs.(*sandboxFs); ok {