Skip to content

Commit b0e5b2c

Browse files
authored
Merge branch 'main' into tokens-labs
2 parents 596f354 + 23e3a80 commit b0e5b2c

48 files changed

Lines changed: 6905 additions & 712 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
name: E2E Checkpoint Store
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
agent:
7+
description: "Agent to run"
8+
required: true
9+
default: "vogon"
10+
type: choice
11+
options:
12+
- vogon
13+
- claude-code
14+
- opencode
15+
- gemini-cli
16+
- factoryai-droid
17+
- cursor-cli
18+
- copilot-cli
19+
- roger-roger
20+
- codex
21+
checkpoint_store:
22+
description: "Checkpoint storage backend to run the suite against"
23+
required: true
24+
default: "git-refs"
25+
type: choice
26+
options:
27+
- git-branch
28+
- git-refs
29+
30+
jobs:
31+
e2e-checkpoint-store:
32+
if: inputs.agent != 'copilot-cli'
33+
runs-on: ubuntu-latest
34+
timeout-minutes: 40
35+
permissions:
36+
actions: read
37+
contents: read
38+
39+
steps:
40+
- name: Checkout repository
41+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
42+
with:
43+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
44+
45+
- name: Install system dependencies
46+
run: |
47+
sudo apt-get update
48+
sudo apt-get install -y gnome-keyring tmux
49+
echo 'somecredstorepass' | gnome-keyring-daemon --unlock
50+
51+
- name: Setup mise
52+
uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4
53+
54+
- name: Build entire CLI
55+
run: go build -o /usr/local/bin/entire ./cmd/entire
56+
57+
- name: Build vogon binary
58+
if: inputs.agent == 'vogon'
59+
run: go build -o /usr/local/bin/vogon ./e2e/vogon
60+
61+
- name: Install agent CLI
62+
if: inputs.agent != 'vogon'
63+
run: |
64+
case "${{ inputs.agent }}" in
65+
claude-code) curl -fsSL https://claude.ai/install.sh | bash ;;
66+
opencode) curl -fsSL https://opencode.ai/install | bash ;;
67+
gemini-cli) npm install -g @google/gemini-cli ;;
68+
codex) npm install -g @openai/codex ;;
69+
cursor-cli) curl https://cursor.com/install -fsS | bash ;;
70+
factoryai-droid) curl -fsSL https://app.factory.ai/cli | sh ;;
71+
copilot-cli) npm install -g @github/copilot ;;
72+
roger-roger) ;; # installed by mise (see mise.toml)
73+
esac
74+
echo "$HOME/.local/bin" >> $GITHUB_PATH
75+
76+
- name: Verify roger-roger agent
77+
if: inputs.agent == 'roger-roger'
78+
run: |
79+
set -euo pipefail
80+
echo "Verifying roger-roger binaries on PATH..."
81+
command -v roger-roger
82+
command -v entire-agent-roger-roger
83+
84+
- name: Bootstrap agent
85+
if: inputs.agent != 'roger-roger' && inputs.agent != 'vogon'
86+
env:
87+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
88+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
89+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
90+
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
91+
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
92+
run: go run ./e2e/bootstrap ${{ inputs.agent }}
93+
94+
- name: E2E Checkpoint Store
95+
env:
96+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
97+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
98+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
99+
E2E_CODEX_MODEL: ${{ inputs.agent == 'codex' && 'gpt-5.1-codex-mini' || '' }}
100+
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
101+
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
102+
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts
103+
E2E_ENTIRE_BIN: /usr/local/bin/entire
104+
E2E_CHECKPOINT_STORE: ${{ inputs.checkpoint_store }}
105+
run: |
106+
mkdir -p "$E2E_ARTIFACT_DIR"
107+
mise run test:e2e --agent ${{ inputs.agent }}
108+
109+
- name: Upload artifacts
110+
if: always()
111+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
112+
with:
113+
name: e2e-checkpoint-store-${{ inputs.agent }}-${{ inputs.checkpoint_store }}
114+
path: e2e-artifacts/
115+
retention-days: 7
116+
117+
e2e-checkpoint-store-copilot:
118+
if: inputs.agent == 'copilot-cli'
119+
runs-on: ubuntu-latest
120+
timeout-minutes: 40
121+
permissions:
122+
actions: read
123+
contents: read
124+
copilot-requests: write
125+
126+
steps:
127+
- name: Checkout repository
128+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
129+
with:
130+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
131+
132+
- name: Install system dependencies
133+
run: |
134+
sudo apt-get update
135+
sudo apt-get install -y gnome-keyring tmux
136+
echo 'somecredstorepass' | gnome-keyring-daemon --unlock
137+
138+
- name: Setup mise
139+
uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4
140+
141+
- name: Build entire CLI
142+
run: go build -o /usr/local/bin/entire ./cmd/entire
143+
144+
- name: Install agent CLI
145+
run: |
146+
npm install -g @github/copilot
147+
echo "$HOME/.local/bin" >> $GITHUB_PATH
148+
149+
- name: Bootstrap agent
150+
env:
151+
COPILOT_GITHUB_TOKEN: ${{ github.token }}
152+
run: go run ./e2e/bootstrap ${{ inputs.agent }}
153+
154+
- name: E2E Checkpoint Store
155+
env:
156+
COPILOT_GITHUB_TOKEN: ${{ github.token }}
157+
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts
158+
E2E_ENTIRE_BIN: /usr/local/bin/entire
159+
E2E_CHECKPOINT_STORE: ${{ inputs.checkpoint_store }}
160+
run: |
161+
mkdir -p "$E2E_ARTIFACT_DIR"
162+
mise run test:e2e --agent ${{ inputs.agent }}
163+
164+
- name: Upload artifacts
165+
if: always()
166+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
167+
with:
168+
name: e2e-checkpoint-store-${{ inputs.agent }}-${{ inputs.checkpoint_store }}
169+
path: e2e-artifacts/
170+
retention-days: 7

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
fi
9595
9696
- name: Run GoReleaser
97-
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
97+
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
9898
with:
9999
distribution: goreleaser-pro
100100
version: latest

