Skip to content

Commit c96cf02

Browse files
committed
feat: port remaining features from entire CLI
Port 86 files from the entire CLI codebase, adding: Features: - investigate command (full feature, 20+ files) - agent spawn orchestration (spawn/, agentlaunch/) - git alternates support (gitrepo/) - checkpoint configloader + remote command cancellation - auth base URL splitting (AuthBaseURL, IsSplitHost) - TUI display utilities, UI forms - provenance environment tracking - e2e tests (alternates, opencode) New types/fields: - settings.InvestigateConfig + Investigate field - checkpoint.CheckpointSummary.HasInvestigation - checkpoint.CommittedMetadata.ReviewPrompt, InvestigateRunID - api.AuthBaseURL(), IsSplitHost(), NormalizeOriginURL() - versioninfo.Load(), resolve() - strategy.checkpointPushBudget Deps: - +github.com/entireio/auth-go v0.3.4 - go-billy/v6 upgraded All content transformed: entireio/cli -> GrayCodeAI/trace, entire -> trace, Entire -> Trace, .entire -> .trace, ENTIRE_* env vars -> TRACE_*. Build, vet, and go mod tidy all pass clean. Zero remaining non-English 'entire' references.
1 parent 92077d2 commit c96cf02

111 files changed

Lines changed: 15634 additions & 123 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.opencode/plugins/trace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const TracePlugin: Plugin = async ({ directory }) => {
2525
}
2626

