# fix: validate adopted transcript ownership

`f4e8ee6`·

peyton-alt·3w ago·4 files·+91 added/-7 removed

## Sessions

ae0dfea989e0View transcript

## Changes

4

- cmd/entire/cli

- checkpoint

- Mopen.go+1/-1

- Msession_adopt.go+21

- Msession_adopt_test.go+67/-4

- strategy

- Mmanual_commit.go+2/-2

```
54 unmodified lines

55
56
57
58
58
59
60
61

54 unmodified lines

}

// Temporary returns the git-backed temporary shadow-branch store.
func (s *Stores) Temporary() TemporaryStore { return s.temporary } //nolint:ireturn // temporary store capability is the abstraction boundary
func (s *Stores) Temporary() TemporaryStore { return s.temporary }

// Refs returns the resolved committed-ref topology.
func (s *Stores) Refs() CommittedRefs { return s.refs }
```

Mcmd/entire/cli/checkpoint/open.go+1/-1

```
11 unmodified lines

12
13
14
15
16
17
18
61 unmodified lines

80
81
82
83
84
85
86
87
88
21 unmodified lines

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

11 unmodified lines

"strings"
	"time"

"github.com/entireio/cli/cmd/entire/cli/agent"
	"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
	"github.com/entireio/cli/cmd/entire/cli/paths"
	"github.com/entireio/cli/cmd/entire/cli/session"
61 unmodified lines

if err != nil {
		return err
	}
	if err := validateAdoptSourceTranscript(sourceState, sourceWorktree); err != nil {
		return err
	}

adopted, filesTouched, err := buildAdoptedSessionState(ctx, sourceState)
	if err != nil {
21 unmodified lines

return nil
}

func validateAdoptSourceTranscript(source *session.State, sourceWorktree string) error {
	if source == nil || strings.TrimSpace(source.TranscriptPath) == "" {
		return nil
	}

owner, ok := agent.AgentForTranscriptPath(source.TranscriptPath, sourceWorktree)
	if !ok {
		return fmt.Errorf("unexpected transcript path for session %s: %s is not owned by a registered agent for %s",
			source.SessionID, source.TranscriptPath, sourceWorktree)
	}
	if source.AgentType != "" && owner.Type() != source.AgentType {
		return fmt.Errorf("unexpected transcript path for session %s: %s belongs to %s, but source state says %s",
			source.SessionID, source.TranscriptPath, owner.Type(), source.AgentType)
	}
	return nil
}

func stateStoreForWorktree(ctx context.Context, worktreePath string) (*session.StateStore, string, string, error) {
	absWorktree, err := filepath.Abs(worktreePath)
	if err != nil {
```

Mcmd/entire/cli/session_adopt.go+21

