Skip to content

Commit 536322d

Browse files
authored
Merge pull request #993 from entireio/feat/entire-review
feat: entire review command
2 parents 2754b40 + d8815dc commit 536322d

103 files changed

Lines changed: 14333 additions & 328 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.

.golangci.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,16 @@ linters:
108108
- grpc.DialOption
109109
- github.com/entireio/cli/cmd/entire/cli/summarize.Generator
110110
- github.com/entireio/cli/cmd/entire/cli/agent\..+
111+
- github.com/entireio/cli/cmd/entire/cli/review/types.Process
112+
- github.com/entireio/cli/cmd/entire/cli/review/types.AgentReviewer
113+
- github.com/entireio/cli/cmd/entire/cli/review.SynthesisProvider
111114
- github.com/entireio/cli/cmd/entire/cli/checkpoint.CommittedReader
112115
- github.com/entireio/cli/cmd/entire/cli/strategy.Strategy
113116
- github.com/go-git/go-git/v6/x/plugin.Signer
114117
- github.com/go-git/go-git/v6/plumbing/storer.ReferenceIter
115118
- github.com/go-git/go-git/v6/plumbing.EncodedObject
116119
- github.com/go-git/go-git/v6/storage.Storer
117120
- github.com/go-git/go-git/v6/plumbing/storer.EncodedObjectIter
118-
- github.com/go-git/go-git/v6/x/plugin.Signer
119-
- golang.org/x/crypto/ssh/agent.Agent
120-
- github.com/entireio/cli/cmd/entire/cli/summarize.Generator
121121
- github.com/entireio/cli/e2e/agents.Session
122122
- github.com/go-git/go-billy/v6.Filesystem
123123
- golang.org/x/crypto/ssh/agent.Agent

CLAUDE.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,112 @@ Trailers:
672672
- Test with `mise run test` - strategy tests are in `*_test.go` files
673673
- **Update both CLAUDE.md and AGENTS.md** when modifying the strategy to keep documentation current
674674

