trail: add resume command · Entire

Home

Log in

trail: add resume command

894aa84→main·

peyton-alt·3w ago·21 files·+2,231 added/-53 removed

Add trail resume with checkpoint-session discovery, interactive session selection, agent resume launching, and integration coverage for restoring sessions when local state is missing.

Sessions

33d13fdff5abView transcript

?\ Add Trail Resume SubcommandCodex·GPT-5.5·30 steps

Changes

21

291 unmodified lines

292
293
294
295
296
297
298
299
300
301
302
303
304
305
306

291 unmodified lines

LaunchCmd(ctx context.Context, initialPrompt string) (*exec.Cmd, error)
}

// ResumeLauncher is implemented by agents that `entire` can subprocess-spawn
// to continue an existing session.
//
// Contract matches Launcher: the returned command is foreground-ready with
// stdio wired to the caller's terminal, and callers run it directly.
type ResumeLauncher interface {
    LaunchResumeCmd(ctx context.Context, sessionID string) (*exec.Cmd, error)
}

// DiscoveredSkill describes one review-adjacent skill found on disk by a
// SkillDiscoverer. Name is the agent-native invocation form (e.g. a
// slash-prefixed command); Description is scraped from on-disk metadata

Mcmd/entire/cli/agent/agent.go+9

387 unmodified lines

388
389
390
391
392
393
394
395
396
397
398

387 unmodified lines

cmd.Env = os.Environ()
    return cmd, nil
}

func (c *ClaudeCodeAgent) LaunchResumeCmd(ctx context.Context, sessionID string) (*exec.Cmd, error) {
    cmd, err := agent.NewForegroundCommand(ctx, "claude", "-r", sessionID)
    if err != nil {
        return nil, fmt.Errorf("build claude resume command: %w", err)
    }
    return cmd, nil
}

Mcmd/entire/cli/agent/claudecode/claude.go+8

239 unmodified lines

240
241
242
243
244
245
246
247
248
249
250
251
252
253

239 unmodified lines

return cmd, nil
}

func (c *CodexAgent) LaunchResumeCmd(ctx context.Context, sessionID string) (*exec.Cmd, error) {
    cmd, err := agent.NewForegroundCommand(ctx, "codex", "resume", sessionID)
    if err != nil {
        return nil, fmt.Errorf("build codex resume command: %w", err)
    }
    return cmd, nil
}

func findRolloutBySessionID(codexHome, agentSessionID string) string {
    if codexHome == "" || validation.ValidateAgentSessionID(agentSessionID) != nil {
        return ""

Mcmd/entire/cli/agent/codex/codex.go+8

193 unmodified lines

194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219

193 unmodified lines

t.Errorf("args missing prompt: %v", cmd.Args)
    }
}

func TestCodexAgent_LaunchResumeCmd(t *testing.T) {
    t.Parallel()
    a := NewCodexAgent()
    launcher, ok := a.(agent.ResumeLauncher)
    if !ok {
        t.Fatal("CodexAgent does not implement agent.ResumeLauncher")
    }
    cmd, err := launcher.LaunchResumeCmd(context.Background(), "019ef36b-a485-7ca2-992b-b4f164266e7f")
    if err != nil {
        if errors.Is(err, exec.ErrNotFound) {
            t.Skip("codex binary not on PATH; skipping cmd shape check")
        }
        t.Fatalf("LaunchResumeCmd: %v", err)
    }
    if cmd == nil {
        t.Fatal("nil cmd")
    }
    joined := strings.Join(cmd.Args, " ")
    if !strings.Contains(joined, "resume 019ef36b-a485-7ca2-992b-b4f164266e7f") {
        t.Errorf("args missing resume session: %v", cmd.Args)
    }
}

Mcmd/entire/cli/agent/codex/codex_test.go+23

5 unmodified lines

6
7
8
9
10
11
12
133 unmodified lines

146
147
148
149
150
151
152
153
154
155
156
157
158
159

5 unmodified lines

"errors"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "time"

133 unmodified lines

return "copilot --resume " + sessionID
}

func (c *CopilotCLIAgent) LaunchResumeCmd(ctx context.Context, sessionID string) (*exec.Cmd, error) {
    cmd, err := agent.NewForegroundCommand(ctx, "copilot", "--resume", sessionID)
    if err != nil {
        return nil, fmt.Errorf("build copilot resume command: %w", err)
    }
    return cmd, nil
}

// ReadTranscript reads the raw JSONL transcript bytes for a session.
func (c *CopilotCLIAgent) ReadTranscript(sessionRef string) ([]byte, error) {
    data, err := os.ReadFile(sessionRef) //nolint:gosec // Path comes from agent hook input

Mcmd/entire/cli/agent/copilotcli/copilotcli.go+9

5 unmodified lines

6
7
8
9
10
11
12
173 unmodified lines

186
187
188
189
190
191
192
193
194
195
196

5 unmodified lines

"errors"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "regexp"
    "time"
173 unmodified lines

func (f *FactoryAIDroidAgent) FormatResumeCommand(sessionID string) string {
    return "droid --session-id " + sessionID
}

func (f *FactoryAIDroidAgent) LaunchResumeCmd(ctx context.Context, sessionID string) (*exec.Cmd, error) {
    cmd, err := agent.NewForegroundCommand(ctx, "droid", "--session-id", sessionID)
    if err != nil {
        return nil, fmt.Errorf("build droid resume command: %w", err)
    }
    return cmd, nil
}

Mcmd/entire/cli/agent/factoryaidroid/factoryaidroid.go+9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

package agent

import (
    "context"
    "fmt"
    "os"
    "os/exec"
)

// NewForegroundCommand builds an exec.Cmd wired to the caller's terminal.
// Agent launchers use this for commands the user should interact with directly.
func NewForegroundCommand(ctx context.Context, binary string, args ...string) (*exec.Cmd, error) {
    bin, err := exec.LookPath(binary)
    if err != nil {
        return nil, fmt.Errorf("%s binary not on PATH: %w", binary, err)
    }
    cmd := exec.CommandContext(ctx, bin, args...)
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.Env = os.Environ()
    return cmd, nil
}

Acmd/entire/cli/agent/foreground.go+23

395 unmodified lines

396
397
398
399
400
401
402
403
404
405
406
407
408
409

395 unmodified lines

return cmd, nil
}

func (g *GeminiCLIAgent) LaunchResumeCmd(ctx context.Context, sessionID string) (*exec.Cmd, error) {
    cmd, err := agent.NewForegroundCommand(ctx, "gemini", "--resume", sessionID)
    if err != nil {
        return nil, fmt.Errorf("build gemini resume command: %w", err)
    }
    return cmd, nil
}

// ReassembleTranscript merges Gemini JSON chunks by combining their message arrays.
func (g *GeminiCLIAgent) ReassembleTranscript(chunks [][]byte) ([]byte, error) {
    var allMessages []GeminiMessage

Mcmd/entire/cli/agent/geminicli/gemini.go+8

7 unmodified lines

8
9
10
11
12
13
14
255 unmodified lines

270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290

7 unmodified lines

"fmt"
    "log/slog"
    "os"
    "os/exec"
    "path/filepath"
    "regexp"
    "strings"
255 unmodified lines

return "opencode -s " + sessionID
}

func (a *OpenCodeAgent) LaunchResumeCmd(ctx context.Context, sessionID string) (*exec.Cmd, error) {
    if strings.TrimSpace(sessionID) == "" {
        cmd, err := agent.NewForegroundCommand(ctx, "opencode")
        if err != nil {
            return nil, fmt.Errorf("build opencode resume command: %w", err)
        }
        return cmd, nil
    }
    cmd, err := agent.NewForegroundCommand(ctx, "opencode", "-s", sessionID)
    if err != nil {
        return nil, fmt.Errorf("build opencode resume command: %w", err)
    }
    return cmd, nil
}