```
23 unmodified lines

24
25
26
27
27
28
29
30
70 unmodified lines

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
2 unmodified lines

164
165
166
112
167
168
169
170
63 unmodified lines

234
235
236
182
237
238
239
240
122 unmodified lines

363
364
365
311
366
367
368
369
302 unmodified lines

672
673
674
675
676
677
678
679
680
681
682
683
684
685

23 unmodified lines

targetRepo := setupAdoptRepo(t)

sessionID := "test-adopt-session-001"
	transcriptPath := filepath.Join(sourceRepo, ".claude", sessionID+".jsonl")
	transcriptPath := claudeAdoptTranscriptPath(t, sourceRepo, sessionID)
	if err := os.MkdirAll(filepath.Dir(transcriptPath), 0o750); err != nil {
		t.Fatal(err)
	}
70 unmodified lines

}
}

func TestSessionAdopt_RejectsUnexpectedSourceTranscriptPath(t *testing.T) {
	sourceRepo := setupAdoptRepo(t)
	targetRepo := setupAdoptRepo(t)

sessionID := "test-adopt-reject-transcript"
	transcriptPath := filepath.Join(t.TempDir(), sessionID+".jsonl")
	if err := os.WriteFile(transcriptPath, []byte(`{"type":"user"}`+"\n"), 0o600); err != nil {
		t.Fatal(err)
	}

sourceStore := session.NewStateStoreWithDir(filepath.Join(sourceRepo, ".git", session.SessionStateDirName))
	lastInteraction := time.Now().Add(-1 * time.Minute)
	if err := sourceStore.Save(context.Background(), &session.State{
		SessionID:             sessionID,
		AgentType:             agent.AgentTypeClaudeCode,
		StartedAt:             time.Now().Add(-5 * time.Minute),
		LastInteractionTime:   &lastInteraction,
		Phase:                 session.PhaseActive,
		BaseCommit:            testutil.GetHeadHash(t, sourceRepo),
		AttributionBaseCommit: testutil.GetHeadHash(t, sourceRepo),
		WorktreePath:          sourceRepo,
		TranscriptPath:        transcriptPath,
		LastPrompt:            "update target file",
	}); err != nil {
		t.Fatal(err)
	}

testutil.WriteFile(t, targetRepo, "feature.txt", "agent change\n")
	t.Chdir(targetRepo)

var out bytes.Buffer
	err := runAdopt(context.Background(), &out, sessionID, adoptOptions{
		FromWorktree: sourceRepo,
		Force:        true,
	})
	if err == nil {
		t.Fatal("runAdopt succeeded, want transcript-path refusal")
	}
	if !strings.Contains(err.Error(), "unexpected transcript path") {
		t.Fatalf("runAdopt error = %v, want unexpected transcript path", err)
	}

targetStore, storeErr := session.NewStateStore(context.Background())
	if storeErr != nil {
		t.Fatal(storeErr)
	}
	adopted, loadErr := targetStore.Load(context.Background(), sessionID)
	if loadErr != nil {
		t.Fatal(loadErr)
	}
	if adopted != nil {
		t.Fatalf("target state was written despite transcript-path refusal: %#v", adopted)
	}
}

func TestSessionAdopt_EnablesPrepareCommitMsgTrailer(t *testing.T) {
	sourceRepo := setupAdoptRepo(t)
	targetRepo := setupAdoptRepo(t)
2 unmodified lines

targetRelPath := "src/feature.go"
	targetAbsPath := filepath.Join(targetRepo, targetRelPath)

transcriptPath := filepath.Join(sourceRepo, ".claude", sessionID+".jsonl")
	transcriptPath := claudeAdoptTranscriptPath(t, sourceRepo, sessionID)
	if err := os.MkdirAll(filepath.Dir(transcriptPath), 0o750); err != nil {
		t.Fatal(err)
	}
63 unmodified lines

sessionID := "test-adopt-idle-source"
	targetRelPath := "src/idle.go"
	targetAbsPath := filepath.Join(targetRepo, targetRelPath)
	transcriptPath := filepath.Join(sourceRepo, ".claude", sessionID+".jsonl")
	transcriptPath := claudeAdoptTranscriptPath(t, sourceRepo, sessionID)
	if err := os.MkdirAll(filepath.Dir(transcriptPath), 0o750); err != nil {
		t.Fatal(err)
	}
122 unmodified lines

targetRelPath := "src/feature.go"
	targetAbsPath := filepath.Join(targetRepo, targetRelPath)

transcriptPath := filepath.Join(sourceRepo, ".claude", sessionID+".jsonl")
	transcriptPath := claudeAdoptTranscriptPath(t, sourceRepo, sessionID)
	if err := os.MkdirAll(filepath.Dir(transcriptPath), 0o750); err != nil {
		t.Fatal(err)
	}
302 unmodified lines

return realRepoDir
}

func claudeAdoptTranscriptPath(t *testing.T, sourceRepo, sessionID string) string {
	t.Helper()

transcriptDir := filepath.Join(sourceRepo, ".claude", "projects", "adopt-test")
	t.Setenv("ENTIRE_TEST_CLAUDE_PROJECT_DIR", transcriptDir)
	return filepath.Join(transcriptDir, sessionID+".jsonl")
}

func runAdoptGit(t *testing.T, dir string, args ...string) {
	t.Helper()