675+
### `entire review` Command
676+
677+
`entire review` runs a set of configured review skills inside an agent session. The review session is an immutable fact attached to a checkpoint — no verdict, no status tracking, no empty commits. On the next `git commit`, the review session is condensed into the checkpoint metadata alongside normal sessions, permanently recording that the code was reviewed and which skills were run.
678+
679+
#### Command Surface
680+
681+
```
682+
entire review # Normal run: load config, run configured agent(s)
683+
entire review --edit # Re-open the skills picker before running
684+
entire review --agent <name> # Force a specific configured agent (skips multi-picker)
685+
entire review attach <session-id> # Tag an existing agent session as a review (post-hoc)
686+
entire review attach --force # Skip confirmation
687+
entire review attach --agent <name> # Agent that created the session
688+
entire review attach --skills <s,...> # Declare which skills were run
689+
```
690+
691+
When two or more launchable agents are configured and `--agent` is not set, a multi-select picker appears with an optional per-run prompt field (e.g. "focus on security"). Selecting one agent or passing `--agent` runs the single-agent path; selecting two or more runs the N-agent path.
692+
693+
#### Settings Schema
694+
695+
Review skills are configured per-agent in `.entire/settings.json`:
696+
697+
```json
698+
{
699+
"review": {
700+
"claude-code": {"skills": ["/pr-review-toolkit:review-pr"], "prompt": "Be thorough."},
701+
"codex": {"skills": ["/codex:adversarial-review"]}
702+
}
703+
}
704+
```
705+
706+
The key is the agent name. The value is a `ReviewConfig` with `skills` (skill invocations passed verbatim to the agent) and optional `prompt` (an always-prompt appended to the composed prompt). Settings field: `EntireSettings.Review` in `cmd/entire/cli/settings/settings.go`.
707+
708+
#### How It Works (env-var handshake)
709+
710+
1. `entire review` selects the configured agent (override → alphabetically first → prompt if multiple), composes the review prompt via `review.ComposeReviewPrompt`, and computes scope (closest-ancestor branch via `review.ComputeScopeStats`).
711+
2. **For launchable agents** (claude-code, codex, gemini-cli): the spawned agent process is given env vars `ENTIRE_REVIEW_{SESSION,AGENT,SKILLS,PROMPT,STARTING_SHA}` that the agent's `UserPromptSubmit` lifecycle hook reads to tag the session as `Kind = "agent_review"` with the configured skills/prompt. Each spawned process has its own env, so multiple worktrees and multi-agent runs are correct by construction (no shared marker file, no race).
712+
3. **For non-launchable agents** (cursor, opencode, factoryai-droid): `RunMarkerFallback` writes a `PendingReviewMarker` file and prints guidance — the user opens the agent themselves and runs the skills. Single shared file (`review/marker_fallback.go`); adding new non-launchable agents is a registry entry, not a new file.
713+
4. The agent runs the review skills; the session ends naturally.
714+
5. On the next `git commit`, the PostCommit hook condenses the review session into the checkpoint on `entire/checkpoints/v1`, with `Kind` and `ReviewSkills` recorded in `CommittedMetadata`.
715+
6. The `CheckpointSummary` sets `HasReview = true` for O(1) lookup. `HasReview` is an umbrella "any review happened" flag — future review kinds (e.g. manual review) should also set it.
716+
7. `entire status` and the re-run guard read `HasReview` from the checkpoint metadata (no commit history walking).
717+
718+
#### Checkpoint Metadata
719+
720+
Review metadata is stored at two levels on `entire/checkpoints/v1`:
721+
722+
- **`CommittedMetadata` (per-session)**: `kind: "agent_review"`, `review_skills: ["/skill1", "/skill2"]`, `review_prompt: "..."`
723+
- **`CheckpointSummary` (per-checkpoint)**: `has_review: true` (umbrella; set when any session in the checkpoint has a review-kind `Kind`)
724+
725+
#### Architecture
726+
727+
- **`AgentReviewer` interface** (`cmd/entire/cli/review/types/reviewer.go`): per-agent contract with `Name() string` and `Start(ctx, RunConfig) (Process, error)`. Each launchable agent implements this in its own package.
728+
- **`ReviewerTemplate`** (`cmd/entire/cli/review/types/template.go`): shared scaffolding (Spawn → pipe stdout → run parser → forward events → close). Each agent supplies only its `BuildCmd` (argv/env) and `Parser` (stdout-to-Event stream).
729+
- **`Sink` interface**: consumers of the event stream. Production sinks: `DumpSink` (post-run per-agent narrative), `TUISink` (Bubble Tea live dashboard with Ctrl+O drill-in), `SynthesisSink` (opt-in y/N cross-agent verdict). Sinks are composed by `composeMultiAgentSinks` based on TTY detection.
730+
- **`Run(ctx, reviewer, cfg, sinks)`** (`cmd/entire/cli/review/run.go`): single-agent orchestrator. Forwards events to all sinks via `AgentEvent`, calls `RunFinished` once at end with a populated `RunSummary`. Sink dispatch is serialized; sinks need not internally synchronize.
731+
- **`RunMulti(ctx, reviewers, cfg, sinks)`** (`cmd/entire/cli/review/run_multi.go`): N-agent orchestrator. Each agent runs concurrently in its own goroutine; events fan into a single dispatch loop so the serial-dispatch contract is preserved. Per-agent skills/prompts are injected via `perAgentConfiguredReviewer` adapter (each reviewer sees its own `RunConfig` despite the shared API surface).
732+
- **Env-var contract** (`cmd/entire/cli/review/env.go`): single source of truth for `ENTIRE_REVIEW_*` constants used by spawn-side and lifecycle adoption.
733+
- **Scope detection** (`cmd/entire/cli/review/scope.go`): `detectScopeBaseRef` finds the closest non-self ancestor branch by tip timestamp, with fallback chain `origin/HEAD → origin/main → origin/master → main → master`. Banner output: "Reviewing feat/X vs main: 3 commits, 7 files changed, 2 uncommitted".
734+
735+
#### Multi-Agent UI
736+
737+
When `RunMulti` is dispatched in a TTY, the sink slice is `[TUISink, DumpSink, SynthesisSink?]`:
738+
739+
- **`TUISink` / `reviewTUIModel`** (`cmd/entire/cli/review/tui_sink.go`, `tui_model.go`, `tui_detail.go`): live dashboard with one row per agent (name, status, tokens, last assistant preview, duration). `Ctrl+O` enters drill-in mode on the alt screen showing the full event buffer for the selected agent; `Esc` returns to the dashboard. `Ctrl+C` cancels the run via the shared `CancelFunc`. The model uses `tea.WithoutSignalHandler` so the cobra root retains SIGINT routing. After all agents finish, the user dismisses with any key — `RunFinished` blocks on dismissal so `DumpSink` renders below the TUI rather than overlapping it.
740+
- **`SynthesisSink`** (`cmd/entire/cli/review/synthesis_sink.go`): opt-in y/N prompt offered after the dump. On "y", composes a synthesis prompt covering all agent narratives + per-run user prompt, calls the configured summary provider, and prints the unified verdict. Skipped silently when stdin can't prompt, the run was cancelled, or fewer than 2 agents produced usable output. Provider failures degrade gracefully ("synthesis unavailable: <err>") so the user can still commit.
741+
- **Sink composition** (`composeMultiAgentSinks` in `cmd/entire/cli/review/cmd.go`): pure helper taking explicit `isTTY`/`canPrompt` so tests don't depend on real TTY detection. `findTUISink` picks the TUI out of the slice for `Start`/`Wait` lifecycle hooks.
742+
743+
#### Skill Discovery (Claude Code)
744+
745+
`DiscoverReviewSkills` (`cmd/entire/cli/agent/claudecode/discovery.go`) walks three roots: plugin cache (`~/.claude/plugins/cache/<market>/<plugin>/<version>/{skills,commands,agents}`), user skills (`~/.claude/skills`), user commands/agents (`~/.claude/commands`, `~/.claude/agents`).
746+
747+
For the plugin cache, `pickLatestVersion` picks ONE version directory per plugin: highest valid semver wins; if no entries parse as semver, the lexicographic max is picked (handles the `unknown` sentinel some plugins ship). Without this, multiple installed versions of a plugin produced duplicate skill entries in the picker and prompt.
748+
749+
#### Anti-Features (do NOT recreate)
750+
751+
The redesign eliminated several constructs from the prior implementation. None should be reintroduced without explicit design:
752+
753+
- `PendingReviewMarker` for launchable agents (env-var handshake makes it unnecessary)
754+
- `WorktreePath` field + worktree-scoping logic (env per process eliminates the multi-tenant problem)
755+
- `AgentEntries` map on the marker (each agent has its own env)
756+
- Marker overwrite tripwire / refuse-attach guard (the bug classes they defended against don't exist)
757+
- `--track-only` flag (intentionally removed by #1009)
758+
- `--postreview` / `--finalize` / empty review commits / `/entire-review:finish` skill installer
759+
- `Launcher` + `HeadlessLauncher` as separate interfaces (single `AgentReviewer`)
760+
- `filterCodexOutput` in shared multi-agent code (lives in codex's adapter)
761+
- `sync.Once`-guarded onCancel + parallel `signal.Notify` goroutine (single cancel from start)
762+
763+
#### Key Files
764+
765+
- `cmd/entire/cli/review/cmd.go``NewCommand()`, `runReview` dispatch fork, `composeMultiAgentSinks`
766+
- `cmd/entire/cli/review/picker.go` / `multipicker.go` — config-edit picker, first-run setup, single- and multi-agent selection
767+
- `cmd/entire/cli/review/attach.go` + `cli/review_helpers.go:newReviewAttachCmd``entire review attach` subcommand
768+
- `cmd/entire/cli/review/marker_fallback.go` — non-launchable agent flow (single shared file)
769+
- `cmd/entire/cli/review/prompt.go` / `scope.go` / `run.go` / `dump.go` / `run_multi.go` — core machinery (single-agent + N-agent fan-in)
770+
- `cmd/entire/cli/review/tui_sink.go` / `tui_model.go` / `tui_detail.go` — Bubble Tea TUI sink
771+
- `cmd/entire/cli/review/synthesis_sink.go` / `synthesis_prompt.go` — opt-in cross-agent verdict
772+
- `cmd/entire/cli/review/types/{reviewer,sink,template}.go` — interface contracts (CU2 + CU4 + CU5b)
773+
- `cmd/entire/cli/review/env.go``ENTIRE_REVIEW_*` constants + `EncodeSkills`/`DecodeSkills` + `AppendReviewEnv`
774+
- `cmd/entire/cli/agent/{claudecode,codex,geminicli}/reviewer.go` — per-agent `AgentReviewer` implementations (claude-code, codex with chrome filter, gemini-cli)
775+
- `cmd/entire/cli/agent/claudecode/discovery.go` — skill discovery + `pickLatestVersion` plugin-cache dedupe
776+
- `cmd/entire/cli/lifecycle.go``adoptReviewEnv` reads `ENTIRE_REVIEW_*` from process env; replaces marker-file adoption
777+
- `cmd/entire/cli/review_bridge.go` / `review_helpers.go` — bridge code in `cli` package for cycle-bound functions (`headHasReviewCheckpoint`, `launchableReviewerFor`, `newReviewAttachCmd`, `lazySynthesisProvider`)
778+
- `cmd/entire/cli/checkpoint/checkpoint.go``Kind`, `ReviewSkills`, `ReviewPrompt` on `CommittedMetadata`; `HasReview` on `CheckpointSummary`
779+
- `cmd/entire/cli/settings/settings.go``EntireSettings.Review` field
780+
675781
# Important Notes
676782

677783
- **Before committing:** Follow the "Before Every Commit (REQUIRED)" checklist above - CI will fail without it

cmd/entire/cli/agent/agent.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package agent
66
import (
77
"context"
88
"io"
9+
"os/exec"
910

1011
"github.com/entireio/cli/cmd/entire/cli/agent/types"
1112
)
@@ -254,6 +255,50 @@ type TestOnly interface {
254255
IsTestOnly() bool
255256
}
256257

258+
// Launcher is implemented by agents that `entire` can subprocess-spawn.
259+
// This is used by `entire review` to start an agent with a pre-composed
260+
// initial prompt; other commands may use it later.
261+
//
262+
// Contract:
263+
// - LaunchCmd builds an *exec.Cmd with stdin/stdout/stderr wired to the
264+
// caller's TTY. The agent runs in the foreground and the call blocks.
265+
// - The returned cmd is ready to Run() or Start(); it must NOT be modified
266+
// by the caller except to set environment variables or working dir.
267+
// - initialPrompt is the first user message to send to the agent.
268+
type Launcher interface {
269+
LaunchCmd(ctx context.Context, initialPrompt string) (*exec.Cmd, error)
270+
}
271+
272+
// DiscoveredSkill describes one review-adjacent skill found on disk by a
273+
// SkillDiscoverer. Name is the agent-native invocation form (e.g. a
274+
// slash-prefixed command); Description is scraped from on-disk metadata
275+
// if available; SourcePath is kept for debug logging and is not shown to
276+
// the user.
277+
type DiscoveredSkill struct {
278+
Name string
279+
Description string
280+
SourcePath string
281+
}
282+
283+
// SkillDiscoverer is implemented by agents that can enumerate review-adjacent
284+
// skills installed locally on disk (e.g. plugin skills under
285+
// ~/.claude/plugins/...). This powers the "Installed plugin skills" section
286+
// of the `entire review` picker and the runtime verification that configured
287+
// skills still exist before spawn.
288+
//
289+
// Contract:
290+
// - Safe to call on fresh installs where no plugin dir exists yet —
291+
// return (nil, nil), not an error.
292+
// - Malformed individual skill metadata must be skipped with a Debug log,
293+
// not propagated as an error.
294+
// - A (nil, non-nil) error means "discovery could not run at all" (e.g.
295+
// home dir inaccessible). Callers may treat all errors as "found nothing"
296+
// and log at Debug — discovery must never block the picker.
297+
type SkillDiscoverer interface {
298+
Agent
299+
DiscoverReviewSkills(ctx context.Context) ([]DiscoveredSkill, error)
300+
}
301+
257302
// SessionBaseDirProvider is implemented by agents that store transcripts in a
258303
// home-directory-based structure with per-project subdirectories. This enables
259304
// cross-project transcript search (e.g., when a session was started from a

cmd/entire/cli/agent/architecture_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func TestAgentPackages_NoForbiddenImports(t *testing.T) {
5454
repoPrefix + "telemetry", // telemetry
5555
repoPrefix + "validation", // validation utilities
5656
repoPrefix + "settings", // settings (read-only access)
57+
repoPrefix + "review", // review env contract + AgentReviewer types (used by per-agent reviewer.go files)
5758
}
5859

5960
agentDir := findAgentDir(t)
@@ -126,9 +127,10 @@ func discoverAgentPackages(t *testing.T, agentDir string) []string {
126127
t.Helper()
127128

128129
skipDirs := map[string]bool{
129-
"types": true, // contract types, not an agent implementation
130-
"testutil": true, // shared test utilities
131-
"external": true, // external agent adapter, not a self-registering agent
130+
"types": true, // contract types, not an agent implementation
131+
"testutil": true, // shared test utilities
132+
"external": true, // external agent adapter, not a self-registering agent
133+
"skilldiscovery": true, // shared capability helper (registries, match), not an agent
132134
}
133135

134136
entries, err := os.ReadDir(agentDir)

cmd/entire/cli/agent/claudecode/claude.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,3 +371,20 @@ func (c *ClaudeCodeAgent) ChunkTranscript(_ context.Context, content []byte, max
371371
func (c *ClaudeCodeAgent) ReassembleTranscript(chunks [][]byte) ([]byte, error) {
372372
return agent.ReassembleJSONL(chunks), nil
373373
}
374+
375+
// LaunchCmd builds an exec.Cmd for `claude "<initialPrompt>"`. Stdio is wired
376+
// to the caller's TTY so the agent runs foreground and the user interacts
377+
// normally. The call site is expected to Run() and wait. Hooks inherit the
378+
// parent environment.
379+
func (c *ClaudeCodeAgent) LaunchCmd(ctx context.Context, initialPrompt string) (*exec.Cmd, error) {
380+
bin, err := exec.LookPath("claude")
381+
if err != nil {
382+
return nil, fmt.Errorf("claude binary not on PATH: %w", err)
383+
}
384+
cmd := exec.CommandContext(ctx, bin, initialPrompt)
385+
cmd.Stdin = os.Stdin
386+
cmd.Stdout = os.Stdout
387+
cmd.Stderr = os.Stderr
388+
cmd.Env = os.Environ()
389+
return cmd, nil
390+
}

cmd/entire/cli/agent/claudecode/claude_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,39 @@ import (
44
"context"
55
"errors"
66
"os/exec"
7+
"strings"
78
"testing"
9+
10+
"github.com/entireio/cli/cmd/entire/cli/agent"
811
)
912

13+
func TestClaudeCodeAgent_LaunchCmd(t *testing.T) {
14+
t.Parallel()
15+
a := NewClaudeCodeAgent()
16+
launcher, ok := a.(agent.Launcher)
17+
if !ok {
18+
t.Fatal("ClaudeCodeAgent does not implement agent.Launcher")
19+
}
20+
// Binary may not be on PATH in CI; ErrNotFound is acceptable for this test.
21+
cmd, err := launcher.LaunchCmd(context.Background(), "hello world")
22+
if err != nil {
23+
if errors.Is(err, exec.ErrNotFound) {
24+
t.Skip("claude binary not on PATH; skipping cmd shape check")
25+
}
26+
t.Fatalf("LaunchCmd: %v", err)
27+
}
28+
if cmd == nil {
29+
t.Fatal("nil cmd")
30+
}
31+
if cmd.Path == "" {
32+
t.Error("cmd.Path empty")
33+
}
34+
joined := strings.Join(cmd.Args, " ")
35+
if !strings.Contains(joined, "hello world") {
36+
t.Errorf("args missing prompt: %v", cmd.Args)
37+
}
38+
}
39+
1040
func TestResolveSessionFile(t *testing.T) {
1141
t.Parallel()
1242
ag := &ClaudeCodeAgent{}

0 commit comments

Comments
 (0)