// nonAlphanumericRegex matches any non-alphanumeric character.
var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9]`)

Mcmd/entire/cli/agent/opencode/opencode.go+16

15 unmodified lines

16
17
18
19
20
21
22
261 unmodified lines

284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302

15 unmodified lines

"errors"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "sort"
    "strings"
261 unmodified lines

}
    return "pi --session " + id
}

func (a *PiAgent) LaunchResumeCmd(ctx context.Context, sessionID string) (*exec.Cmd, error) {
    id := strings.TrimSpace(sessionID)
    if id == "" {
        cmd, err := agent.NewForegroundCommand(ctx, "pi", "--continue")
        if err != nil {
            return nil, fmt.Errorf("build pi resume command: %w", err)
        }
        return cmd, nil
    }
    cmd, err := agent.NewForegroundCommand(ctx, "pi", "--session", id)
    if err != nil {
        return nil, fmt.Errorf("build pi resume command: %w", err)
    }
    return cmd, nil
}

Mcmd/entire/cli/agent/pi/pi.go+17

278 unmodified lines

279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295

278 unmodified lines

return l, ok
}

// ResumeLauncherFor returns the ResumeLauncher implementation for the given
// agent name, or ok=false if the agent cannot be subprocess-launched for resume.
func ResumeLauncherFor(name types.AgentName) (ResumeLauncher, bool) {
    a, err := Get(name)
    if err != nil {
        return nil, false
    }
    l, ok := a.(ResumeLauncher)
    return l, ok
}

// Default returns the default agent.
// Returns nil if the default agent is not registered.
//

Mcmd/entire/cli/agent/registry.go+11

492 unmodified lines

493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
3 unmodified lines

528
529
530
531
532
533
534
535
536
537
538
539

492 unmodified lines

}
}

func TestResumeLauncherFor(t *testing.T) {
    Register(types.AgentName("resume-launcher-test-agent"), func() Agent {
        return &mockResumeLauncherAgent{}
    })
    t.Cleanup(func() {
        registryMu.Lock()
        delete(registry, types.AgentName("resume-launcher-test-agent"))
        registryMu.Unlock()
    })

l, ok := ResumeLauncherFor(types.AgentName("resume-launcher-test-agent"))
    if !ok {
        t.Fatal("expected resume-launcher-test-agent to implement ResumeLauncher")
    }
    if l == nil {
        t.Fatal("expected non-nil ResumeLauncher")
    }
    l2, ok2 := ResumeLauncherFor(types.AgentName("does-not-exist"))
    if ok2 {
        t.Error("expected ok=false for unknown agent")
    }
    if l2 != nil {
        t.Error("expected nil ResumeLauncher for unknown agent")
    }
}

// mockLauncherAgent implements Agent and Launcher for testing.
type mockLauncherAgent struct {
    mockAgent
3 unmodified lines

func (m *mockLauncherAgent) LaunchCmd(ctx context.Context, _ string) (*exec.Cmd, error) {
    return exec.CommandContext(ctx, "true"), nil
}

type mockResumeLauncherAgent struct {
    mockAgent
}

//nolint:unparam // error is always nil in this mock; satisfies the ResumeLauncher interface.
func (m *mockResumeLauncherAgent) LaunchResumeCmd(ctx context.Context, _ string) (*exec.Cmd, error) {
    return exec.CommandContext(ctx, "true"), nil
}

Mcmd/entire/cli/agent/registry_test.go+35

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244

//go:build integration

package integration

import (
    "context"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "net/url"
    "os"
    "os/exec"
    "path/filepath"
    "strings"
    "testing"
    "time"

"github.com/entireio/cli/cmd/entire/cli/api"
    "github.com/entireio/cli/cmd/entire/cli/testutil"
    "github.com/entireio/cli/internal/entireclient/contexts"
    "github.com/entireio/cli/internal/entireclient/discovery"
    "github.com/entireio/cli/internal/entireclient/tokenstore"
)

func TestTrailResume_UsesCheckpointSessionsWhenLocalStateIsMissing(t *testing.T) {
    t.Parallel()

env := NewFeatureBranchEnv(t)
    addTrailResumeIntegrationOrigin(t, env, "https://github.com/entireio/cli.git")

firstSession := env.NewSession()
    firstPrompt := "Create hello method"
    if err := env.SimulateUserPromptSubmitWithPrompt(firstSession.ID, firstPrompt); err != nil {
        t.Fatalf("SimulateUserPromptSubmit first session: %v", err)
    }
    firstContent := "def hello; :hello; end\n"
    env.WriteFile("hello.rb", firstContent)
    firstSession.CreateTranscript(firstPrompt, []FileChange{{Path: "hello.rb", Content: firstContent}})
    if err := env.SimulateStop(firstSession.ID, firstSession.TranscriptPath); err != nil {
        t.Fatalf("SimulateStop first session: %v", err)
    }

secondSession := env.NewSession()
    secondPrompt := "Create goodbye method"
    if err := env.SimulateUserPromptSubmitWithPrompt(secondSession.ID, secondPrompt); err != nil {
        t.Fatalf("SimulateUserPromptSubmit second session: %v", err)
    }
    secondContent := "def goodbye; :goodbye; end\n"
    env.WriteFile("goodbye.rb", secondContent)
    secondSession.CreateTranscript(secondPrompt, []FileChange{{Path: "goodbye.rb", Content: secondContent}})
    if err := env.SimulateStop(secondSession.ID, secondSession.TranscriptPath); err != nil {
        t.Fatalf("SimulateStop second session: %v", err)
    }

env.GitCommitWithShadowHooks("Add hello and goodbye methods", "hello.rb", "goodbye.rb")
    checkpointID := env.GetLatestCheckpointIDFromHistory()

if err := env.ClearSessionState(firstSession.ID); err != nil {
        t.Fatalf("clear first session state: %v", err)
    }
    if err := env.ClearSessionState(secondSession.ID); err != nil {
        t.Fatalf("clear second session state: %v", err)
    }

trail := api.TrailResource{
        ID:        "trail-integration-321",
        Number:    321,
        URL:       "https://entire.io/gh/entireio/cli/trails/321",
        Branch:    env.GetCurrentBranch(),
        Base:      masterBranch,
        Title:     "Resume checkpoint sessions from trail",
        Status:    "open",
        Phase:     "building",
        CreatedAt: time.Now().Add(-time.Hour).UTC(),
        UpdatedAt: time.Now().UTC(),
    }
    server := newTrailResumeIntegrationAPIServer(t, trail)
    defer server.Close()
    configureTrailResumeIntegrationAuth(t, env, server.URL)

contextOutput := env.RunCLI("trail", "--insecure-http-auth", "resume", "321", "--no-resume")
    for _, want := range []string{
        "Trail #321",
        "Checkpoint sessions:",
        firstSession.ID,
        secondSession.ID,
        checkpointID,
        "Create hello method",
        "Create goodbye method",
        "entire trail resume 321 --session " + firstSession.ID,
        "entire trail resume 321 --session " + secondSession.ID,
    } {
        if !strings.Contains(contextOutput, want) {
            t.Fatalf("trail resume --no-resume output missing %q:\n%s", want, contextOutput)
        }
    }
    if strings.Contains(contextOutput, "none found") {
        t.Fatalf("trail resume should not report missing local sessions after reading checkpoint metadata:\n%s", contextOutput)
    }

resumeOutput := env.RunCLI("trail", "--insecure-http-auth", "resume", "321", "--session", secondSession.ID)
    for _, want := range []string{
        "Restored checkpoint session " + secondSession.ID,
        "claude -r " + secondSession.ID,
        "Create goodbye method",
    } {
        if !strings.Contains(resumeOutput, want) {
            t.Fatalf("trail resume --session output missing %q:\n%s", want, resumeOutput)
        }
    }

restoredTranscript := filepath.Join(env.ClaudeProjectDir, secondSession.ID+".jsonl")
    data, err := os.ReadFile(restoredTranscript)
    if err != nil {
        t.Fatalf("read restored transcript %s: %v", restoredTranscript, err)
    }
    if !strings.Contains(string(data), "Create goodbye method") {
        t.Fatalf("restored transcript does not contain selected session prompt:\n%s", data)
    }
}

func newTrailResumeIntegrationAPIServer(t *testing.T, trail api.TrailResource) *httptest.Server {
    t.Helper()

return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        switch {
        case r.Method == http.MethodPost && r.URL.Path == "/oauth/token":
            writeTrailResumeIntegrationJSON(t, w, http.StatusOK, map[string]any{
                "access_token": "trail-resume-data-token",
                "token_type":   "Bearer",
                "expires_in":   3600,
            })
        case r.Method == http.MethodGet && r.URL.Path == "/api/v1/trails/gh/entireio/cli":
            writeTrailResumeIntegrationJSON(t, w, http.StatusOK, api.TrailListResponse{
                Trails:       []api.TrailResource{trail},
                Total:        1,
                Limit:        200,
                RepoFullName: "entireio/cli",
            })
        case r.Method == http.MethodGet && r.URL.Path == "/api/v1/trails/"+url.PathEscape(trail.ID)+"/reviews/comments":
            writeTrailResumeIntegrationJSON(t, w, http.StatusOK, map[string]any{
                "comments": []any{},
                "has_more": false,
            })
        default:
            http.NotFound(w, r)
        }
    }))
}

func writeTrailResumeIntegrationJSON(t *testing.T, w http.ResponseWriter, status int, body any) {
    t.Helper()
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    if err := json.NewEncoder(w).Encode(body); err != nil {
        t.Fatalf("encode JSON response: %v", err)
    }
}

func configureTrailResumeIntegrationAuth(t *testing.T, env *TestEnv, coreURL string) {
    t.Helper()

configDir := filepath.Join(env.RepoDir, ".entire-test-config")
    xdgCacheHome := filepath.Join(env.RepoDir, ".entire-test-cache")
    tokenStorePath := filepath.Join(env.RepoDir, ".entire-test-tokens.json")
    service := tokenstore.CoreKeyringService(coreURL)
    handle := "tester"

if err := contexts.Save(configDir, &contexts.File{
        CurrentContext: "tester@trail-resume",
        Contexts: []*contexts.Context{
            {
                Name:            "tester@trail-resume",
                CoreURL:         coreURL,
                Handle:          handle,
                KeychainService: service,
            },
        },
    }); err != nil {
        t.Fatalf("save auth context: %v", err)
    }

host := mustTrailResumeIntegrationHost(t, coreURL)
    cacheDir := filepath.Join(xdgCacheHome, "entire")
    if err := discovery.ModifyAPICores(cacheDir, func(c discovery.ClusterCoresCache) error {
        c.Set(host, []string{coreURL})
        return nil
    }); err != nil {
        t.Fatalf("seed API discovery cache: %v", err)
    }

tokenStore := map[string]map[string]string{
        service: {
            handle: tokenstore.EncodeTokenWithExpiration(fakeLoginJWT(coreURL), 7200),
        },
    }
    tokenData, err := json.Marshal(tokenStore)
    if err != nil {
        t.Fatalf("marshal token store: %v", err)
    }
    if err := os.WriteFile(tokenStorePath, tokenData, 0o600); err != nil {
        t.Fatalf("write token store: %v", err)
    }

env.ExtraEnv = append(env.ExtraEnv,
        "ENTIRE_API_BASE_URL="+coreURL,
        "ENTIRE_CONFIG_DIR="+configDir,
        "XDG_CACHE_HOME="+xdgCacheHome,
        "ENTIRE_TOKEN_STORE=file",
        "ENTIRE_TOKEN_STORE_PATH="+tokenStorePath,
    )
}

func mustTrailResumeIntegrationHost(t *testing.T, rawURL string) string {
    t.Helper()
    parsed, err := url.Parse(rawURL)
    if err != nil {
        t.Fatalf("parse URL %q: %v", rawURL, err)
    }
    if parsed.Host == "" {
        t.Fatalf("URL %q has no host", rawURL)
    }
    return parsed.Host
}

func addTrailResumeIntegrationOrigin(t *testing.T, env *TestEnv, remoteURL string) {
    t.Helper()

cmd := exec.CommandContext(context.Background(), "git", "remote", "add", "origin", remoteURL)
    cmd.Dir = env.RepoDir
    cmd.Env = testutil.GitIsolatedEnv()
    if output, err := cmd.CombinedOutput(); err != nil {
        t.Fatalf("git remote add origin: %v\n%s", err, output)
    }

configData, err := os.ReadFile(filepath.Join(env.RepoDir, ".git", "config"))
    if err != nil {
        t.Fatalf("read git config after remote add: %v", err)
    }
    if !strings.Contains(string(configData), remoteURL) {
        t.Fatalf("git config does not contain origin URL %q:\n%s", remoteURL, configData)
    }
    env.AcceptGitConfigChanges(string(configData))
}

Acmd/entire/cli/integration_test/trail_resume_test.go+244

191 unmodified lines

192
193
194
195
196
197
198
199
200
201
202
203
196
204
205
206
207
208
201
209
210
211
212
213
214
207
215
216
217
218
7 unmodified lines

226
227
228
221
229
230
231
224
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
232
248
249
250
251
252
253
254
239
255
256
257
258
243
259
260
261
262
12 unmodified lines

275
276
277
262
278
279
280
281
266
282
283
284
285
2 unmodified lines

288
289
290
275
291
292
293
294
13 unmodified lines

308
309
310
295
311
312
313
314
15 unmodified lines

330
331
332
317
333
334
335
336
3 unmodified lines

340
341
342
327
343
344
345
346
220 unmodified lines

567
568
569
554
555
570
571
572
5 unmodified lines

578
579
580
567
568
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
570
571
616
617
618
573
619
620
621
576
622
623
624
625
8 unmodified lines

634
635
636
591
637
638
639
640
641
642
643
598
644
645
646
647
648
649
604
650
651
652
653
608
654
655
656
611
657
658
659
660
615
661
662
663
664
211 unmodified lines

876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
839
893
894
895
896
7 unmodified lines

904
905
906
853
907
908
909
910
911
858
912
913
914
915
916
863
917
918
919
920
10 unmodified lines

931
932
933
880
934
935
936
937
1 unmodified line

939
940
941
888
942
943
944
945

191 unmodified lines

// does not search branch history — the caller already knows which checkpoint to
// resume, so two sessions on the same branch resume independently.
func resumeByCheckpointID(ctx context.Context, w, errW io.Writer, checkpointID id.CheckpointID, force bool) error {
    sessions, err := restoreByCheckpointID(ctx, w, errW, checkpointID, force)
    if err != nil || len(sessions) == 0 {
        return err
    }
    return displayRestoredSessions(w, sessions)
}

func restoreByCheckpointID(ctx context.Context, w, errW io.Writer, checkpointID id.CheckpointID, force bool) ([]strategy.RestoredSession, error) {
    if checkpointID.IsEmpty() {
        return errors.New("no checkpoint to resume")
        return nil, errors.New("no checkpoint to resume")
    }

repo, err := openRepository(ctx)
    if err != nil {
        return fmt.Errorf("not a git repository: %w", err)
        return nil, fmt.Errorf("not a git repository: %w", err)
    }
    defer repo.Close()

stores, err := checkpoint.Open(ctx, repo, checkpoint.OpenOptions{BlobFetcher: FetchBlobsByHash})
    if err != nil {
        return fmt.Errorf("open checkpoint store: %w", err)
        return nil, fmt.Errorf("open checkpoint store: %w", err)
    }
    store := stores.Primary
    refs := stores.Refs()
7 unmodified lines

slog.String("checkpoint_id", checkpointID.String()),
            slog.String("error", err.Error()),
        )
        return checkRemoteMetadata(ctx, w, errW, checkpointID, stores.Refs())
        return nil, checkRemoteMetadata(ctx, w, errW, checkpointID, stores.Refs())
    }

return resumeSession(ctx, w, errW, metadata, force)
    return restoreResumeSessions(ctx, w, errW, metadata, force)
}

func resumeFromCurrentBranch(ctx context.Context, w, errW io.Writer, branchName string, force bool) error {
    sessions, err := restoreFromCurrentBranch(ctx, w, errW, branchName, force)
    if err != nil || len(sessions) == 0 {
        return err
    }
    return displayRestoredSessions(w, sessions)
}

func restoreFromCurrentBranch(ctx context.Context, w, errW io.Writer, branchName string, force bool) ([]strategy.RestoredSession, error) {
    logCtx := logging.WithComponent(ctx, "resume")

// Find a commit with an Entire-Checkpoint trailer, looking at branch-only commits
    result, err := findBranchCheckpoints(repo, branchName)
    if err != nil {
        return err
        return nil, err
    }
    if len(result.checkpointIDs) == 0 {
        fmt.Fprintf(w, "No Entire checkpoint found on branch '%s'\n", branchName)
        return nil
        return nil, nil
    }

logging.Debug(logCtx, "found checkpoint(s) on branch",
12 unmodified lines

shouldResume, err := promptResumeFromOlderCheckpoint()
        if err != nil {
            return err
            return nil, err
        }
        if !shouldResume {
            fmt.Fprintf(w, "Resume cancelled.\n")
            return nil
            return nil, nil
        }
    }

2 unmodified lines

stores, err := checkpoint.Open(ctx, repo, checkpoint.OpenOptions{BlobFetcher: FetchBlobsByHash})
    if err != nil {
        return fmt.Errorf("open checkpoint store: %w", err)
        return nil, fmt.Errorf("open checkpoint store: %w", err)
    }
    store := stores.Primary

13 unmodified lines

)
            fmt.Fprintf(w, "Found %d checkpoints for commit %s but metadata is not available\n",
                len(result.checkpointIDs), result.commitHash[:7])
            return checkRemoteMetadata(ctx, w, errW, result.checkpointIDs[0], stores.Refs())
            return nil, checkRemoteMetadata(ctx, w, errW, result.checkpointIDs[0], stores.Refs())
        }
        skipped := len(result.checkpointIDs) - 1
        fmt.Fprintf(w, "Found %d checkpoints for commit %s, resuming from the latest (%d older checkpoints skipped)\n",
15 unmodified lines

slog.String("checkpoint_id", checkpointID.String()),
                slog.String("error", storeErr.Error()),
            )
            return checkRemoteMetadata(ctx, w, errW, checkpointID, stores.Refs())
            return nil, checkRemoteMetadata(ctx, w, errW, checkpointID, stores.Refs())
        }
    }

3 unmodified lines

slog.Int("session_count", metadata.SessionCount),
    )

return resumeSession(ctx, w, errW, metadata, force)
    return restoreResumeSessions(ctx, w, errW, metadata, force)
}

// resolveLatestCheckpoint reads metadata for each checkpoint ID and returns
220 unmodified lines

// among commits that are unique to this branch (not reachable from the default branch).
// This handles the case where main has been merged into the feature branch.
func findBranchCheckpoints(repo *git.Repository, branchName string) (*branchCheckpointsResult, error) {
    result := &branchCheckpointsResult{}

// Get HEAD commit
    head, err := repo.Head()
    if err != nil {
5 unmodified lines

return nil, fmt.Errorf("failed to get HEAD commit: %w", err)
    }

// First, check if HEAD itself has a checkpoint (most common case)
    if cpIDs := trailers.ParseAllCheckpoints(headCommit.Message); len(cpIDs) > 0 {
    return findBranchCheckpointsFromCommit(repo, branchName, headCommit), nil
}

func findBranchCheckpointsForBranchRef(repo *git.Repository, branchName string) (*branchCheckpointsResult, error) {
    commit, err := branchCommit(repo, branchName)
    if err != nil {
        return nil, err
    }
    return findBranchCheckpointsFromCommit(repo, branchName, commit), nil
}

func branchCommit(repo *git.Repository, branchName string) (*object.Commit, error) {
    for _, refName := range []plumbing.ReferenceName{
        plumbing.NewBranchReferenceName(branchName),
        plumbing.NewRemoteReferenceName("origin", branchName),
    } {
        ref, err := repo.Reference(refName, true)
        if err != nil {
            continue
        }
        commit, commitErr := repo.CommitObject(ref.Hash())
        if commitErr != nil {
            return nil, fmt.Errorf("failed to get branch commit for %s: %w", refName, commitErr)
        }
        return commit, nil
    }
    return nil, fmt.Errorf("branch '%s' not found locally or on origin", branchName)
}

func findBranchCheckpointsFromCommit(repo *git.Repository, branchName string, startCommit *object.Commit) *branchCheckpointsResult {
    result := &branchCheckpointsResult{}

// First, check if the branch tip itself has a checkpoint (most common case).
    if cpIDs := trailers.ParseAllCheckpoints(startCommit.Message); len(cpIDs) > 0 {
        result.checkpointIDs = cpIDs
        result.commitHash = head.Hash().String()
        result.commitMessage = headCommit.Message
        result.commitHash = startCommit.Hash.String()
        result.commitMessage = startCommit.Message
        result.newerCommitsExist = false
        return result, nil
        return result
    }

// HEAD doesn't have a checkpoint - find branch-only commits
    // The branch tip doesn't have a checkpoint - find branch-only commits.
    // Get the default branch name
    defaultBranch := getDefaultBranchFromRemote(repo)
    if defaultBranch == "" {
8 unmodified lines

// If we can't find a default branch, or we're on it, just walk all commits
    if defaultBranch == "" || defaultBranch == branchName {
        return findCheckpointInHistory(headCommit, nil), nil
        return findCheckpointInHistory(startCommit, nil)
    }

// Get the default branch reference
    defaultRef, err := repo.Reference(plumbing.NewBranchReferenceName(defaultBranch), true)
    if err != nil {
        // Default branch doesn't exist locally, fall back to walking all commits
        return findCheckpointInHistory(headCommit, nil), nil //nolint:nilerr // Intentional fallback
        return findCheckpointInHistory(startCommit, nil)
    }

defaultCommit, err := repo.CommitObject(defaultRef.Hash())
    if err != nil {
        // Can't get default commit, fall back to walking all commits
        return findCheckpointInHistory(headCommit, nil), nil //nolint:nilerr // Intentional fallback
        return findCheckpointInHistory(startCommit, nil)
    }

// Find merge base
    mergeBase, err := headCommit.MergeBase(defaultCommit)
    mergeBase, err := startCommit.MergeBase(defaultCommit)
    if err != nil || len(mergeBase) == 0 {
        // No common ancestor, fall back to walking all commits
        return findCheckpointInHistory(headCommit, nil), nil //nolint:nilerr // Intentional fallback
        return findCheckpointInHistory(startCommit, nil)
    }

// Walk from HEAD to merge base, looking for checkpoint
    return findCheckpointInHistory(headCommit, &mergeBase[0].Hash), nil
    return findCheckpointInHistory(startCommit, &mergeBase[0].Hash)
}

// findCheckpointInHistory walks commit history from start looking for a checkpoint trailer.
211 unmodified lines

// The caller must provide the already-resolved checkpoint metadata to avoid redundant lookups
// and to support both local and remote metadata trees.
func resumeSession(ctx context.Context, w, errW io.Writer, metadata *strategy.CheckpointInfo, force bool) error {
    sessions, err := restoreResumeSessions(ctx, w, errW, metadata, force)
    if err != nil || len(sessions) == 0 {
        return err
    }
    return displayRestoredSessions(w, sessions)
}

func restoreResumeSessions(ctx context.Context, w, errW io.Writer, metadata *strategy.CheckpointInfo, force bool) ([]strategy.RestoredSession, error) {
    checkpointID := metadata.CheckpointID
    sessionID := metadata.SessionID

// Resolve agent from checkpoint metadata (same as rewind)
    ag, err := strategy.ResolveAgentForRewind(metadata.Agent)
    if err != nil {
        return fmt.Errorf("failed to resolve agent: %w", err)
        return nil, fmt.Errorf("failed to resolve agent: %w", err)
    }

// Initialize logging context with agent
7 unmodified lines

// Get worktree root for session directory lookup
    repoRoot, err := paths.WorktreeRoot(ctx)
    if err != nil {
        return fmt.Errorf("failed to get worktree root: %w", err)
        return nil, fmt.Errorf("failed to get worktree root: %w", err)
    }

sessionDir, err := ag.GetSessionDir(repoRoot)
    if err != nil {
        return fmt.Errorf("failed to determine session directory: %w", err)
        return nil, fmt.Errorf("failed to determine session directory: %w", err)
    }

// Create directory if it doesn't exist
    if err := os.MkdirAll(sessionDir, 0o700); err != nil {
        return fmt.Errorf("failed to create session directory: %w", err)
        return nil, fmt.Errorf("failed to create session directory: %w", err)
    }

// Get strategy and restore sessions using full checkpoint data
10 unmodified lines

sessions, restoreErr := strat.RestoreLogsOnly(ctx, w, errW, point, force)
    if restoreErr != nil || len(sessions) == 0 {
        // Fall back to single-session restore (e.g., old checkpoints without agent metadata)
        return resumeSingleSession(ctx, w, errW, ag, sessionID, checkpointID, repoRoot, force)
        return nil, resumeSingleSession(ctx, w, errW, ag, sessionID, checkpointID, repoRoot, force)
    }

logging.Debug(logCtx, "resume session completed",
1 unmodified line

slog.Int("session_count", len(sessions)),
    )

return displayRestoredSessions(w, sessions)
    return sessions, nil
}

// displayRestoredSessions sorts sessions by CreatedAt and prints resume commands.

Mcmd/entire/cli/resume.go+88/-34

25 unmodified lines

26
27
28
29
29
30
31
32
33
34
35
345 unmodified lines

381
382
383
381
384
385
386
387

25 unmodified lines

)

// resumePickerCancel is the sentinel option value for the picker's Cancel entry.
const resumePickerCancel = "cancel"
const (
    resumePickerCancel = "cancel"
    unknownAgentLabel  = "(unknown agent)"
)

// resumableSession pairs a session with the branch and committed checkpoint we
// resolved for it. A session is only resumable when BOTH are known: the branch
345 unmodified lines

agentLabel := string(s.AgentType)
    if agentLabel == "" {
        agentLabel = "(unknown agent)"
        agentLabel = unknownAgentLabel
    }

prompt := strings.TrimSpace(s.LastPrompt)

Mcmd/entire/cli/resume_picker.go+5/-2

732 unmodified lines

733
734
735
736
737
736
737
738
739
4 unmodified lines

744
745
746
748
749
750
751
747
748
749
750
751
752
753
754
755
34 unmodified lines

790
791
792
792
793
794
795
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865

732 unmodified lines

}
        }

// Get first prompt for display
        promptPreview := ExtractFirstPrompt(content.Prompts)
        promptPreview := restoredPromptPreview(sessionAgent, content.Prompts, content.Transcript, content.Metadata.ReviewPrompt)

// Local log already present and not forcing: keep it untouched, but still
        // report the session so the caller can print its resume command.
4 unmodified lines

fmt.Fprintf(w, "Keeping existing local session log\n")
            }
            restored = append(restored, RestoredSession{
                SessionID: sessionID,
                Agent:     sessionAgent.Type(),
                Prompt:    promptPreview,
                CreatedAt: content.Metadata.CreatedAt,
                SessionID:    sessionID,
                Agent:        sessionAgent.Type(),
                Prompt:       promptPreview,
                CreatedAt:    content.Metadata.CreatedAt,
                Kind:         content.Metadata.Kind,
                ReviewPrompt: content.Metadata.ReviewPrompt,
            })
            continue
        }
34 unmodified lines

}

restored = append(restored, RestoredSession{
            SessionID: sessionID,
            Agent:     sessionAgent.Type(),
            Prompt:    promptPreview,
            CreatedAt: content.Metadata.CreatedAt,
            SessionID:    sessionID,
            Agent:        sessionAgent.Type(),
            Prompt:       promptPreview,
            CreatedAt:    content.Metadata.CreatedAt,
            Kind:         content.Metadata.Kind,
            ReviewPrompt: content.Metadata.ReviewPrompt,
        })
    }

return restored, nil
}

func restoredPromptPreview(sessionAgent agent.Agent, promptContent string, transcript []byte, reviewPrompt string) string {
    if prompt := ExtractFirstPrompt(promptContent); prompt != "" {
        return prompt
    }
    if prompt := strings.TrimSpace(reviewPrompt); prompt != "" {
        return prompt
    }
    extractor, ok := sessionAgent.(agent.PromptExtractor)
    if !ok || len(transcript) == 0 {
        return ""
    }
    prompts, err := extractPromptsFromTranscriptBytes(extractor, transcript)
    if err != nil || len(prompts) == 0 {
        return ""
    }
    return firstRestoredDisplayPrompt(prompts)
}

func firstRestoredDisplayPrompt(prompts []string) string {
    for _, prompt := range prompts {
        cleaned := strings.TrimSpace(prompt)
        if cleaned == "" || isOnlySeparators(cleaned) || isInjectedInstructionPrompt(cleaned) {
            continue
        }
        return TruncateDescription(cleaned, MaxDescriptionLength)
    }
    return ""
}

func isInjectedInstructionPrompt(prompt string) bool {
    trimmed := strings.TrimSpace(prompt)
    return strings.HasPrefix(trimmed, "# AGENTS.md instructions for ") ||
        strings.HasPrefix(trimmed, "<environment_context>") ||
        (strings.Contains(trimmed, "<INSTRUCTIONS>") && strings.Contains(trimmed, "AGENTS.md instructions"))
}

func extractPromptsFromTranscriptBytes(extractor agent.PromptExtractor, transcript []byte) ([]string, error) {
    tmp, err := os.CreateTemp("", "entire-restored-transcript-*.jsonl")
    if err != nil {
        return nil, fmt.Errorf("create temporary transcript: %w", err)
    }
    tmpPath := tmp.Name()
    defer os.Remove(tmpPath)

if _, err := tmp.Write(transcript); err != nil {
        _ = tmp.Close()
        return nil, fmt.Errorf("write temporary transcript: %w", err)
    }
    if err := tmp.Close(); err != nil {
        return nil, fmt.Errorf("close temporary transcript: %w", err)
    }
    prompts, err := extractor.ExtractPrompts(tmpPath, 0)
    if err != nil {
        return nil, fmt.Errorf("extract prompts from temporary transcript: %w", err)
    }
    return prompts, nil
}

// ResolveAgentForRewind resolves the agent from checkpoint metadata.
func ResolveAgentForRewind(agentType types.AgentType) (agent.Agent, error) {
    ag, err := agent.GetByAgentType(agentType)

Mcmd/entire/cli/strategy/manual_commit_rewind.go+71/-10

261 unmodified lines

262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
363 unmodified lines

653
654
655
634
635
636
656
657
658
659
660
661
662
33 unmodified lines

696
697
698
699
700
701
702
703
704
705
706

261 unmodified lines

require.Equal(t, string(checkpointTranscript), string(got), "force restore must overwrite from the checkpoint")
}

func TestRestoredPromptPreviewFallsBackInOrder(t *testing.T) {
    t.Parallel()

ag := &restoreLogsOnlyAgent{
        extractedPrompts: []string{
            "# AGENTS.md instructions for /repo\n\n<INSTRUCTIONS>\nskip me\n</INSTRUCTIONS>",
            "<environment_context>\n  <cwd>/repo</cwd>\n</environment_context>",
            "prompt from transcript",
        },
    }

if got := restoredPromptPreview(ag, "prompt sidecar", []byte("transcript"), "review prompt"); got != "prompt sidecar" {
        t.Fatalf("sidecar prompt = %q, want prompt sidecar", got)
    }
    if got := restoredPromptPreview(ag, "", []byte("transcript"), "review prompt"); got != "review prompt" {
        t.Fatalf("review prompt = %q, want review prompt", got)
    }
    if got := restoredPromptPreview(ag, "", []byte("transcript"), ""); got != "prompt from transcript" {
        t.Fatalf("transcript prompt = %q, want prompt from transcript", got)
    }
}

func TestResolveAgentForRewind(t *testing.T) {
    t.Parallel()

363 unmodified lines

}

type restoreLogsOnlyAgent struct {
    name       types.AgentName
    agentType  types.AgentType
    sessionDir string
    name             types.AgentName
    agentType        types.AgentType
    sessionDir       string
    extractedPrompts []string
}

var _ agent.Agent = (*restoreLogsOnlyAgent)(nil)
33 unmodified lines

return "restore-logs " + sessionID
}

//nolint:unparam // error is always nil in this test helper; satisfies PromptExtractor.
func (a *restoreLogsOnlyAgent) ExtractPrompts(string, int) ([]string, error) {
    return a.extractedPrompts, nil
}

// fakeExternalAgent is a minimal Agent implementation for testing dynamic registration.
// It simulates an external agent that was discovered and registered at runtime.
type fakeExternalAgent struct {

Mcmd/entire/cli/strategy/rewind_test.go+31/-3

265 unmodified lines

266
267
268
269
270
271
272
269
270
271
272
273
274
275

265 unmodified lines

// Each session may come from a different agent, so callers use this to print
// per-session resume commands without re-reading the metadata tree.
type RestoredSession struct {
    SessionID string
    Agent     types.AgentType
    Prompt    string
    CreatedAt time.Time // From session metadata; used by resume to determine most recent
    SessionID    string
    Agent        types.AgentType
    Prompt       string
    CreatedAt    time.Time // From session metadata; used by resume to determine most recent
    Kind         string
    ReviewPrompt string
}

Mcmd/entire/cli/strategy/strategy.go+6/-4

62 unmodified lines

63
64
65
66
67
68
69

62 unmodified lines

cmd.AddCommand(newTrailCreateCmd())
    cmd.AddCommand(newTrailUpdateCmd())
    cmd.AddCommand(newTrailCheckoutCmd())
    cmd.AddCommand(newTrailResumeCmd())
    cmd.AddCommand(newTrailDeleteCmd())
    cmd.AddCommand(newTrailFindingCmd())
    cmd.AddCommand(newTrailWatchCmd())

Mcmd/entire/cli/trail_cmd.go+1

package cli

import ( "context" "encoding/json" "errors" "fmt" "io" "sort" "strconv" "strings" "text/tabwriter" "time"

"github.com/entireio/cli/cmd/entire/cli/agent" "github.com/entireio/cli/cmd/entire/cli/agent/external" "github.com/entireio/cli/cmd/entire/cli/api" "github.com/entireio/cli/cmd/entire/cli/checkpoint" "github.com/entireio/cli/cmd/entire/cli/checkpoint/id" "github.com/entireio/cli/cmd/entire/cli/interactive" sessionpkg "github.com/entireio/cli/cmd/entire/cli/session" "github.com/entireio/cli/cmd/entire/cli/strategy" "github.com/entireio/cli/cmd/entire/cli/stringutil"

"charm.land/huh/v2" "github.com/spf13/cobra" )

const ( trailResumeNoPrompt = "(no prompt)" )

type trailResumeOptions struct { Selector string SessionID string CheckpointID string Force bool JSON bool NoResume bool }

type trailResumeContext struct { Trail trailResumeTrailContext json:"trail" Sessions []trailResumeSessionContext json:"sessions" Findings trailResumeFindingsContext json:"-" DefaultResume *trailResumeDefaultContext json:"default_resume,omitempty" Commands []string json:"commands" }

type trailResumeTrailContext struct { ID string json:"id,omitempty" Number int json:"number,omitempty" Title string json:"title,omitempty" Branch string json:"branch" Base string json:"base,omitempty" Status string json:"status,omitempty" Phase string json:"phase,omitempty" URL string json:"url,omitempty" }

type trailResumeSessionContext struct { SessionID string json:"session_id" Agent string json:"agent,omitempty" LastPrompt string json:"last_prompt,omitempty" LastActive time.Time json:"last_active,omitempty" CheckpointID string json:"checkpoint_id" }

type trailResumeDefaultContext struct { Branch string json:"branch" SessionID string json:"session_id,omitempty" CheckpointID string json:"checkpoint_id,omitempty" }

type trailResumeFindingsContext struct { Counts trailReviewCommentCounts json:"counts" Top []api.TrailReviewComment json:"top" HasMore bool json:"has_more,omitempty" Unavailable string json:"unavailable,omitempty" }

type trailResumeFindingCounts struct { Open int json:"open" OpenHigh int json:"open_high" OpenMedium int json:"open_medium" OpenLow int json:"open_low" Resolved int json:"resolved" Dismissed int json:"dismissed" Stale int json:"stale" }

func newTrailResumeCmd() *cobra.Command { var opts trailResumeOptions

cmd := &cobra.Command{ Use: "resume []", Short: "Resume a trail's agent session", Long: `Resume an agent session for a trail.

