Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
a5a6f5e
feat(investigate): add entire investigate command (labs)
alishakawaguchi May 9, 2026
718aebb
fix(investigate): address code review findings
alishakawaguchi May 9, 2026
02704e4
fix(investigate): address adversarial review findings
alishakawaguchi May 9, 2026
44cca8a
feat(investigate): replace per-turn logs with live TUI dashboard
alishakawaguchi May 12, 2026
38cd769
Merge branch 'main' into entire-labs-investigate
alishakawaguchi May 12, 2026
a0e31c1
fix(investigate): store config in .entire/settings.local.json
alishakawaguchi May 12, 2026
1b31a6c
investigate: update first-run banner to mention local settings
alishakawaguchi May 12, 2026
b3b6868
investigate: add one-time migration from .entire/settings.json
alishakawaguchi May 12, 2026
b3ca7ac
investigate: wire settings migration prompt into NewCommand
alishakawaguchi May 12, 2026
4f4c57d
cli: pin investigate wrapper contract — no HasReview piggyback
alishakawaguchi May 12, 2026
f5b7aa2
investigate: soft-warn when HEAD already has an investigation
alishakawaguchi May 12, 2026
c1612ad
investigate: add spawn-time PickInvestigateAgents
alishakawaguchi May 12, 2026
d5555c5
investigate: spawn-time multipicker + per-run prompt
alishakawaguchi May 12, 2026
693a9c5
investigate: track per-turn timeline entries on the TUI model
alishakawaguchi May 12, 2026
6a51b14
investigate: parse and forward per-turn findings preview
alishakawaguchi May 12, 2026
c6afd3d
investigate: add detailView pure-function renderer
alishakawaguchi May 12, 2026
01716f6
investigate: add Ctrl+O drill-in detail view with mouse-wheel
alishakawaguchi May 12, 2026
6e41d2c
investigate: address final review findings
alishakawaguchi May 12, 2026
1bb9b6d
investigate: route logs to file so the TUI doesn't get corrupted
alishakawaguchi May 12, 2026
98df345
claude-code: pass --permission-mode acceptEdits so writes succeed
alishakawaguchi May 12, 2026
f2c1076
tests: capture last argv positional in review claude stub
alishakawaguchi May 12, 2026
0e595b9
investigate: restore //nolint:ireturn on buildProgressSink
alishakawaguchi May 12, 2026
1de7ae0
investigate: suppress pre-TUI banner in TTY mode
alishakawaguchi May 12, 2026
1217866
investigate: stop forcing the multipicker on --continue and the
alishakawaguchi May 13, 2026
845b200
investigate: drop per-turn .log files; rely on session transcripts
alishakawaguchi May 13, 2026
91196d0
investigate: per-run doc paths so runs don't share findings/timeline
alishakawaguchi May 13, 2026
f22f430
investigate: drop timeline.md and move per-run dir to git-common-dir
alishakawaguchi May 15, 2026
9c3c1ff
investigate: collaborative findings.md (review/verify/edit, not append)
alishakawaguchi May 15, 2026
4545f27
investigate: capture findings into manifest + auto-clean per-run dir
alishakawaguchi May 15, 2026
6c02b36
investigate: add `show [run-id]` subcommand
alishakawaguchi May 15, 2026
6f0af68
tests: read findings from manifest in issue-link integration test
alishakawaguchi May 15, 2026
6d2c31e
investigate: restore richer prompt + multi-section scaffold (no timel…
alishakawaguchi May 15, 2026
2e7b20e
investigate: add `clean [run-id]` / `clean --all` subcommand
alishakawaguchi May 15, 2026
bf42d67
investigate: scaffold seed-doc / issue-link bootstrap (don't dump raw)
alishakawaguchi May 15, 2026
9774ae6
codex: add `-s workspace-write` + `--add-dir <run-dir>` for investigate
alishakawaguchi May 15, 2026
6c8add5
claude-code: --permission-mode bypassPermissions (not acceptEdits)
alishakawaguchi May 15, 2026
8cbe870
investigate: embed findings body in post-run footer
alishakawaguchi May 15, 2026
aa5ca1d
investigate: default per-agent max-turns to 2 (was 3)
alishakawaguchi May 15, 2026
4776ea4
codex: --dangerously-bypass-approvals-and-sandbox for agent runs
alishakawaguchi May 15, 2026
f98e340
investigate: reframe ## Approach as a concise summary (no per-agent)
alishakawaguchi May 15, 2026
092bf30
investigate: render findings through glamour (mdrender) in footer + show
alishakawaguchi May 15, 2026
18b8ccd
investigate: fix uses manifest.FindingsContent before disk
alishakawaguchi May 15, 2026
3c322de
Merge main into entire-labs-investigate
alishakawaguchi May 18, 2026
0d93411
investigate: restore non-obvious WHY comments on InvestigateConfig fi…
alishakawaguchi May 18, 2026
032f3fc
investigate: reuse existing helpers (jsonutil, checkpoint/id, agent c…
alishakawaguchi May 19, 2026
df4a18e
tuiutil: extract shared TUI text helpers used by review and investigate
alishakawaguchi May 19, 2026
8ab83e7
gitexec: extract shared git-CLI runner used by review and investigate
alishakawaguchi May 19, 2026
b7d3272
uiform: extract shared accessible-form constructor
alishakawaguchi May 19, 2026
037d398
review: gofmt sort gitexec import
alishakawaguchi May 19, 2026
cc72d21
lifecycle: unify adoptReviewEnv and adoptInvestigateEnv via tryAdoptEnv
alishakawaguchi May 19, 2026
9670e76
provenance: own the agent-provenance env contract
alishakawaguchi May 19, 2026
05bb259
investigate: drop --topic flag and investigate attach; rename picker …
alishakawaguchi May 19, 2026
d9370a3
investigate: rename user-facing "Topic" labels to "Prompt" / "Investi…
alishakawaguchi May 19, 2026
59491ff
tuiutil: lift FormatDuration out of review and investigate
alishakawaguchi May 19, 2026
fd91403
investigate: drive PlanChanged from stat tuple, not findings SHA
alishakawaguchi May 19, 2026
d49bd1b
investigate: share run-id resolution between show and clean
alishakawaguchi May 19, 2026
5d9c29d
uiform: extract PromptYN; collapse review and investigate copies
alishakawaguchi May 19, 2026
d4e81a4
investigate: migration uses settings.{LoadProjectRaw,LoadLocalRaw,Sav…
alishakawaguchi May 19, 2026
5d7125b
investigate: drop the settings migration (feature is not live)
alishakawaguchi May 19, 2026
06b6b08
investigate: redact URL userinfo in gh exec errors
alishakawaguchi May 19, 2026
0beef1b
investigate: persist terminal manifest on --continue; confirm untrust…
alishakawaguchi May 19, 2026
c15946f
Merge main into entire-labs-investigate
alishakawaguchi May 19, 2026
f8edb81
investigate + extracted packages: condense comments, drop past-state …
alishakawaguchi May 19, 2026
a17123a
investigate: drop round/turn/prompt fields and unused AttachSession
alishakawaguchi May 20, 2026
185888b
investigate: replace vacuous progress-sink test with compile-time guard
alishakawaguchi May 20, 2026
23e1f08
investigate: address review findings — correctness, security, layering
alishakawaguchi May 20, 2026
64d2f15
investigate: PR review + CI lint fixes
alishakawaguchi May 20, 2026
642edc5
investigate: replace "<captured in manifest>" with actionable view/re…
alishakawaguchi May 20, 2026
115fdf4
Merge main into entire-labs-investigate
alishakawaguchi May 20, 2026
93751ea
Merge branch 'main' into entire-labs-investigate
alishakawaguchi May 26, 2026
1c8daeb
Merge branch 'main' into entire-labs-investigate
alishakawaguchi May 27, 2026
de5d0a8
refactor(cli): split HEAD-checkpoint flag helpers into own file
alishakawaguchi May 27, 2026
c42252f
fix(lint): drop unused ireturn nolint directive on alternates_fs.go
alishakawaguchi May 27, 2026
10e58ee
fix(investigate): harden the --issue-link → bypass-agent path
alishakawaguchi May 27, 2026
3efbbee
test(investigate): pass --allow-untrusted-seed in non-interactive iss…
alishakawaguchi May 27, 2026
dabe501
Merge branch 'main' into entire-labs-investigate
alishakawaguchi May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ linters:
- grpc.DialOption
- github.com/entireio/cli/cmd/entire/cli/summarize.Generator
- github.com/entireio/cli/cmd/entire/cli/agent\..+
- github.com/entireio/cli/cmd/entire/cli/agent/spawn.Spawner
- github.com/entireio/cli/cmd/entire/cli/review/types.Process
- github.com/entireio/cli/cmd/entire/cli/review/types.AgentReviewer
- github.com/entireio/cli/cmd/entire/cli/review.SynthesisProvider
Expand Down Expand Up @@ -161,6 +162,9 @@ linters:
- path: ^cmd/entire/cli/review/tui_model\.go$
linters:
- ireturn
- path: ^cmd/entire/cli/investigate/cmd\.go$
linters:
- ireturn
# configloader.go is a thin os-backed billy.Basic adapter. It must return
# raw os errors unwrapped so go-git's os.IsNotExist() fall-through (for
# absent config files) keeps working; wrapping would break it.
Expand Down
1 change: 1 addition & 0 deletions cmd/entire/cli/agent/architecture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ func discoverAgentPackages(t *testing.T, agentDir string) []string {
"testutil": true, // shared test utilities
"external": true, // external agent adapter, not a self-registering agent
"skilldiscovery": true, // shared capability helper (registries, match), not an agent
"spawn": true, // shared Spawner interface for review/investigate, not an agent
}

entries, err := os.ReadDir(agentDir)
Expand Down
28 changes: 28 additions & 0 deletions cmd/entire/cli/agent/claudecode/spawner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package claudecode

import (
"context"
"os/exec"

"github.com/entireio/cli/cmd/entire/cli/agent"
"github.com/entireio/cli/cmd/entire/cli/agent/spawn"
)

// claudeCodeSpawner produces argv: claude -p <prompt>; no stdin.
type claudeCodeSpawner struct{}

// NewSpawner returns a Spawner for claude-code's non-interactive review/investigate mode.
func NewSpawner() spawn.Spawner { return claudeCodeSpawner{} }

func (claudeCodeSpawner) Name() string { return string(agent.AgentNameClaudeCode) }

func (claudeCodeSpawner) BuildCmd(ctx context.Context, env []string, prompt string) *exec.Cmd {
// --permission-mode bypassPermissions auto-accepts every tool call.
// `-p` (print) mode has no UI to answer permission prompts, so the
// default mode silently denies anything not pre-approved — including
// Bash (so `git`, `grep`, `ls` would be blocked). The prompt forbids
// destructive commands; the flag is a no-op for the review path.
cmd := exec.CommandContext(ctx, "claude", "-p", "--permission-mode", "bypassPermissions", prompt)
cmd.Env = env
return cmd
}
42 changes: 42 additions & 0 deletions cmd/entire/cli/agent/claudecode/spawner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package claudecode

import (
"context"
"reflect"
"testing"
)

// TestClaudeCodeSpawner_Name asserts the spawner reports the stable
// registry name used by both review and investigate callers.
func TestClaudeCodeSpawner_Name(t *testing.T) {
t.Parallel()
if got := NewSpawner().Name(); got != "claude-code" {
t.Errorf("Name() = %q, want %q", got, "claude-code")
}
}

// TestClaudeCodeSpawner_Argv pins the argv contract:
//
// claude -p --permission-mode bypassPermissions <prompt>
//
// The prompt is the last positional. --permission-mode bypassPermissions is
// required so file writes succeed in non-interactive mode (see
// spawner.go); stdin is unused.
func TestClaudeCodeSpawner_Argv(t *testing.T) {
t.Parallel()
env := []string{"FOO=bar", "BAZ=qux"}
cmd := NewSpawner().BuildCmd(context.Background(), env, "the-prompt")

wantArgs := []string{"claude", "-p", "--permission-mode", "bypassPermissions", "the-prompt"}
if !reflect.DeepEqual(cmd.Args, wantArgs) {
t.Errorf("Args = %v, want %v", cmd.Args, wantArgs)
}

if !reflect.DeepEqual(cmd.Env, env) {
t.Errorf("Env = %v, want %v", cmd.Env, env)
}

if cmd.Stdin != nil {
t.Errorf("Stdin = %v, want nil (claude uses argv, not stdin)", cmd.Stdin)
}
}
41 changes: 41 additions & 0 deletions cmd/entire/cli/agent/codex/spawner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package codex

import (
"context"
"os/exec"
"strings"

"github.com/entireio/cli/cmd/entire/cli/agent"
"github.com/entireio/cli/cmd/entire/cli/agent/spawn"
)

// codexSpawner produces argv:
//
// codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox -
//
// Prompt is piped on stdin. The "dangerously-bypass" flag is codex's
// documented way to run autonomously without sandbox + approval gates.
// Less aggressive options (-s workspace-write, --add-dir) are NOT
// sufficient for `entire investigate`: codex's workspace-write policy
// excludes `.git/` regardless of --add-dir, so the agent could not
// write to <git-common-dir>/entire-investigations/<run-id>/
// (findings.md / state.json) even when that path was added. The user
// explicitly invoked the agent; the prompt forbids destructive commands.
type codexSpawner struct{}

// NewSpawner returns a Spawner for codex's non-interactive review/investigate mode.
func NewSpawner() spawn.Spawner { return codexSpawner{} }

func (codexSpawner) Name() string { return string(agent.AgentNameCodex) }

func (codexSpawner) BuildCmd(ctx context.Context, env []string, prompt string) *exec.Cmd {
cmd := exec.CommandContext(ctx, string(agent.AgentNameCodex),
codexExecCommand,
"--skip-git-repo-check",
"--dangerously-bypass-approvals-and-sandbox",
"-",
)
cmd.Stdin = strings.NewReader(prompt)
cmd.Env = env
return cmd
}
81 changes: 81 additions & 0 deletions cmd/entire/cli/agent/codex/spawner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package codex

import (
"context"
"io"
"reflect"
"testing"
)

// TestCodexSpawner_Name asserts the spawner reports the stable registry name.
func TestCodexSpawner_Name(t *testing.T) {
t.Parallel()
if got := NewSpawner().Name(); got != wantCodexAgentName {
t.Errorf("Name() = %q, want %q", got, wantCodexAgentName)
}
}

// TestCodexSpawner_Argv pins the argv + stdin contract:
//
// codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox -
//
// Prompt is piped on stdin. The bypass-approvals-and-sandbox flag is
// codex's documented way to run autonomously: less aggressive options
// (-s workspace-write, --add-dir) are not sufficient because codex's
// workspace-write policy excludes anything under `.git/` regardless of
// --add-dir, which blocks investigate's per-run dir at
// <git-common-dir>/entire-investigations/<run-id>/.
func TestCodexSpawner_Argv(t *testing.T) {
t.Parallel()
env := []string{"FOO=bar", "BAZ=qux"}
cmd := NewSpawner().BuildCmd(context.Background(), env, "the-prompt")

wantArgs := []string{
wantCodexAgentName, "exec",
"--skip-git-repo-check",
"--dangerously-bypass-approvals-and-sandbox",
"-",
}
if !reflect.DeepEqual(cmd.Args, wantArgs) {
t.Errorf("Args = %v, want %v", cmd.Args, wantArgs)
}

if !reflect.DeepEqual(cmd.Env, env) {
t.Errorf("Env = %v, want %v", cmd.Env, env)
}

if cmd.Stdin == nil {
t.Fatal("Stdin = nil, want a reader carrying the prompt")
}
got, err := io.ReadAll(cmd.Stdin)
if err != nil {
t.Fatalf("read stdin: %v", err)
}
if string(got) != "the-prompt" {
t.Errorf("stdin = %q, want %q", string(got), "the-prompt")
}
}

// TestCodexSpawner_Argv_StableUnderInvestigateEnv pins the contract
// that the argv does NOT change based on env vars. (A previous
// implementation appended --add-dir from ENTIRE_INVESTIGATE_FINDINGS_DOC;
// that approach didn't actually unblock writes under .git/, so we
// dropped it. This test pins the regression.)
func TestCodexSpawner_Argv_StableUnderInvestigateEnv(t *testing.T) {
t.Parallel()
env := []string{
"FOO=bar",
"ENTIRE_INVESTIGATE_FINDINGS_DOC=/repo/.git/entire-investigations/abcdef012345/findings.md",
}
cmd := NewSpawner().BuildCmd(context.Background(), env, "prompt")

wantArgs := []string{
wantCodexAgentName, "exec",
"--skip-git-repo-check",
"--dangerously-bypass-approvals-and-sandbox",
"-",
}
if !reflect.DeepEqual(cmd.Args, wantArgs) {
t.Errorf("Args = %v, want %v", cmd.Args, wantArgs)
}
}
26 changes: 26 additions & 0 deletions cmd/entire/cli/agent/geminicli/spawner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package geminicli

import (
"context"
"os/exec"
"strings"

"github.com/entireio/cli/cmd/entire/cli/agent/spawn"
)

// geminiSpawner produces argv: gemini -p " "; prompt via stdin.
// The " " argv placeholder triggers headless mode; the prompt goes via stdin
// because gemini's -p flag appends to stdin content.
type geminiSpawner struct{}

// NewSpawner returns a Spawner for gemini-cli's non-interactive review/investigate mode.
func NewSpawner() spawn.Spawner { return geminiSpawner{} }

func (geminiSpawner) Name() string { return "gemini-cli" }

func (geminiSpawner) BuildCmd(ctx context.Context, env []string, prompt string) *exec.Cmd {
cmd := exec.CommandContext(ctx, "gemini", "-p", " ")
cmd.Stdin = strings.NewReader(prompt)
cmd.Env = env
return cmd
}
44 changes: 44 additions & 0 deletions cmd/entire/cli/agent/geminicli/spawner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package geminicli

import (
"context"
"io"
"reflect"
"testing"
)

// TestGeminiCLISpawner_Name asserts the spawner reports the stable registry name.
func TestGeminiCLISpawner_Name(t *testing.T) {
t.Parallel()
if got := NewSpawner().Name(); got != "gemini-cli" {
t.Errorf("Name() = %q, want %q", got, "gemini-cli")
}
}

// TestGeminiCLISpawner_Argv pins the argv + stdin contract:
// gemini -p " " (space placeholder triggers headless mode), prompt via stdin.
func TestGeminiCLISpawner_Argv(t *testing.T) {
t.Parallel()
env := []string{"FOO=bar", "BAZ=qux"}
cmd := NewSpawner().BuildCmd(context.Background(), env, "the-prompt")

wantArgs := []string{"gemini", "-p", " "}
if !reflect.DeepEqual(cmd.Args, wantArgs) {
t.Errorf("Args = %v, want %v", cmd.Args, wantArgs)
}

if !reflect.DeepEqual(cmd.Env, env) {
t.Errorf("Env = %v, want %v", cmd.Env, env)
}

if cmd.Stdin == nil {
t.Fatal("Stdin = nil, want a reader carrying the prompt")
}
got, err := io.ReadAll(cmd.Stdin)
if err != nil {
t.Fatalf("read stdin: %v", err)
}
if string(got) != "the-prompt" {
t.Errorf("stdin = %q, want %q", string(got), "the-prompt")
}
}
30 changes: 30 additions & 0 deletions cmd/entire/cli/agent/spawn/spawn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Package spawn provides the Spawner interface used by both `entire review`
// and `entire investigate` to start an agent process non-interactively.
//
// The interface is intentionally env-contract-agnostic: callers compose
// their own ENTIRE_REVIEW_* or ENTIRE_INVESTIGATE_* env via
// review.AppendReviewEnv or investigate.AppendInvestigateEnv before calling
// BuildCmd. Spawners only own the agent-specific argv shape and stdin
// wiring; they do not append review/investigate env.
package spawn

import (
"context"
"os/exec"
)

// Spawner builds *exec.Cmd values for a specific agent in non-interactive,
// review/investigate mode. The returned Cmd MUST NOT be started yet —
// callers may attach pipes, modify env, etc., before invoking Start.
type Spawner interface {
// Name returns the agent's stable registry name (e.g. "claude-code").
Name() string

// BuildCmd constructs the *exec.Cmd to spawn the agent.
// - env: the full process environment to set on cmd.Env (the caller has
// already appended ENTIRE_REVIEW_* or ENTIRE_INVESTIGATE_* values
// and stripped any stale entries before calling).
// - prompt: the composed prompt string. The spawner decides whether
// this goes via argv or stdin per the agent's CLI shape.
BuildCmd(ctx context.Context, env []string, prompt string) *exec.Cmd
}
Loading
Loading