cmd/entire/cli/agent/foreground.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
)
9+
10+
// NewForegroundCommand builds an exec.Cmd wired to the caller's terminal.
11+
// Agent launchers use this for commands the user should interact with directly.
12+
func NewForegroundCommand(_ context.Context, binary string, args ...string) (*exec.Cmd, error) {
13+
bin, err := exec.LookPath(binary)
14+
if err != nil {
15+
return nil, fmt.Errorf("%s binary not on PATH: %w", binary, err)
16+
}
17+
// Foreground agents are interactive terminal processes that handle SIGINT
18+
// themselves. Binding them to the root command context would let
19+
// exec.CommandContext SIGKILL them on Ctrl+C before they restore terminal
20+
// state or persist session data.
21+
cmd := exec.CommandContext(context.Background(), bin, args...)
22+
cmd.Stdin = os.Stdin
23+
cmd.Stdout = os.Stdout
24+
cmd.Stderr = os.Stderr
25+
cmd.Env = os.Environ()
26+
return cmd, nil
27+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
)
8+
9+
func TestNewForegroundCommandDoesNotBindToCallerCancellation(t *testing.T) {
10+
t.Parallel()
11+
12+
ctx, cancel := context.WithCancel(context.Background())
13+
exe, err := os.Executable()
14+
if err != nil {
15+
t.Fatalf("os.Executable() error = %v", err)
16+
}
17+
cmd, err := NewForegroundCommand(ctx, exe, "-test.run=TestForegroundCommandHelperProcess", "--")
18+
if err != nil {
19+
t.Fatalf("NewForegroundCommand() error = %v", err)
20+
}
21+
cmd.Env = append(cmd.Env, "ENTIRE_FOREGROUND_HELPER_PROCESS=1")
22+
23+
cancel()
24+
25+
if err := cmd.Run(); err != nil {
26+
t.Fatalf("foreground command should ignore caller cancellation and let the child run: %v", err)
27+
}
28+
}
29+
30+
func TestForegroundCommandHelperProcess(_ *testing.T) {
31+
if os.Getenv("ENTIRE_FOREGROUND_HELPER_PROCESS") != "1" {
32+
return
33+
}
34+
os.Exit(0)
35+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os/exec"
7+
"strings"
8+
9+
"github.com/entireio/cli/cmd/entire/cli/agent/types"
10+
"github.com/entireio/cli/cmd/entire/cli/validation"
11+
)
12+
13+
// ForegroundCommandSpec describes a command Entire can launch in the caller's
14+
// terminal without going through a shell.
15+
type ForegroundCommandSpec struct {
16+
Binary string
17+
Args []string
18+
}
19+
20+
// ResumeCommandSpecFor returns the foreground command shape for agents whose
21+
// resume command is safe for Entire to launch directly. Agents not listed here
22+
// still expose FormatResumeCommand for print-only resume instructions.
23+
func ResumeCommandSpecFor(name types.AgentName, sessionID string) (ForegroundCommandSpec, bool) {
24+
sessionID = strings.TrimSpace(sessionID)
25+
switch name {
26+
case AgentNameClaudeCode:
27+
if !isLaunchableResumeSessionID(sessionID) {
28+
return ForegroundCommandSpec{}, false
29+
}
30+
return ForegroundCommandSpec{Binary: "claude", Args: []string{"-r", sessionID}}, true
31+
case AgentNameCodex:
32+
if !isLaunchableResumeSessionID(sessionID) {
33+
return ForegroundCommandSpec{}, false
34+
}
35+
return ForegroundCommandSpec{Binary: "codex", Args: []string{"resume", sessionID}}, true
36+
case AgentNameCopilotCLI:
37+
if !isLaunchableResumeSessionID(sessionID) {
38+
return ForegroundCommandSpec{}, false
39+
}
40+
return ForegroundCommandSpec{Binary: "copilot", Args: []string{"--resume", sessionID}}, true
41+
case AgentNameFactoryAIDroid:
42+
if !isLaunchableResumeSessionID(sessionID) {
43+
return ForegroundCommandSpec{}, false
44+
}
45+
return ForegroundCommandSpec{Binary: "droid", Args: []string{"--session-id", sessionID}}, true
46+
case AgentNameGemini:
47+
if !isLaunchableResumeSessionID(sessionID) {
48+
return ForegroundCommandSpec{}, false
49+
}
50+
return ForegroundCommandSpec{Binary: "gemini", Args: []string{"--resume", sessionID}}, true
51+
case AgentNameOpenCode:
52+
if sessionID == "" {
53+
return ForegroundCommandSpec{Binary: "opencode"}, true
54+
}
55+
if !isLaunchableResumeSessionID(sessionID) {
56+
return ForegroundCommandSpec{}, false
57+
}
58+
return ForegroundCommandSpec{Binary: "opencode", Args: []string{"-s", sessionID}}, true
59+
case AgentNamePi:
60+
if sessionID == "" {
61+
return ForegroundCommandSpec{Binary: "pi", Args: []string{"--continue"}}, true
62+
}
63+
if !isLaunchableResumeSessionID(sessionID) {
64+
return ForegroundCommandSpec{}, false
65+
}
66+
return ForegroundCommandSpec{Binary: "pi", Args: []string{"--session", sessionID}}, true
67+
default:
68+
return ForegroundCommandSpec{}, false
69+
}
70+
}
71+
72+
func isLaunchableResumeSessionID(sessionID string) bool {
73+
return sessionID != "" && validation.ValidateSessionID(sessionID) == nil
74+
}
75+
76+
// NewResumeForegroundCommand builds a foreground command for resuming a session,
77+
// when the agent has a launchable resume command. ok=false means callers should
78+
// print FormatResumeCommand for the user instead.
79+
func NewResumeForegroundCommand(ctx context.Context, name types.AgentName, sessionID string) (*exec.Cmd, bool, error) {
80+
spec, ok := ResumeCommandSpecFor(name, sessionID)
81+
if !ok {
82+
return nil, false, nil
83+
}
84+
cmd, err := NewForegroundCommand(ctx, spec.Binary, spec.Args...)
85+
if err != nil {
86+
return nil, true, fmt.Errorf("build %s resume command: %w", spec.Binary, err)
87+
}
88+
return cmd, true, nil
89+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package agent_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/entireio/cli/cmd/entire/cli/agent"
8+
_ "github.com/entireio/cli/cmd/entire/cli/agent/claudecode"
9+
_ "github.com/entireio/cli/cmd/entire/cli/agent/codex"
10+
_ "github.com/entireio/cli/cmd/entire/cli/agent/copilotcli"
11+
_ "github.com/entireio/cli/cmd/entire/cli/agent/factoryaidroid"
12+
_ "github.com/entireio/cli/cmd/entire/cli/agent/geminicli"
13+
_ "github.com/entireio/cli/cmd/entire/cli/agent/opencode"
14+
_ "github.com/entireio/cli/cmd/entire/cli/agent/pi"
15+
"github.com/entireio/cli/cmd/entire/cli/agent/types"
16+
)
17+
18+
func TestResumeCommandSpecMatchesFormattedResumeCommand(t *testing.T) {
19+
t.Parallel()
20+
21+
sessionID := "session-123"
22+
for _, name := range []types.AgentName{
23+
agent.AgentNameClaudeCode,
24+
agent.AgentNameCodex,
25+
agent.AgentNameCopilotCLI,
26+
agent.AgentNameFactoryAIDroid,
27+
agent.AgentNameGemini,
28+
agent.AgentNameOpenCode,
29+
agent.AgentNamePi,
30+
} {
31+
t.Run(string(name), func(t *testing.T) {
32+
t.Parallel()
33+
34+
ag, err := agent.Get(name)
35+
if err != nil {
36+
t.Fatalf("Get(%s): %v", name, err)
37+
}
38+
spec, ok := agent.ResumeCommandSpecFor(name, sessionID)
39+
if !ok {
40+
t.Fatalf("ResumeCommandSpecFor(%s) ok = false, want true", name)
41+
}
42+
got := strings.Join(append([]string{spec.Binary}, spec.Args...), " ")
43+
if want := ag.FormatResumeCommand(sessionID); got != want {
44+
t.Fatalf("resume command spec = %q, FormatResumeCommand = %q", got, want)
45+
}
46+
})
47+
}
48+
}

0 commit comments

Comments
 (0)