# e2e: share the run-capture/deadline-wrap block across agent runners

`e2659f1`·

Soph·2w ago·4 files·+26 added/-41 removed

codex, copilot-cli, and gemini repeated the exit-code extraction and
prompt-deadline wrapping after cmd.Run(); extract runCapture into
agents/agent.go. The other runners don't take a prompt context and never
wrapped the deadline, so they keep their direct handling. Verified with
the Vogon canary suite (59/59).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

## Sessions

ab0d1ee8ae62View transcript

[?\
/goal simplify this repo, make sure to de-slop it continue until you are happy, make sure to backfill tests and validate any significant change. The output of this should be a PR with green CI, reviewClaude Code·Fable 5·1 step](/content/gh/entireio/cli/session/1c940797-e8a8-42af-af74-e4b69e3ff567#timeline-ab0d1ee8ae62/index.html)

## Changes

4

- e2e/agents

- Magent.go+23

- Mcodex.go+1/-13

- Mcopilot-cli.go+1/-13

- Mgemini.go+1/-15

```
1 unmodified line

2
3
4
5
6
7
8
9
10
11
96 unmodified lines

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

1 unmodified line

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"
96 unmodified lines

return registry
}

// runCapture runs cmd and returns its exit code (-1 when the process
// didn't produce one) and the run error. When promptCtx hit its deadline
// the error wraps context.DeadlineExceeded so IsTransientError can detect
// it — cmd.Run reports "signal: killed" in that case, not the context error.
func runCapture(cmd *exec.Cmd, promptCtx context.Context) (int, error) {
	err := cmd.Run()
	if err == nil {
		return 0, nil
	}

exitCode := -1
	exitErr := &exec.ExitError{}
	if errors.As(err, &exitErr) {
		exitCode = exitErr.ExitCode()
	}
	if errors.Is(promptCtx.Err(), context.DeadlineExceeded) {
		err = fmt.Errorf("%w: %w", err, context.DeadlineExceeded)
	}
	return exitCode, err
}

// filterEnv returns env with entries matching any of the given variable names
// removed. Used to strip test-only overrides (e.g. ENTIRE_TEST_TTY) from agent
// processes so they exercise real detection paths.
```

Me2e/agents/agent.go+23

```
129 unmodified lines

130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
133
134
135
136

129 unmodified lines

cmd.Stdout = &stdout
	cmd.Stderr = &stderr

err = cmd.Run()
	exitCode := 0
	if err != nil {
		exitErr := &exec.ExitError{}
		if errors.As(err, &exitErr) {
			exitCode = exitErr.ExitCode()
		} else {
			exitCode = -1
		}
		if promptCtx.Err() == context.DeadlineExceeded {
			err = fmt.Errorf("%w: %w", err, context.DeadlineExceeded)
		}
	}
	exitCode, err := runCapture(cmd, promptCtx)

return Output{
		Command:  c.Binary() + " " + strings.Join(args[:len(args)-1], " ") + " " + fmt.Sprintf("%q", prompt),
```

Me2e/agents/codex.go+1/-13

```
86 unmodified lines

87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
90
91
92
93

86 unmodified lines

cmd.Stdout = &stdout
	cmd.Stderr = &stderr

err := cmd.Run()
	exitCode := 0
	if err != nil {
		exitErr := &exec.ExitError{}
		if errors.As(err, &exitErr) {
		exitCode = exitErr.ExitCode()
		} else {
		exitCode = -1
		}
		if promptCtx.Err() == context.DeadlineExceeded {
			err = fmt.Errorf("%w: %w", err, context.DeadlineExceeded)
		}
	}
	exitCode, err := runCapture(cmd, promptCtx)

out := Output{
		Command:  c.Binary() + " " + strings.Join(displayArgs, " "),
```

Me2e/agents/copilot-cli.go+1/-13

```
119 unmodified lines

120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
123
124
125
126

119 unmodified lines

cmd.Stdout = &stdout
	cmd.Stderr = &stderr

err := cmd.Run()
	exitCode := 0
	if err != nil {
		exitErr := &exec.ExitError{}
		if errors.As(err, &exitErr) {
		exitCode = exitErr.ExitCode()
		} else {
		exitCode = -1
		}
		// Wrap the prompt-level deadline so IsTransientError can detect it.
		// cmd.Run() returns "signal: killed", not the context error.
		if promptCtx.Err() == context.DeadlineExceeded {
			err = fmt.Errorf("%w: %w", err, context.DeadlineExceeded)
		}
	}
	exitCode, err := runCapture(cmd, promptCtx)

// gemini-cli can abort a turn server-side (empty/malformed model response)
	// yet still exit 0 with empty stdout. Surface it as an error so the
```

Me2e/agents/gemini.go+1/-15