The trail may be given as the first argument or via --trail, as a number, id, or branch. Without one, the trail for the current branch is used.

By default, interactive terminals show the trail context and let you choose between checkpoint sessions on the trail branch when there are multiple. Non-interactive runs show the same context and resume the latest checkpoint on the trail branch. Use --session or --checkpoint to resume an exact session or checkpoint.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { selector, err := parseOptionalTrailSelector(args, opts.Selector) if err != nil { return err } opts.Selector = selector if err := validateTrailResumeOptions(opts); err != nil { return err } external.DiscoverAndRegister(cmd.Context()) return runTrailResume(cmd, opts) }, }

cmd.Flags().StringVar(&opts.Selector, "trail", "", "Trail to resume (number, id, or branch; defaults to the current branch's trail)") cmd.Flags().StringVar(&opts.SessionID, "session", "", "Resume a specific known local session on the trail branch") cmd.Flags().StringVar(&opts.CheckpointID, "checkpoint", "", "Resume a specific checkpoint on the trail branch") cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Skip prompts and overwrite existing session logs from checkpoints") cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output trail resume context as JSON") cmd.Flags().BoolVar(&opts.NoResume, "no-resume", false, "Show trail resume context without restoring or resuming a session")

return cmd }

func validateTrailResumeOptions(opts trailResumeOptions) error { if strings.TrimSpace(opts.SessionID) != "" && strings.TrimSpace(opts.CheckpointID) != "" { return errors.New("cannot combine --session and --checkpoint") } if opts.JSON && !opts.NoResume { return errors.New("--json can only be used with --no-resume") } if opts.NoResume && (strings.TrimSpace(opts.SessionID) != "" || strings.TrimSpace(opts.CheckpointID) != "") { return errors.New("cannot combine --no-resume with --session or --checkpoint") } if checkpointID := strings.TrimSpace(opts.CheckpointID); checkpointID != "" { if err := id.Validate(checkpointID); err != nil { return fmt.Errorf("validate --checkpoint: %w", err) } } return nil }