2727
/**
28-
* Pipe JSON payload to an entire hooks command (async).
28+
* Pipe JSON payload to an trace hooks command (async).
2929
* Errors are logged but never thrown — plugin failures must not crash OpenCode.
3030
*/
3131
async function callHook(hookName: string, payload: Record<string, unknown>) {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package claudecode
2+
3+
import (
4+
"context"
5+
"os/exec"
6+
7+
"github.com/GrayCodeAI/trace/cmd/trace/cli/agent"
8+
"github.com/GrayCodeAI/trace/cmd/trace/cli/agent/spawn"
9+
)
10+
11+
// claudeCodeSpawner produces argv: claude -p <prompt>; no stdin.
12+
type claudeCodeSpawner struct{}
13+
14+
// NewSpawner returns a Spawner for claude-code's non-interactive review/investigate mode.
15+
func NewSpawner() spawn.Spawner { return claudeCodeSpawner{} }
16+
17+
func (claudeCodeSpawner) Name() string { return string(agent.AgentNameClaudeCode) }
18+
19+
func (claudeCodeSpawner) BuildCmd(ctx context.Context, env []string, prompt string) *exec.Cmd {
20+
// --permission-mode bypassPermissions auto-accepts every tool call.
21+
// `-p` (print) mode has no UI to answer permission prompts, so the
22+
// default mode silently denies anything not pre-approved — including
23+
// Bash (so `git`, `grep`, `ls` would be blocked). The prompt forbids
24+
// destructive commands; the flag is a no-op for the review path.
25+
cmd := exec.CommandContext(ctx, "claude", "-p", "--permission-mode", "bypassPermissions", prompt)
26+
cmd.Env = env
27+
return cmd
28+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package claudecode
2+
3+
import (
4+
"context"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
// TestClaudeCodeSpawner_Name asserts the spawner reports the stable
10+
// registry name used by both review and investigate callers.
11+
func TestClaudeCodeSpawner_Name(t *testing.T) {
12+
t.Parallel()
13+
if got := NewSpawner().Name(); got != "claude-code" {
14+
t.Errorf("Name() = %q, want %q", got, "claude-code")
15+
}
16+
}
17+
18+
// TestClaudeCodeSpawner_Argv pins the argv contract:
19+
//
20+
// claude -p --permission-mode bypassPermissions <prompt>
21+
//
22+
// The prompt is the last positional. --permission-mode bypassPermissions is
23+
// required so file writes succeed in non-interactive mode (see
24+
// spawner.go); stdin is unused.
25+
func TestClaudeCodeSpawner_Argv(t *testing.T) {
26+
t.Parallel()
27+
env := []string{"FOO=bar", "BAZ=qux"}
28+
cmd := NewSpawner().BuildCmd(context.Background(), env, "the-prompt")
29+
30+
wantArgs := []string{"claude", "-p", "--permission-mode", "bypassPermissions", "the-prompt"}
31+
if !reflect.DeepEqual(cmd.Args, wantArgs) {
32+
t.Errorf("Args = %v, want %v", cmd.Args, wantArgs)
33+
}
34+
35+
if !reflect.DeepEqual(cmd.Env, env) {
36+
t.Errorf("Env = %v, want %v", cmd.Env, env)
37+
}
38+
39+
if cmd.Stdin != nil {
40+
t.Errorf("Stdin = %v, want nil (claude uses argv, not stdin)", cmd.Stdin)
41+
}
42+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{"type":"system","subtype":"init","cwd":"/redacted/worktree","session_id":"f6da5c2c-aaaa-aaaa-aaaa-aaaaaaaaaaaa","model":"claude-opus-4-7[1m]","permissionMode":"default","output_style":"default","apiKeySource":"none","fast_mode_state":"off","uuid":"c5a08ce6-3a63-4db2-b456-ab2c5a8e289d"}
2+
{"type":"assistant","message":{"model":"claude-opus-4-7","id":"msg_01G5oM1j98ym4vJKwdys6MUm","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"REDACTED"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":32750,"cache_read_input_tokens":22082,"output_tokens":0}},"parent_tool_use_id":null,"session_id":"f6da5c2c-aaaa-aaaa-aaaa-aaaaaaaaaaaa","uuid":"2eedb798-4bb3-4e2a-8620-a699aa00bd02"}
3+
{"type":"assistant","message":{"model":"claude-opus-4-7","id":"msg_01G5oM1j98ym4vJKwdys6MUm","type":"message","role":"assistant","content":[{"type":"text","text":"Cats are among the most enigmatic companions humans have welcomed into their homes. Domesticated roughly 9,000 years ago in the Fertile Crescent, they originally earned their place hunting rodents in grain stores."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":32750,"cache_read_input_tokens":22082,"output_tokens":0}},"parent_tool_use_id":null,"session_id":"f6da5c2c-aaaa-aaaa-aaaa-aaaaaaaaaaaa","uuid":"98509118-f965-4e8b-9195-5dbe1c93d937"}
4+
{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1778614200,"rateLimitType":"five_hour","overageStatus":"allowed","overageResetsAt":1780272000,"isUsingOverage":false},"uuid":"c771b482-c0a9-4115-8e9c-9eef2dccabe2","session_id":"f6da5c2c-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}
5+
{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":16014,"duration_api_ms":11681,"num_turns":1,"result":"Cats are among the most enigmatic companions humans have welcomed into their homes.","stop_reason":"end_turn","session_id":"f6da5c2c-aaaa-aaaa-aaaa-aaaaaaaaaaaa","total_cost_usd":0.23315850000000002,"usage":{"input_tokens":6,"cache_creation_input_tokens":32750,"cache_read_input_tokens":22082,"output_tokens":696,"service_tier":"standard"},"terminal_reason":"completed","fast_mode_state":"off","uuid":"ddcee86e-c4ce-47be-905a-263c2723e9cf"}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package codex
2+
3+
import (
4+
"context"
5+
"os/exec"
6+
"strings"
7+
8+
"github.com/GrayCodeAI/trace/cmd/trace/cli/agent"
9+
"github.com/GrayCodeAI/trace/cmd/trace/cli/agent/spawn"
10+
)
11+
12+
// codexSpawner produces argv:
13+
//
14+
// codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox -
15+
//
16+
// Prompt is piped on stdin. The "dangerously-bypass" flag is codex's
17+
// documented way to run autonomously without sandbox + approval gates.
18+
// Less aggressive options (-s workspace-write, --add-dir) are NOT
19+
// sufficient for `trace investigate`: codex's workspace-write policy
20+
// excludes `.git/` regardless of --add-dir, so the agent could not
21+
// write to <git-common-dir>/entire-investigations/<run-id>/
22+
// (findings.md / state.json) even when that path was added. The user
23+
// explicitly invoked the agent; the prompt forbids destructive commands.
24+
type codexSpawner struct{}
25+
26+
// NewSpawner returns a Spawner for codex's non-interactive review/investigate mode.
27+
func NewSpawner() spawn.Spawner { return codexSpawner{} }
28+
29+
func (codexSpawner) Name() string { return string(agent.AgentNameCodex) }
30+
31+
func (codexSpawner) BuildCmd(ctx context.Context, env []string, prompt string) *exec.Cmd {
32+
cmd := exec.CommandContext(ctx, string(agent.AgentNameCodex),
33+
codexExecCommand,
34+
"--skip-git-repo-check",
35+
"--dangerously-bypass-approvals-and-sandbox",
36+
"-",
37+
)
38+
cmd.Stdin = strings.NewReader(prompt)
39+
cmd.Env = env
40+
return cmd
41+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package codex
2+
3+
import (
4+
"context"
5+
"io"
6+
"reflect"
7+
"testing"
8+
)
9+
10+
// TestCodexSpawner_Name asserts the spawner reports the stable registry name.
11+
func TestCodexSpawner_Name(t *testing.T) {
12+
t.Parallel()
13+
if got := NewSpawner().Name(); got != wantCodexAgentName {
14+
t.Errorf("Name() = %q, want %q", got, wantCodexAgentName)
15+
}
16+
}
17+
18+
// TestCodexSpawner_Argv pins the argv + stdin contract:
19+
//
20+
// codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox -
21+
//
22+
// Prompt is piped on stdin. The bypass-approvals-and-sandbox flag is
23+
// codex's documented way to run autonomously: less aggressive options
24+
// (-s workspace-write, --add-dir) are not sufficient because codex's
25+
// workspace-write policy excludes anything under `.git/` regardless of
26+
// --add-dir, which blocks investigate's per-run dir at
27+
// <git-common-dir>/entire-investigations/<run-id>/.
28+
func TestCodexSpawner_Argv(t *testing.T) {
29+
t.Parallel()
30+
env := []string{"FOO=bar", "BAZ=qux"}
31+
cmd := NewSpawner().BuildCmd(context.Background(), env, "the-prompt")
32+
33+
wantArgs := []string{
34+
wantCodexAgentName, "exec",
35+
"--skip-git-repo-check",
36+
"--dangerously-bypass-approvals-and-sandbox",
37+
"-",
38+
}
39+
if !reflect.DeepEqual(cmd.Args, wantArgs) {
40+
t.Errorf("Args = %v, want %v", cmd.Args, wantArgs)
41+
}
42+
43+
if !reflect.DeepEqual(cmd.Env, env) {
44+
t.Errorf("Env = %v, want %v", cmd.Env, env)
45+
}
46+
47+
if cmd.Stdin == nil {
48+
t.Fatal("Stdin = nil, want a reader carrying the prompt")
49+
}
50+
got, err := io.ReadAll(cmd.Stdin)
51+
if err != nil {
52+
t.Fatalf("read stdin: %v", err)
53+
}
54+
if string(got) != "the-prompt" {
55+
t.Errorf("stdin = %q, want %q", string(got), "the-prompt")
56+
}
57+
}
58+
59+
// TestCodexSpawner_Argv_StableUnderInvestigateEnv pins the contract
60+
// that the argv does NOT change based on env vars. (A previous
61+
// implementation appended --add-dir from ENTIRE_INVESTIGATE_FINDINGS_DOC;
62+
// that approach didn't actually unblock writes under .git/, so we
63+
// dropped it. This test pins the regression.)
64+
func TestCodexSpawner_Argv_StableUnderInvestigateEnv(t *testing.T) {
65+
t.Parallel()
66+
env := []string{
67+
"FOO=bar",
68+
"ENTIRE_INVESTIGATE_FINDINGS_DOC=/repo/.git/entire-investigations/abcdef012345/findings.md",
69+
}
70+
cmd := NewSpawner().BuildCmd(context.Background(), env, "prompt")
71+
72+
wantArgs := []string{
73+
wantCodexAgentName, "exec",
74+
"--skip-git-repo-check",
75+
"--dangerously-bypass-approvals-and-sandbox",
76+
"-",
77+
}
78+
if !reflect.DeepEqual(cmd.Args, wantArgs) {
79+
t.Errorf("Args = %v, want %v", cmd.Args, wantArgs)
80+
}
81+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{"type":"thread.started","thread_id":"019e1cad-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}
2+
{"type":"turn.started"}
3+
{"type":"item.started","item":{"id":"item_0","type":"command_execution","command":"/bin/zsh -lc \"sed -n '1,200p' /Users/redacted/SKILL.md\"","aggregated_output":"","exit_code":null,"status":"in_progress"}}
4+
{"type":"item.completed","item":{"id":"item_0","type":"command_execution","command":"/bin/zsh -lc \"sed -n '1,200p' /Users/redacted/SKILL.md\"","aggregated_output":"---\nname: using-superpowers\ndescription: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions\n---\n\nfile content trimmed for fixture brevity\n","exit_code":0,"status":"completed"}}
5+
{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"Hi"}}
6+
{"type":"turn.completed","usage":{"input_tokens":50787,"cached_input_tokens":29952,"output_tokens":354,"reasoning_output_tokens":251}}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package geminicli
2+
3+
import (
4+
"context"
5+
"os/exec"
6+
"strings"
7+
8+
"github.com/GrayCodeAI/trace/cmd/trace/cli/agent/spawn"
9+
)
10+
11+
// geminiSpawner produces argv: gemini -p " "; prompt via stdin.
12+
// The " " argv placeholder triggers headless mode; the prompt goes via stdin
13+
// because gemini's -p flag appends to stdin content.
14+
type geminiSpawner struct{}
15+
16+
// NewSpawner returns a Spawner for gemini-cli's non-interactive review/investigate mode.
17+
func NewSpawner() spawn.Spawner { return geminiSpawner{} }
18+
19+
func (geminiSpawner) Name() string { return "gemini-cli" }
20+
21+
func (geminiSpawner) BuildCmd(ctx context.Context, env []string, prompt string) *exec.Cmd {
22+
cmd := exec.CommandContext(ctx, "gemini", "-p", " ")
23+
cmd.Stdin = strings.NewReader(prompt)
24+
cmd.Env = env
25+
return cmd
26+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package geminicli
2+
3+
import (
4+
"context"
5+
"io"
6+
"reflect"
7+
"testing"
8+
)
9+
10+
// TestGeminiCLISpawner_Name asserts the spawner reports the stable registry name.
11+
func TestGeminiCLISpawner_Name(t *testing.T) {
12+
t.Parallel()
13+
if got := NewSpawner().Name(); got != "gemini-cli" {
14+
t.Errorf("Name() = %q, want %q", got, "gemini-cli")
15+
}
16+
}
17+
18+
// TestGeminiCLISpawner_Argv pins the argv + stdin contract:
19+
// gemini -p " " (space placeholder triggers headless mode), prompt via stdin.
20+
func TestGeminiCLISpawner_Argv(t *testing.T) {
21+
t.Parallel()
22+
env := []string{"FOO=bar", "BAZ=qux"}
23+
cmd := NewSpawner().BuildCmd(context.Background(), env, "the-prompt")
24+
25+
wantArgs := []string{"gemini", "-p", " "}
26+
if !reflect.DeepEqual(cmd.Args, wantArgs) {
27+
t.Errorf("Args = %v, want %v", cmd.Args, wantArgs)
28+
}
29+
30+
if !reflect.DeepEqual(cmd.Env, env) {
31+
t.Errorf("Env = %v, want %v", cmd.Env, env)
32+
}
33+
34+
if cmd.Stdin == nil {
35+
t.Fatal("Stdin = nil, want a reader carrying the prompt")
36+
}
37+
got, err := io.ReadAll(cmd.Stdin)
38+
if err != nil {
39+
t.Fatalf("read stdin: %v", err)
40+
}
41+
if string(got) != "the-prompt" {
42+
t.Errorf("stdin = %q, want %q", string(got), "the-prompt")
43+
}
44+
}

cmd/trace/cli/agent/pi/hooks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const (
3131
// content (and so it survives the TRACE_CMD placeholder substitution).
3232
entireMarker = "Auto-generated by `trace enable --agent pi`"
3333

34-
// entireCmdPlaceholder is replaced at install time with either `entire`
34+
// entireCmdPlaceholder is replaced at install time with either `trace`
3535
// (production) or a `go run …` path (local-dev).
3636
entireCmdPlaceholder = "__TRACE_CMD__"
3737
)

0 commit comments

Comments
 (0)