Skip to content

Commit 3a73ea3

Browse files
ddx-checkpointclaude
andcommitted
fix(worker): seed lifecycle readiness scratch worktree from HEAD [ddx-efadca32]
The lifecycle readiness-check classifier ran in an empty scratch repo (git init, no source), so file-presence checks blocked every bead with 'target file not found in working directory'. Seed the scratch from a detached HEAD worktree so it carries the project's real source while staying isolated from the master worktree; fall back to an empty repo only when HEAD is unborn, and deregister the worktree on cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5b074a0 commit 3a73ea3

2 files changed

Lines changed: 56 additions & 4 deletions

File tree

cli/internal/agent/lifecycle_dispatch.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ func dispatchLifecycleRun(ctx context.Context, projectRoot string, svc agentlib.
3232
if err != nil {
3333
return nil, fmt.Errorf("lifecycle dispatch: create scratch workdir: %w", err)
3434
}
35-
defer func() { _ = os.RemoveAll(scratchDir) }()
35+
defer func() {
36+
// Deregister the seeded worktree (no-op/ignored if the fallback empty
37+
// repo was used) before removing the directory, so it does not linger
38+
// in the project's worktree registry.
39+
_ = internalgit.Command(context.Background(), projectRoot, "worktree", "remove", "--force", scratchDir).Run()
40+
_ = os.RemoveAll(scratchDir)
41+
}()
3642

3743
runtime.WorkDir = scratchDir
3844
runtime.PermissionsOverride = PermissionsReadOnlyLifecycle
@@ -58,9 +64,25 @@ func newLifecycleScratchDir(projectRoot string) (string, error) {
5864
if err != nil {
5965
return "", err
6066
}
61-
if out, err := internalgit.Command(context.Background(), dir, "init", "-q").CombinedOutput(); err != nil {
62-
_ = os.RemoveAll(dir)
63-
return "", fmt.Errorf("initialize lifecycle scratch git repo: %s: %w", strings.TrimSpace(string(out)), err)
67+
// Seed the scratch workdir from the project's HEAD so the readiness-check
68+
// classifier sees the project's real source files rather than an empty repo
69+
// — otherwise file-presence checks block every bead (ddx-efadca32). A
70+
// detached worktree keeps the classifier isolated from the master worktree
71+
// (it cannot mutate tracked files there). git worktree add must create the
72+
// directory itself, so clear the placeholder MkdirExecutionScratch made.
73+
if rmErr := os.RemoveAll(dir); rmErr != nil {
74+
return "", fmt.Errorf("reset lifecycle scratch dir: %w", rmErr)
75+
}
76+
if out, addErr := internalgit.Command(context.Background(), projectRoot, "worktree", "add", "--detach", dir, "HEAD").CombinedOutput(); addErr != nil {
77+
// HEAD may be unborn (a project with no commits yet); readiness has
78+
// nothing to check there, so fall back to an empty scratch repo.
79+
if mkErr := os.MkdirAll(dir, 0o700); mkErr != nil {
80+
return "", fmt.Errorf("recreate lifecycle scratch dir after worktree add failed (%s): %w", strings.TrimSpace(string(out)), mkErr)
81+
}
82+
if out2, initErr := internalgit.Command(context.Background(), dir, "init", "-q").CombinedOutput(); initErr != nil {
83+
_ = os.RemoveAll(dir)
84+
return "", fmt.Errorf("initialize lifecycle scratch git repo: %s: %w", strings.TrimSpace(string(out2)), initErr)
85+
}
6486
}
6587
return dir, nil
6688
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package agent
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestNewLifecycleScratchDirSeedsFromHEAD verifies the lifecycle readiness
13+
// scratch worktree is seeded with the project's HEAD source instead of an empty
14+
// repo. An empty scratch made the readiness-check classifier block every bead
15+
// with "target file not found in working directory" (ddx-efadca32).
16+
func TestNewLifecycleScratchDirSeedsFromHEAD(t *testing.T) {
17+
projectRoot, _ := newScriptHarnessRepo(t, 1)
18+
19+
dir, err := newLifecycleScratchDir(projectRoot)
20+
require.NoError(t, err)
21+
t.Cleanup(func() { _ = os.RemoveAll(dir) })
22+
23+
// The file committed at HEAD must be present in the scratch worktree —
24+
// the bug left only an empty .git here.
25+
seed := filepath.Join(dir, "seed.txt")
26+
require.FileExists(t, seed)
27+
content, err := os.ReadFile(seed)
28+
require.NoError(t, err)
29+
assert.Equal(t, "seed\n", string(content))
30+
}

0 commit comments

Comments
 (0)