func runTrailResume(cmd *cobra.Command, opts trailResumeOptions) error { return runAuthenticatedDataAPI(cmd.Context(), cmd.ErrOrStderr(), trailInsecureHTTP(cmd), func(ctx context.Context, client *api.Client) error { forge, owner, repo, err := resolveTrailRemote(ctx) if err != nil { return err }

found, err := resolveTrailBySelector(ctx, client, forge, owner, repo, opts.Selector) if err != nil { return err } branch := strings.TrimSpace(found.Branch) if branch == "" { return fmt.Errorf("%s has no branch to resume", describeTrailRef(found)) }

sessions, sessionErr := resolveTrailResumeSessionContexts(ctx, branch) if sessionErr != nil && strings.TrimSpace(opts.SessionID) != "" { return sessionErr }

findings, findingsErr := loadTrailResumeFindingsContext(ctx, client, found.ID) if findingsErr != nil { findings.Unavailable = findingsErr.Error() fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not load trail findings: %v\n", findingsErr) }

resumeCtx := buildTrailResumeContext(*found, sessions, findings) if opts.JSON { return encodeTrailResumeContextJSON(cmd.OutOrStdout(), resumeCtx) }

printTrailResumeContext(cmd.OutOrStdout(), resumeCtx) if opts.NoResume { return nil }

if opts.CheckpointID != "" { return resumeTrailCheckpoint(ctx, cmd, branch, id.CheckpointID(opts.CheckpointID), "", opts.Force) }

if opts.SessionID != "" { sessionCtx, ok := findTrailResumeSession(resumeCtx.Sessions, opts.SessionID) if ok { return resumeTrailCheckpoint(ctx, cmd, branch, id.CheckpointID(sessionCtx.CheckpointID), opts.SessionID, opts.Force) } return resumeTrailLatest(ctx, cmd, branch, opts.Force, opts.SessionID) }

if interactive.CanPromptInteractively() && len(resumeCtx.Sessions) > 1 { return runTrailResumePicker(ctx, cmd, branch, resumeCtx.Sessions, opts.Force) }

return resumeTrailLatest(ctx, cmd, branch, opts.Force, "") }) }

func resumeTrailLatest(ctx context.Context, cmd *cobra.Command, branch string, force bool, preferredSessionID string) error { w := cmd.OutOrStdout() errW := cmd.ErrOrStderr()

if !ensureTrailResumeBranchAvailable(ctx, w, branch) { return nil } proceed, err := switchToBranchForResume(ctx, w, errW, branch, trailResumeSkipBranchPrompts(force)) if err != nil || !proceed { return err } sessions, err := restoreFromCurrentBranch(ctx, w, errW, branch, force) if err != nil { return err } return continueTrailRestoredSessions(ctx, cmd, sessions, preferredSessionID) }

func resumeTrailCheckpoint(ctx context.Context, cmd *cobra.Command, branch string, checkpointID id.CheckpointID, preferredSessionID string, force bool) error { w := cmd.OutOrStdout() errW := cmd.ErrOrStderr()

if !ensureTrailResumeBranchAvailable(ctx, w, branch) { return nil } proceed, err := switchToBranchForResume(ctx, w, errW, branch, trailResumeSkipBranchPrompts(force)) if err != nil || !proceed { return err } sessions, err := restoreByCheckpointID(ctx, w, errW, checkpointID, force) if err != nil { return err } return continueTrailRestoredSessions(ctx, cmd, sessions, preferredSessionID) }

func trailResumeSkipBranchPrompts(force bool) bool { return force || !interactive.CanPromptInteractively() }

func continueTrailRestoredSessions(ctx context.Context, cmd *cobra.Command, sessions []strategy.RestoredSession, preferredSessionID string) error { w := cmd.OutOrStdout() if len(sessions) == 0 { return nil }

if preferredSessionID != "" { session, ok := findTrailRestoredSession(sessions, preferredSessionID) if !ok { return fmt.Errorf("session %q was not found in the restored checkpoint", preferredSessionID) } if !interactive.CanPromptInteractively() { return displayTrailRestoredSessions(w, []strategy.RestoredSession{session}) } printTrailRestoredSessionSummary(w, []strategy.RestoredSession{session}) return launchTrailRestoredSession(ctx, w, session) }

if !interactive.CanPromptInteractively() { return displayTrailRestoredSessions(w, sessions) }

printTrailRestoredSessionSummary(w, sessions) if len(sessions) == 1 { return launchTrailRestoredSession(ctx, w, sessions[0]) }

selected, ok, err := promptTrailRestoredSession(ctx, w, sessions) if err != nil || !ok { return err } return launchTrailRestoredSession(ctx, w, selected) }

func printTrailRestoredSessionSummary(w io.Writer, sessions []strategy.RestoredSession) { if len(sessions) > 1 { fmt.Fprintf(w, "\n✓ Restored %d checkpoint sessions.\n", len(sessions)) } else if len(sessions) == 1 { fmt.Fprintf(w, "✓ Restored checkpoint session %s.\n", sessions[0].SessionID) } if len(sessions) > 0 && trailRestoredSessionsAreAllReviewOrInvestigation(sessions) { fmt.Fprintln(w, " Only review/investigation checkpoint sessions were found; these are transcript logs and may not appear as trail UI sessions.") } }

func displayTrailRestoredSessions(w io.Writer, sessions []strategy.RestoredSession) error { if len(sessions) == 0 { return nil } choices := buildTrailResumeRestoredSessionChoices(sessions) printTrailRestoredSessionSummary(w, sessions) if len(choices) > 1 { fmt.Fprintln(w, "To continue:") } else { fmt.Fprintln(w, "\nTo continue this checkpoint session:") }

isMulti := len(choices) > 1 mostRecentSessionID := mostRecentRestoredSessionID(sessions) for _, choice := range choices { sessionAgent, err := strategy.ResolveAgentForRewind(choice.Session.Agent) if err != nil { return fmt.Errorf("failed to resolve agent for session %s: %w", choice.SessionID, err) } printSessionCommand(w, sessionAgent.FormatResumeCommand(choice.SessionID), trailRestoredSessionPrompt(choice.Session), isMulti, choice.SessionID == mostRecentSessionID) } return nil }

func mostRecentRestoredSessionID(sessions []strategy.RestoredSession) string { var latestID string var latestTime time.Time for _, session := range sessions { if session.SessionID == "" || session.CreatedAt.IsZero() { continue } if latestID == "" || session.CreatedAt.After(latestTime) { latestID = session.SessionID latestTime = session.CreatedAt } } return latestID }

func resolveTrailResumeSessionContexts(ctx context.Context, branch string) ([]trailResumeSessionContext, error) { sessions, err := resolveTrailCheckpointSessions(ctx, branch) if err == nil && len(sessions) > 0 { return sessions, nil }

items, localErr := resolveTrailResumeSessions(ctx, branch) if localErr != nil { if err != nil { return nil, err } return nil, localErr } localSessions := trailResumeSessionContextsFromLocal(branch, items) if len(localSessions) > 0 { return localSessions, nil } return sessions, err }

func resolveTrailCheckpointSessions(ctx context.Context, branch string) ([]trailResumeSessionContext, error) { repo, err := openRepository(ctx) if err != nil { return nil, fmt.Errorf("not a git repository: %w", err) } defer repo.Close()

result, err := findBranchCheckpointsForBranchRef(repo, branch) if err != nil { return nil, err } if len(result.checkpointIDs) == 0 { return nil, nil }

stores, err := checkpoint.Open(ctx, repo, checkpoint.OpenOptions{BlobFetcher: FetchBlobsByHash}) if err != nil { return nil, fmt.Errorf("open checkpoint store: %w", err) } store := stores.Primary refs := stores.Refs() if refs.ReadBootstrappableFromOrigin() { promoteRemoteTrackingPrimary(ctx, repo, refs) }

checkpointID := result.checkpointIDs[0] if len(result.checkpointIDs) > 1 { latestMetadata, latestErr := resolveLatestCheckpoint(ctx, store, result.checkpointIDs) if latestErr != nil { return nil, fmt.Errorf("resolve latest checkpoint: %w", latestErr) } checkpointID = latestMetadata.CheckpointID }

return readTrailCheckpointSessionContexts(ctx, store, checkpointID) }

func readTrailCheckpointSessionContexts(ctx context.Context, store checkpointInfoReader, checkpointID id.CheckpointID) ([]trailResumeSessionContext, error) { summary, err := checkpoint.ReadCommittedCheckpoint(ctx, store, checkpointID) if err != nil { return nil, fmt.Errorf("read checkpoint %s: %w", checkpointID, err) }

sessions := make([]trailResumeSessionContext, 0, len(summary.Sessions)) for i := range summary.Sessions { content, contentErr := readTrailCheckpointSessionContent(ctx, store, checkpointID, i) if contentErr != nil { continue } metadata := content.Metadata if strings.TrimSpace(metadata.SessionID) == "" { continue } prompt := strategy.ExtractFirstPrompt(content.Prompts) if prompt == "" { prompt = strings.TrimSpace(metadata.ReviewPrompt) } sessions = append(sessions, trailResumeSessionContext{ SessionID: metadata.SessionID, Agent: string(metadata.Agent), LastPrompt: prompt, LastActive: metadata.CreatedAt, CheckpointID: checkpointID.String(), }) }

if len(sessions) == 0 { info, infoErr := readCheckpointInfoFromStore(ctx, store, checkpointID) if infoErr == nil && strings.TrimSpace(info.SessionID) != "" { sessions = append(sessions, trailResumeSessionContext{ SessionID: info.SessionID, Agent: string(info.Agent), LastActive: info.CreatedAt, CheckpointID: checkpointID.String(), }) } }

sort.SliceStable(sessions, func(i, j int) bool { return sessions[i].LastActive.After(sessions[j].LastActive) }) return sessions, nil }

func readTrailCheckpointSessionContent( ctx context.Context, store checkpointInfoReader, checkpointID id.CheckpointID, sessionIndex int, ) (*checkpoint.SessionContent, error) { if reader, ok := store.(interface { ReadSessionMetadataAndPrompts(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*checkpoint.SessionContent, error) }); ok { return reader.ReadSessionMetadataAndPrompts(ctx, checkpointID, sessionIndex) //nolint:wrapcheck // Contextualized by caller. } metadata, err := store.ReadSessionMetadata(ctx, checkpointID, sessionIndex) if err != nil { return nil, err //nolint:wrapcheck // Contextualized by caller. } return &checkpoint.SessionContent{Metadata: *metadata}, nil }

func resolveTrailResumeSessions(ctx context.Context, branch string) ([]resumableSession, error) { states, err := strategy.ListSessionStates(ctx) if err != nil { return nil, fmt.Errorf("failed to list sessions: %w", err) } states = filterResumableSessions(states)

repo, err := openRepository(ctx) if err != nil { items := make([]resumableSession, 0, len(states)) for _, state := range states { if state == nil { continue } items = append(items, resumableSession{ state: state, branch: state.Branch, checkpointID: state.LastCheckpointID, }) } return items, nil } defer repo.Close()

items := resolveResumableBranches(repo, states) for i := range items { if items[i].branch == "" && items[i].state != nil && items[i].state.Branch == branch { items[i].branch = branch } } return items, nil }

func trailResumeSessionContextsFromLocal(branch string, items []resumableSession) []trailResumeSessionContext { var sessions []trailResumeSessionContext for _, item := range items { if item.state == nil || !item.isResumable() || item.branch != branch { continue } sessions = append(sessions, trailResumeSessionContext{ SessionID: item.state.SessionID, Agent: string(item.state.AgentType), LastPrompt: strings.TrimSpace(item.state.LastPrompt), LastActive: sessionLastActiveTime(item.state), CheckpointID: item.checkpointID.String(), }) } sort.SliceStable(sessions, func(i, j int) bool { return sessions[i].LastActive.After(sessions[j].LastActive) }) return sessions }

func loadTrailResumeFindingsContext(ctx context.Context, client *api.Client, trailID string) (trailResumeFindingsContext, error) { if strings.TrimSpace(trailID) == "" { return trailResumeFindingsContext{}, nil }

summaryComments, err := fetchAllTrailReviewComments(ctx, client, trailID, trailReviewSummaryOptions()) if err != nil { return trailResumeFindingsContext{}, err } top, hasMore, err := fetchTrailReviewComments(ctx, client, trailID, trailResumeTopFindingOptions()) if err != nil { return trailResumeFindingsContext{}, err } return trailResumeFindingsContext{ Counts: countTrailReviewComments(summaryComments), Top: top, HasMore: hasMore, }, nil }

func trailResumeTopFindingOptions() trailReviewListOptions { return trailReviewListOptions{ Status: trailReviewStatusOpen, Severity: strings.Join([]string{trailReviewSeverityHigh, trailReviewSeverityMedium}, ","), Freshness: trailReviewFreshnessCurrent, Limit: 3, } }

func buildTrailResumeContext(found api.TrailResource, sessions []trailResumeSessionContext, findings trailResumeFindingsContext) trailResumeContext { trailCtx := trailResumeTrailContext{ ID: found.ID, Number: found.Number, Title: strings.TrimSpace(found.Title), Branch: strings.TrimSpace(found.Branch), Base: strings.TrimSpace(found.Base), Status: strings.TrimSpace(found.Status), Phase: strings.TrimSpace(found.Phase), URL: strings.TrimSpace(found.URL), }

sort.SliceStable(sessions, func(i, j int) bool { return sessions[i].LastActive.After(sessions[j].LastActive) })

var defaultResume *trailResumeDefaultContext if len(sessions) > 0 { defaultResume = &trailResumeDefaultContext{ Branch: trailCtx.Branch, SessionID: sessions[0].SessionID, CheckpointID: sessions[0].CheckpointID, } } else { defaultResume = &trailResumeDefaultContext{Branch: trailCtx.Branch} }

ctx := trailResumeContext{ Trail: trailCtx, Sessions: sessions, Findings: findings, DefaultResume: defaultResume, } ctx.Commands = buildTrailResumeCommands(ctx) return ctx }

func buildTrailResumeCommands(ctx trailResumeContext) []string { selector := trailResumeSelectorForCommands(ctx.Trail) if selector == "" { return nil } arg := shellArg(selector) commands := []string{ "entire trail finding " + arg + " --json", "entire trail resume " + arg, } if ctx.DefaultResume != nil && ctx.DefaultResume.CheckpointID != "" { commands = append(commands, "entire trail resume "+arg+" --checkpoint "+shellArg(ctx.DefaultResume.CheckpointID)) } for _, session := range ctx.Sessions { commands = append(commands, "entire trail resume "+arg+" --session "+shellArg(session.SessionID)) } return commands }

func trailResumeSelectorForCommands(trail trailResumeTrailContext) string { if trail.Number > 0 { return strconv.Itoa(trail.Number) } if trail.ID != "" { return trail.ID } return trail.Branch }

func shellArg(s string) string { if s == "" { return "''" } for , r := range s { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '' || r == '.' || r == '/' || r == ':' { continue } return shellQuote(s) } return s }

func printTrailResumeContext(w io.Writer, ctx trailResumeContext) { printTrailResumeTrail(w, ctx.Trail) printTrailResumeSessions(w, ctx.Sessions) printTrailResumeFindings(w, ctx.Findings) printTrailResumeCommands(w, ctx.Commands) fmt.Fprintln(w) }

func printTrailResumeTrail(w io.Writer, trail trailResumeTrailContext) { switch { case trail.Number > 0: fmt.Fprintf(w, " Trail #%d %s\n", trail.Number, trail.Title) case trail.ID != "": fmt.Fprintf(w, " Trail %s %s\n", trail.ID, trail.Title) default: fmt.Fprintf(w, " Trail %s\n", trail.Title) } parts := []string{} if trail.Status != "" { parts = append(parts, "Status: "+trail.Status) } if trail.Phase != "" { parts = append(parts, "Phase: "+trail.Phase) } if trail.Branch != "" { parts = append(parts, "Branch: "+trail.Branch) } if len(parts) > 0 { fmt.Fprintf(w, " %s\n", strings.Join(parts, " · ")) } if trail.Base != "" { fmt.Fprintf(w, " Base: %s\n", trail.Base) } if trail.URL != "" { fmt.Fprintf(w, " URL: %s\n", trail.URL) } fmt.Fprintln(w) }

func printTrailResumeSessions(w io.Writer, sessions []trailResumeSessionContext) { fmt.Fprintln(w, " Checkpoint sessions:") if len(sessions) == 0 { fmt.Fprintln(w, " none found before restore") fmt.Fprintln(w) return } var table strings.Builder tw := tabwriter.NewWriter(&table, 0, 0, 2, ' ', 0) fmt.Fprintln(tw, "SESSION\tAGENT\tCHECKPOINT\tLAST ACTIVE\tPROMPT") for _, session := range sessions { prompt := strings.TrimSpace(session.LastPrompt) if prompt == "" { prompt = trailResumeNoPrompt } else { prompt = stringutil.TruncateRunes(stringutil.CollapseWhitespace(prompt), 72, "...") } agent := session.Agent if agent == "" { agent = unknownPlaceholder } when := "-" if !session.LastActive.IsZero() { when = timeAgo(session.LastActive) } fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", abbreviate12(session.SessionID), agent, session.CheckpointID, when, prompt, ) } _ = tw.Flush() printIndentedBlock(w, table.String(), " ") fmt.Fprintln(w) }

func printTrailResumeFindings(w io.Writer, findings trailResumeFindingsContext) { counts := findings.Counts fmt.Fprintf(w, " Findings: open %d high %d medium %d low %d resolved %d dismissed %d stale %d\n", counts.Open, counts.OpenHigh, counts.OpenMedium, counts.OpenLow, counts.Resolved, counts.Dismissed, counts.Stale) if findings.Unavailable != "" { fmt.Fprintf(w, " unavailable: %s\n\n", findings.Unavailable) return } if len(findings.Top) == 0 { fmt.Fprintln(w, " no current high/medium open findings") fmt.Fprintln(w) return } var table strings.Builder tw := tabwriter.NewWriter(&table, 0, 0, 2, ' ', 0) fmt.Fprintln(tw, "ID\tSEV\tLOCATION\tSUMMARY") for _, finding := range findings.Top { fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", abbreviate12(finding.ID), severityTableDisplay(finding.Severity), trailReviewLocationDisplay(finding.Location), trailReviewCommentSummary(finding), ) } _ = tw.Flush() printIndentedBlock(w, table.String(), " ") if findings.HasMore { fmt.Fprintln(w, " more high/medium findings available; run the finding command for the full list") } fmt.Fprintln(w) }

func printTrailResumeCommands(w io.Writer, commands []string) { if len(commands) == 0 { return } fmt.Fprintln(w, " Commands:") for _, command := range commands { fmt.Fprintf(w, " %s\n", command) } }

func encodeTrailResumeContextJSON(w io.Writer, ctx trailResumeContext) error { payload := struct { Trail trailResumeTrailContext json:"trail" Sessions []trailResumeSessionContext json:"sessions" FindingsSummary trailResumeFindingCounts json:"findings_summary" Findings []api.TrailReviewComment json:"findings" FindingsHasMore bool json:"findings_has_more,omitempty" FindingsUnavailable string json:"findings_unavailable,omitempty" DefaultResume *trailResumeDefaultContext json:"default_resume,omitempty" Commands []string json:"commands" }{ Trail: ctx.Trail, Sessions: ctx.Sessions, FindingsSummary: trailResumeFindingCountsFromReviewCounts(ctx.Findings.Counts), Findings: ctx.Findings.Top, FindingsHasMore: ctx.Findings.HasMore, FindingsUnavailable: ctx.Findings.Unavailable, DefaultResume: ctx.DefaultResume, Commands: ctx.Commands, }

enc := json.NewEncoder(w) enc.SetIndent("", " ") if err := enc.Encode(payload); err != nil { return fmt.Errorf("encode trail resume JSON: %w", err) } return nil }

func trailResumeFindingCountsFromReviewCounts(counts trailReviewCommentCounts) trailResumeFindingCounts { return trailResumeFindingCounts(counts) }

func findTrailResumeSession(sessions []trailResumeSessionContext, sessionID string) (trailResumeSessionContext, bool) { for _, session := range sessions { if session.SessionID == sessionID { return session, true } } return trailResumeSessionContext{}, false }

type trailResumeRestoredSessionChoice struct { SessionID string Label string Session strategy.RestoredSession }

func buildTrailResumeRestoredSessionChoices(sessions []strategy.RestoredSession) []trailResumeRestoredSessionChoice { sorted := append([]strategy.RestoredSession(nil), sessions...) sort.SliceStable(sorted, func(i, j int) bool { if trailRestoredSessionSortRank(sorted[i]) != trailRestoredSessionSortRank(sorted[j]) { return trailRestoredSessionSortRank(sorted[i]) < trailRestoredSessionSortRank(sorted[j]) } return sorted[i].CreatedAt.After(sorted[j].CreatedAt) })

choices := make([]trailResumeRestoredSessionChoice, 0, len(sorted)) for i, session := range sorted { choices = append(choices, trailResumeRestoredSessionChoice{ SessionID: session.SessionID, Label: trailRestoredSessionChoiceLabel(session, i == 0 && len(sorted) > 1), Session: session, }) } return choices }

func trailRestoredSessionSortRank(session strategy.RestoredSession) int { switch sessionpkg.Kind(session.Kind) { case sessionpkg.KindAgentReview, sessionpkg.KindAgentInvestigate: return 1 default: if trailRestoredSessionLooksReviewLike(session) { return 1 } return 0 } }

func trailRestoredSessionChoiceLabel(session strategy.RestoredSession, isDefault bool) string { prompt := trailRestoredSessionPrompt(session) if prompt == "" { prompt = trailResumeNoPrompt } else { prompt = stringutil.TruncateRunes(stringutil.CollapseWhitespace(prompt), 50, "...") } agentName := strings.TrimSpace(string(session.Agent)) if agentName == "" { agentName = unknownAgentLabel } when := "-" if !session.CreatedAt.IsZero() { when = timeAgo(session.CreatedAt) } parts := []string{session.SessionID, prompt, agentName, "last active " + when} if kindLabel := trailRestoredSessionKindLabel(session.Kind); kindLabel != "" { parts = append(parts, kindLabel) } else if trailRestoredSessionLooksReviewLike(session) { parts = append(parts, "review") } if isDefault { parts = append(parts, "default") } return strings.Join(parts, " · ") }

func trailRestoredSessionKindLabel(kind string) string { switch sessionpkg.Kind(kind) { case sessionpkg.KindAgentReview: return "review" case sessionpkg.KindAgentInvestigate: return "investigation" default: return "" } }

func trailRestoredSessionPrompt(session strategy.RestoredSession) string { if prompt := strings.TrimSpace(session.Prompt); prompt != "" { return prompt } return strings.TrimSpace(session.ReviewPrompt) }

func trailRestoredSessionLooksReviewLike(session strategy.RestoredSession) bool { prompt := strings.ToLower(trailRestoredSessionPrompt(session)) return strings.HasPrefix(prompt, "review the code changes") || strings.HasPrefix(prompt, "review this branch") || strings.HasPrefix(prompt, "review the branch") }

func trailRestoredSessionsAreAllReviewOrInvestigation(sessions []strategy.RestoredSession) bool { for _, session := range sessions { if trailRestoredSessionSortRank(session) == 0 { return false } } return len(sessions) > 0 }

func promptTrailRestoredSession(ctx context.Context, w io.Writer, sessions []strategy.RestoredSession) (strategy.RestoredSession, bool, error) { choices := buildTrailResumeRestoredSessionChoices(sessions) if len(choices) == 0 { return strategy.RestoredSession{}, false, nil }

options := make([]huh.Option[string], 0, len(choices)+1) for _, choice := range choices { options = append(options, huh.NewOption(choice.Label, choice.SessionID)) } options = append(options, huh.NewOption("Cancel", resumePickerCancel))

selected := choices[0].SessionID form := NewAccessibleForm( huh.NewGroup( huh.NewSelectstring. Title("Choose a checkpoint session to resume"). Description("These are agent transcript logs restored from the branch checkpoint; they may not appear as trail UI sessions."). Options(options...). Value(&selected), ), ) if err := form.RunWithContext(ctx); err != nil { if errors.Is(err, huh.ErrUserAborted) || errors.Is(err, context.Canceled) { return strategy.RestoredSession{}, false, nil } return strategy.RestoredSession{}, false, fmt.Errorf("selection failed: %w", err) } if selected == "" || selected == resumePickerCancel { fmt.Fprintln(w, "Resume cancelled.") return strategy.RestoredSession{}, false, nil } for _, choice := range choices { if choice.SessionID == selected { return choice.Session, true, nil } } return strategy.RestoredSession{}, false, fmt.Errorf("invalid selection %q", selected) }

func findTrailRestoredSession(sessions []strategy.RestoredSession, sessionID string) (strategy.RestoredSession, bool) { for _, session := range sessions { if session.SessionID == sessionID { return session, true } } return strategy.RestoredSession{}, false }

func launchTrailRestoredSession(ctx context.Context, w io.Writer, session strategy.RestoredSession) error { resumeAgent, err := strategy.ResolveAgentForRewind(session.Agent) if err != nil { return fmt.Errorf("failed to resolve agent for session %s: %w", session.SessionID, err) } resumeCmd := resumeAgent.FormatResumeCommand(session.SessionID) launcher, ok := agent.ResumeLauncherFor(resumeAgent.Name()) if !ok { fmt.Fprintf(w, "\nTo continue this session:\n") printSessionCommand(w, resumeCmd, session.Prompt, false, true) return nil } cmd, err := launcher.LaunchResumeCmd(ctx, session.SessionID) if err != nil { fmt.Fprintf(w, "\nCould not launch %s: %v\n", resumeCmd, err) fmt.Fprintf(w, "\nTo continue this session:\n") printSessionCommand(w, resumeCmd, session.Prompt, false, true) return nil } fmt.Fprintf(w, "\nLaunching: %s\n", resumeCmd) if err := cmd.Run(); err != nil { return fmt.Errorf("resume command failed: %w", err) } return nil }

func runTrailResumePicker(ctx context.Context, cmd *cobra.Command, branch string, sessions []trailResumeSessionContext, force bool) error { if !ensureTrailResumeBranchAvailable(ctx, cmd.OutOrStdout(), branch) { return nil }

options := make([]huh.Option[string], 0, len(sessions)+1) for _, session := range sessions { options = append(options, huh.NewOption(trailResumeSessionOptionLabel(session), session.SessionID)) } options = append(options, huh.NewOption("Cancel", resumePickerCancel))

selected := sessions[0].SessionID form := NewAccessibleForm( huh.NewGroup( huh.NewSelectstring. Title("Choose a checkpoint session to resume"). Description("These sessions are recorded in the trail branch checkpoint."). Options(options...). Value(&selected), ), ) if err := form.RunWithContext(ctx); err != nil { if errors.Is(err, huh.ErrUserAborted) || errors.Is(err, context.Canceled) { return nil } return fmt.Errorf("selection failed: %w", err) } if selected == "" || selected == resumePickerCancel { fmt.Fprintln(cmd.OutOrStdout(), "Resume cancelled.") return nil } sessionCtx, ok := findTrailResumeSession(sessions, selected) if !ok { return fmt.Errorf("invalid selection %q", selected) } return resumeTrailCheckpoint(ctx, cmd, branch, id.CheckpointID(sessionCtx.CheckpointID), sessionCtx.SessionID, force) }

func ensureTrailResumeBranchAvailable(ctx context.Context, w io.Writer, branch string) bool { otherPath, ok := branchCheckedOutElsewhere(ctx, branch) if !ok { return true } fmt.Fprint(w, trailResumeWorktreeClashMessage(branch, otherPath)) return false }

func trailResumeWorktreeClashMessage(branch, otherPath string) string { var b strings.Builder fmt.Fprintf(&b, "Branch %q is already checked out in another worktree:\n", branch) fmt.Fprintf(&b, " %s\n\n", otherPath) fmt.Fprintf(&b, "Resume from that worktree with:\n") fmt.Fprintf(&b, " cd %s && entire trail resume %s\n", shellQuote(otherPath), shellArg(branch)) return b.String() }

func trailResumeSessionOptionLabel(session trailResumeSessionContext) string { prompt := strings.TrimSpace(session.LastPrompt) if prompt == "" { prompt = trailResumeNoPrompt } else { prompt = stringutil.TruncateRunes(stringutil.CollapseWhitespace(prompt), 50, "...") } agent := session.Agent if agent == "" { agent = unknownAgentLabel } when := "-" if !session.LastActive.IsZero() { when = timeAgo(session.LastActive) } return fmt.Sprintf("%s · %s · %s · last active %s", session.SessionID, prompt, agent, when) }


Acmd/entire/cli/trail\_resume\_cmd.go+1023

package cli

import (
    "bytes"
    "context"
    "encoding/json"
    "os"
    "path/filepath"
    "strings"
    "testing"
    "time"

"github.com/entireio/cli/cmd/entire/cli/agent"
    "github.com/entireio/cli/cmd/entire/cli/agent/types"
    "github.com/entireio/cli/cmd/entire/cli/api"
    "github.com/entireio/cli/cmd/entire/cli/checkpoint"
    "github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
    "github.com/entireio/cli/cmd/entire/cli/session"
    "github.com/entireio/cli/cmd/entire/cli/strategy"
    "github.com/entireio/cli/cmd/entire/cli/testutil"
    "github.com/entireio/cli/redact"

"github.com/go-git/go-git/v6"
    "github.com/go-git/go-git/v6/plumbing/object"
)

func TestTrailResumeCmdRejectsConflictingSelectors(t *testing.T) {
    t.Parallel()

cmd := newTrailResumeCmd()
    cmd.SetOut(&bytes.Buffer{})
    cmd.SetErr(&bytes.Buffer{})
    cmd.SetArgs([]string{"575", "--trail", "feature/a"})

err := cmd.Execute()
    if err == nil {
        t.Fatal("expected error combining positional trail with --trail, got nil")
    }
    if !strings.Contains(err.Error(), "not both") {
        t.Fatalf("error = %q, want it to mention 'not both'", err)
    }
}

func TestValidateTrailResumeOptions(t *testing.T) {
    t.Parallel()

cases := []struct {
        name    string
        opts    trailResumeOptions
        wantErr string
    }{
        {
            name:    "session and checkpoint conflict",
            opts:    trailResumeOptions{SessionID: "session-1", CheckpointID: "0123456789ab"},
            wantErr: "cannot combine --session and --checkpoint",
        },
        {
            name:    "json requires no resume",
            opts:    trailResumeOptions{JSON: true},
            wantErr: "--json can only be used with --no-resume",
        },
        {
            name: "json no resume accepted",
            opts: trailResumeOptions{JSON: true, NoResume: true},
        },
    }
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            t.Parallel()
            err := validateTrailResumeOptions(tc.opts)
            if tc.wantErr == "" {
                if err != nil {
                    t.Fatalf("validateTrailResumeOptions() = %v, want nil", err)
                }
                return
            }
            if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
                t.Fatalf("validateTrailResumeOptions() = %v, want %q", err, tc.wantErr)
            }
        })
    }
}

func TestBuildTrailResumeContextSortsCheckpointSessions(t *testing.T) {
    t.Parallel()

now := time.Date(2026, 6, 23, 12, 0, 0, 0, time.UTC)
    ctx := buildTrailResumeContext(api.TrailResource{
        ID:     "trl_1",
        Number: 575,
        Title:  "Add trail resume",
        Branch: "feature/trail-resume",
        Status: "open",
        Phase:  "has_code",
    }, []trailResumeSessionContext{
        {
            SessionID:    "old-session",
            Agent:        "claude-code",
            LastPrompt:   "older work",
            LastActive:   now.Add(-time.Hour),
            CheckpointID: "bbbbbbbbbbbb",
        },
        {
            SessionID:    "new-session",
            Agent:        "codex",
            LastPrompt:   "newer work",
            LastActive:   now,
            CheckpointID: "aaaaaaaaaaaa",
        },
    }, trailResumeFindingsContext{})

if len(ctx.Sessions) != 2 {
        t.Fatalf("sessions len = %d, want 2: %#v", len(ctx.Sessions), ctx.Sessions)
    }
    if ctx.Sessions[0].SessionID != "new-session" || ctx.Sessions[0].CheckpointID != "aaaaaaaaaaaa" {
        t.Fatalf("first session = %#v, want newest trail session", ctx.Sessions[0])
    }
    if ctx.Sessions[1].SessionID != "old-session" {
        t.Fatalf("second session = %#v, want old-session", ctx.Sessions[1])
    }
    if ctx.DefaultResume == nil || ctx.DefaultResume.SessionID != "new-session" {
        t.Fatalf("DefaultResume = %#v, want new-session", ctx.DefaultResume)
    }
}

func TestResolveTrailCheckpointSessionsUsesBranchCheckpointMetadata(t *testing.T) {
    tmpDir := t.TempDir()
    t.Chdir(tmpDir)

testutil.InitRepo(t, tmpDir)
    repo, err := git.PlainOpen(tmpDir)
    if err != nil {
        t.Fatalf("open repo: %v", err)
    }
    t.Cleanup(func() { _ = repo.Close() })
    wt, err := repo.Worktree()
    if err != nil {
        t.Fatalf("worktree: %v", err)
    }
    if err := os.WriteFile(filepath.Join(tmpDir, "readme.md"), []byte("init\n"), 0o644); err != nil {
        t.Fatalf("write readme: %v", err)
    }
    if _, err := wt.Add("readme.md"); err != nil {
        t.Fatalf("add readme: %v", err)
    }
    if _, err := wt.Commit("init", &git.CommitOptions{Author: testTrailResumeSignature(time.Date(2026, 6, 23, 9, 0, 0, 0, time.UTC))}); err != nil {
        t.Fatalf("commit init: %v", err)
    }

if err := wt.Checkout(&git.CheckoutOptions{Create: true, Branch: "refs/heads/feature/trail"}); err != nil {
        t.Fatalf("checkout feature: %v", err)
    }
    cpID := id.MustCheckpointID("abc123def456")
    firstTime := time.Date(2026, 6, 23, 10, 0, 0, 0, time.UTC)
    secondTime := firstTime.Add(time.Hour)
    writeTrailResumeCheckpointSession(t, repo, cpID, "session-alice", firstTime, agent.AgentTypeClaudeCode, "alice started this trail")
    writeTrailResumeCheckpointSession(t, repo, cpID, "session-bob", secondTime, agent.AgentTypeCodex, "bob continued from another machine")
    if err := os.WriteFile(filepath.Join(tmpDir, "readme.md"), []byte("feature\n"), 0o644); err != nil {
        t.Fatalf("write feature: %v", err)
    }
    if _, err := wt.Add("readme.md"); err != nil {
        t.Fatalf("add feature: %v", err)
    }
    if _, err := wt.Commit("feature work\n\nEntire-Checkpoint: "+cpID.String(), &git.CommitOptions{Author: testTrailResumeSignature(secondTime)}); err != nil {
        t.Fatalf("commit feature: %v", err)
    }

sessions, err := resolveTrailCheckpointSessions(context.Background(), "feature/trail")
    if err != nil {
        t.Fatalf("resolveTrailCheckpointSessions() error = %v", err)
    }
    if len(sessions) != 2 {
        t.Fatalf("sessions len = %d, want 2: %#v", len(sessions), sessions)
    }
    if sessions[0].SessionID != "session-bob" || sessions[0].CheckpointID != cpID.String() {
        t.Fatalf("first session = %#v, want newest checkpoint session", sessions[0])
    }
    if sessions[0].Agent != string(agent.AgentTypeCodex) {
        t.Fatalf("first agent = %q, want %q", sessions[0].Agent, agent.AgentTypeCodex)
    }
    if sessions[0].LastPrompt != "bob continued from another machine" {
        t.Fatalf("first prompt = %q", sessions[0].LastPrompt)
    }
    if sessions[1].SessionID != "session-alice" {
        t.Fatalf("second session = %#v, want session-alice", sessions[1])
    }
}

func testTrailResumeSignature(when time.Time) *object.Signature {
    return &object.Signature{
        Name:  "Test User",
        Email: "test@example.com",
        When:  when,
    }
}

func writeTrailResumeCheckpointSession(
    t *testing.T,
    repo *git.Repository,
    checkpointID id.CheckpointID,
    sessionID string,
    createdAt time.Time,
    agentType types.AgentType,
    prompt string,
) {
    t.Helper()

if err := checkpoint.NewGitStore(repo, checkpoint.DefaultV1Refs()).WriteCommitted(context.Background(), checkpoint.WriteCommittedOptions{
        CheckpointID: checkpointID,
        SessionID:    sessionID,
        CreatedAt:    createdAt,
        Strategy:     resumeTestStrategy,
        Branch:       "feature/trail",
        Transcript:   redact.AlreadyRedacted([]byte(`{"type":"user","message":{"content":[{"type":"text","text":"` + prompt + `"}]}}` + "\n")),
        Prompts:      []string{prompt},
        Agent:        agentType,
        AuthorName:   "Test",
        AuthorEmail:  "test@example.com",
    }); err != nil {
        t.Fatalf("WriteCommitted(%s): %v", sessionID, err)
    }
}

func TestPrintTrailResumeContextIncludesSessionsFindingsAndCommands(t *testing.T) {
    t.Parallel()

sev := trailReviewSeverityHigh
    line := 42
    file := "cmd/entire/cli/trail_cmd.go"
    ctx := trailResumeContext{
        Trail: trailResumeTrailContext{
            ID:     "trl_1",
            Number: 575,
            Title:  "Add trail resume",
            Branch: "feature/trail-resume",
            Status: "open",
            Phase:  "has_code",
            URL:    "https://entire.io/gh/o/r/trails/575",
        },
        Sessions: []trailResumeSessionContext{{
            SessionID:    "session-1",
            Agent:        "codex",
            LastPrompt:   "implement trail resume",
            LastActive:   time.Date(2026, 6, 23, 12, 0, 0, 0, time.UTC),
            CheckpointID: "aaaaaaaaaaaa",
        }},
        Findings: trailResumeFindingsContext{
            Counts: trailReviewCommentCounts{Open: 1, OpenHigh: 1, Resolved: 2},
            Top: []api.TrailReviewComment{{
                ID:       "finding-1",
                Body:     trailReviewStrPtr("Resume output should show context"),
                Severity: &sev,
                Status:   trailReviewStatusOpen,
                Location: api.TrailReviewLocation{
                    Granularity: "line",
                    FilePath:    &file,
                    StartLine:   &line,
                },
            }},
        },
        Commands: []string{
            "entire trail finding 575 --json",
            "entire trail resume 575 --session session-1",
        },
    }

var out strings.Builder
    printTrailResumeContext(&out, ctx)
    text := out.String()
    for _, want := range []string{
        "Trail #575  Add trail resume",
        "Status: open · Phase: has_code · Branch: feature/trail-resume",
        "Checkpoint sessions:",
        "session-1",
        "codex",
        "aaaaaaaaaaaa",
        "Findings: open 1",
        "high 1",
        "finding-1",
        "cmd/entire/cli/trail_cmd.go:42",
        "Resume output should show context",
        "Commands:",
        "entire trail finding 575 --json",
        "entire trail resume 575 --session session-1",
    } {
        if !strings.Contains(text, want) {
            t.Fatalf("context output missing %q:\n%s", want, text)
        }
    }
}

func TestEncodeTrailResumeContextJSON(t *testing.T) {
    t.Parallel()

sev := trailReviewSeverityHigh
    ctx := trailResumeContext{
        Trail: trailResumeTrailContext{ID: "trl_1", Number: 575, Branch: "feature/trail-resume"},
        Sessions: []trailResumeSessionContext{{
            SessionID:    "session-1",
            CheckpointID: "aaaaaaaaaaaa",
        }},
        Findings: trailResumeFindingsContext{
            Counts: trailReviewCommentCounts{Open: 1, OpenHigh: 1},
            Top: []api.TrailReviewComment{{
                ID:       "finding-1",
                Severity: &sev,
                Status:   trailReviewStatusOpen,
            }},
        },
        DefaultResume: &trailResumeDefaultContext{SessionID: "session-1", CheckpointID: "aaaaaaaaaaaa", Branch: "feature/trail-resume"},
        Commands:      []string{"entire trail resume 575 --session session-1"},
    }

var out bytes.Buffer
    if err := encodeTrailResumeContextJSON(&out, ctx); err != nil {
        t.Fatalf("encodeTrailResumeContextJSON: %v", err)
    }
    var decoded struct {
        Trail struct {
            ID     string `json:"id"`
            Number int    `json:"number"`
            Branch string `json:"branch"`
        } `json:"trail"`
        Sessions []struct {
            SessionID string `json:"session_id"`
        } `json:"sessions"`
        DefaultResume struct {
            SessionID string `json:"session_id"`
        } `json:"default_resume"`
        FindingsSummary struct {
            Open     int `json:"open"`
            OpenHigh int `json:"open_high"`
        } `json:"findings_summary"`
        Findings []struct {
            ID string `json:"id"`
        } `json:"findings"`
        Commands []string `json:"commands"`
    }
    if err := json.Unmarshal(out.Bytes(), &decoded); err != nil {
        t.Fatalf("unmarshal output: %v\n%s", err, out.String())
    }
    if decoded.Trail.ID != "trl_1" || decoded.Trail.Number != 575 || decoded.Trail.Branch != "feature/trail-resume" {
        t.Fatalf("decoded trail = %#v", decoded.Trail)
    }
    if len(decoded.Sessions) != 1 || decoded.Sessions[0].SessionID != "session-1" {
        t.Fatalf("decoded sessions = %#v", decoded.Sessions)
    }
    if decoded.DefaultResume.SessionID != "session-1" {
        t.Fatalf("decoded default_resume = %#v", decoded.DefaultResume)
    }
    if decoded.FindingsSummary.Open != 1 || decoded.FindingsSummary.OpenHigh != 1 {
        t.Fatalf("decoded findings_summary = %#v", decoded.FindingsSummary)
    }
    if len(decoded.Findings) != 1 || decoded.Findings[0].ID != "finding-1" {
        t.Fatalf("decoded findings = %#v", decoded.Findings)
    }
}

func TestBuildTrailResumeRestoredSessionChoicesDefaultsToMostRecent(t *testing.T) {
    t.Parallel()

oldTime := time.Date(2026, 6, 22, 14, 30, 0, 0, time.UTC)
    newTime := time.Date(2026, 6, 23, 7, 39, 0, 0, time.UTC)
    choices := buildTrailResumeRestoredSessionChoices([]strategy.RestoredSession{
        {
            SessionID: "019eefbd-bb6a-7f51-a909-feb4cd95588d",
            Agent:     types.AgentType("Codex"),
            Prompt:    "set up the persistent checkpoint contract",
            CreatedAt: oldTime,
        },
        {
            SessionID: "019ef36b-a485-7ca2-992b-b4f164266e7f",
            Agent:     types.AgentType("Codex"),
            Prompt:    "finish the api/checkpoint extraction",
            CreatedAt: newTime,
        },
    })

if len(choices) != 2 {
        t.Fatalf("choices len = %d, want 2", len(choices))
    }
    if choices[0].SessionID != "019ef36b-a485-7ca2-992b-b4f164266e7f" {
        t.Fatalf("first choice = %#v, want most recent restored session", choices[0])
    }
    if !strings.Contains(choices[0].Label, "default") {
        t.Fatalf("first choice label = %q, want default marker", choices[0].Label)
    }
    if choices[1].SessionID != "019eefbd-bb6a-7f51-a909-feb4cd95588d" {
        t.Fatalf("second choice = %#v, want older restored session", choices[1])
    }
}

func TestBuildTrailResumeRestoredSessionChoicesPrefersWorkSessionOverReview(t *testing.T) {
    t.Parallel()

workTime := time.Date(2026, 6, 23, 7, 30, 0, 0, time.UTC)
    reviewTime := workTime.Add(10 * time.Minute)
    choices := buildTrailResumeRestoredSessionChoices([]strategy.RestoredSession{
        {
            SessionID: "work-session",
            Agent:     types.AgentType("Codex"),
            Prompt:    "extract the persistent contract",
            CreatedAt: workTime,
        },
        {
            SessionID:    "review-session",
            Agent:        types.AgentType("Codex"),
            Kind:         "agent_review",
            ReviewPrompt: "Review the code changes introduced by commit f9000bc1a.",
            CreatedAt:    reviewTime,
        },
    })

if len(choices) != 2 {
        t.Fatalf("choices len = %d, want 2", len(choices))
    }
    if choices[0].SessionID != "work-session" {
        t.Fatalf("first choice = %#v, want normal work session before newer review session", choices[0])
    }
    if !strings.Contains(choices[0].Label, "default") {
        t.Fatalf("work choice label = %q, want default marker", choices[0].Label)
    }
    if choices[1].SessionID != "review-session" {
        t.Fatalf("second choice = %#v, want review session after work session", choices[1])
    }
    if !strings.Contains(choices[1].Label, "review") {
        t.Fatalf("review choice label = %q, want review marker", choices[1].Label)
    }
    if !strings.Contains(choices[1].Label, "Review the code changes") {
        t.Fatalf("review choice label = %q, want review prompt fallback", choices[1].Label)
    }
}

func TestBuildTrailResumeRestoredSessionChoicesPrefersWorkSessionOverReviewPrompt(t *testing.T) {
    t.Parallel()

workTime := time.Date(2026, 6, 23, 7, 30, 0, 0, time.UTC)
    reviewTime := workTime.Add(10 * time.Minute)
    choices := buildTrailResumeRestoredSessionChoices([]strategy.RestoredSession{
        {
            SessionID: "work-session",
            Agent:     types.AgentType("Codex"),
            Prompt:    "extract the persistent contract",
            CreatedAt: workTime,
        },
        {
            SessionID: "review-session",
            Agent:     types.AgentType("Codex"),
            Prompt:    "Review the code changes introduced by commit f9000bc1a.",
            CreatedAt: reviewTime,
        },
    })

if len(choices) != 2 {
        t.Fatalf("choices len = %d, want 2", len(choices))
    }
    if choices[0].SessionID != "work-session" {
        t.Fatalf("first choice = %#v, want work session before newer review prompt", choices[0])
    }
    if choices[1].SessionID != "review-session" {
        t.Fatalf("second choice = %#v, want review prompt after work session", choices[1])
    }
    if !strings.Contains(choices[1].Label, "review") {
        t.Fatalf("review choice label = %q, want review marker", choices[1].Label)
    }
}

func TestPrintTrailRestoredSessionSummaryIdentifiesReviewOnlyCheckpointSessions(t *testing.T) {
    t.Parallel()

var out strings.Builder
    printTrailRestoredSessionSummary(&out, []strategy.RestoredSession{
        {
            SessionID:    "review-session-1",
            Kind:         string(session.KindAgentReview),
            ReviewPrompt: "Review the code changes introduced by commit abc123.",
        },
        {
            SessionID: "review-session-2",
            Prompt:    "Review this branch for regressions.",
        },
    })

text := out.String()
    for _, want := range []string{
        "Restored 2 checkpoint sessions",
        "Only review/investigation checkpoint sessions were found",
        "may not appear as trail UI sessions",
    } {
        if !strings.Contains(text, want) {
            t.Fatalf("summary missing %q:\n%s", want, text)
        }
    }
}

func TestDisplayTrailRestoredSessionsIncludesReviewWarning(t *testing.T) {
    t.Parallel()

var out strings.Builder
    err := displayTrailRestoredSessions(&out, []strategy.RestoredSession{
        {
            SessionID:    "019ef36b-a485-7ca2-992b-b4f164266e7f",
            Agent:        types.AgentType("Codex"),
            Kind:         string(session.KindAgentReview),
            ReviewPrompt: "Review the code changes introduced by commit abc123.",
            CreatedAt:    time.Date(2026, 6, 23, 7, 39, 0, 0, time.UTC),
        },
    })
    if err != nil {
        t.Fatalf("displayTrailRestoredSessions() error = %v", err)
    }

text := out.String()
    for _, want := range []string{
        "Restored checkpoint session 019ef36b-a485-7ca2-992b-b4f164266e7f",
        "Only review/investigation checkpoint sessions were found",
        "To continue this checkpoint session:",
        "codex resume 019ef36b-a485-7ca2-992b-b4f164266e7f",
        "Review the code changes introduced by commit abc123.",
    } {
        if !strings.Contains(text, want) {
            t.Fatalf("display output missing %q:\n%s", want, text)
        }
    }
}

func TestDisplayTrailRestoredSessionsMarksActualMostRecent(t *testing.T) {
    t.Parallel()

workTime := time.Date(2026, 6, 23, 7, 30, 0, 0, time.UTC)
    reviewTime := workTime.Add(10 * time.Minute)
    var out strings.Builder
    err := displayTrailRestoredSessions(&out, []strategy.RestoredSession{
        {
            SessionID: "work-session",
            Agent:     agent.AgentTypeClaudeCode,
            Prompt:    "continue implementation",
            CreatedAt: workTime,
        },
        {
            SessionID:    "review-session",
            Agent:        types.AgentType("Codex"),
            Kind:         string(session.KindAgentReview),
            ReviewPrompt: "Review this branch for regressions.",
            CreatedAt:    reviewTime,
        },
    })
    if err != nil {
        t.Fatalf("displayTrailRestoredSessions() error = %v", err)
    }

text := out.String()
    workLine := lineContaining(text, "claude -r work-session")
    if strings.Contains(workLine, "most recent") {
        t.Fatalf("work session command should not be marked most recent:\n%s", text)
    }
    reviewLine := lineContaining(text, "codex resume review-session")
    if !strings.Contains(reviewLine, "most recent") {
        t.Fatalf("newest review session command should be marked most recent:\n%s", text)
    }
}

func lineContaining(text, needle string) string {
    for _, line := range strings.Split(text, "\n") {
        if strings.Contains(line, needle) {
            return line
        }
    }
    return ""
}

func TestTrailResumeWorktreeClashMessage(t *testing.T) {
    t.Parallel()

msg := trailResumeWorktreeClashMessage("feature/work", "/tmp/path with spaces")
    for _, want := range []string{
        `Branch "feature/work" is already checked out in another worktree:`,
        "/tmp/path with spaces",
        "Resume from that worktree with:",
        "cd '/tmp/path with spaces' && entire trail resume feature/work",
    } {
        if !strings.Contains(msg, want) {
            t.Fatalf("message missing %q:\n%s", want, msg)
        }
    }
}

Acmd/entire/cli/trail_resume_cmd_test.go+586