From 2882b91e7c6bde73a0fc60b167dae4ccda602c1d Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:14:04 +0900 Subject: [PATCH] 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) --- pkg/agent/loop.go | 5 +++-- pkg/tools/workspace_ctx.go | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 025c03173..b3e92144e 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -2511,10 +2511,11 @@ func (al *AgentLoop) executeToolCalls( }) // 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) { taskName := "heartbeat-" + time.Now().Format("20060102") - hbDir := agent.ContextBuilder.GetPlanWorkDir() - if wt, wtErr := agent.ActivateWorktree(opts.SessionKey, taskName, hbDir); wtErr == nil { + if wt, wtErr := agent.ActivateWorktree(opts.SessionKey, taskName, agent.Workspace); wtErr == nil { logger.InfoCF("agent", "Heartbeat worktree created", map[string]any{"branch": wt.Branch}) } } diff --git a/pkg/tools/workspace_ctx.go b/pkg/tools/workspace_ctx.go index 9460431a9..b8657647f 100644 --- a/pkg/tools/workspace_ctx.go +++ b/pkg/tools/workspace_ctx.go @@ -61,6 +61,16 @@ func resolveFS(ctx context.Context, fs fileSystem, path string) fileSystem { 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 if sfs, ok := fs.(*sandboxFs); ok {