agent: centralize resume launch commands · Entire

agent: centralize resume launch commands

4475372→main·

Sessions

eddbfc484319View transcript

Changes

14

291 unmodified lines
292
293
294
295
296
297
298
299
300
301
302
303
295
296
297

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
243
244
245

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
9
10
11
133 unmodified lines

145
146
147
149
150
151
152
153
154
155
156
148
149
150

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
9
10
11
173 unmodified lines

185
186
187
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

395 unmodified lines

396
397
398
399
400
401
402
403
404
405
406
399
400
401

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
11
12
13
255 unmodified lines

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

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
19
20
21
261 unmodified lines

283
284
285
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
282
283
284

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
496
497
498
3 unmodified lines

502
503
504
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
}

//nolint:unparam // error is always nil in this mock; satisfies the ResumeLauncher interface.
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