Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d13528a
Add wingman automated code review feature
dipree Feb 11, 2026
a6c2d1b
Add debug logging to wingman background review process
dipree Feb 11, 2026
41456e6
Enhance wingman: intent-aware review, post-commit trigger, recursion …
dipree Feb 11, 2026
73d03f2
Inject wingman review instruction into agent context on prompt-submit
dipree Feb 11, 2026
d4dbd80
Extract wingman apply instruction into embedded markdown file
dipree Feb 11, 2026
962a3c9
Rename wingman_apply.md to wingman_instruction.md
dipree Feb 11, 2026
b5c8d26
Use branch-level diff for wingman reviews instead of single commit
dipree Feb 11, 2026
6289e2b
Show wingman status in session start message
dipree Feb 11, 2026
467e432
Merge branch 'main' into dipree/entire-wingman
dipree Feb 12, 2026
3dceaba
Add reliable wingman auto-apply via stop hook and stale review cleanup
dipree Feb 12, 2026
7dcd943
Fix wingman auto-apply not triggering on no-changes turn end
dipree Feb 12, 2026
9ff20c8
Add structured logging for wingman review lifecycle
dipree Feb 12, 2026
5913a8e
Fix isSessionIdle failing in detached wingman subprocesses
dipree Feb 12, 2026
18accf7
Use additionalContext for wingman review injection to ensure it's add…
dipree Feb 12, 2026
6627812
Prefer visible review delivery and add session-end auto-apply trigger
dipree Feb 12, 2026
7491e85
Add timeout to resolveHEAD and fix log file cleanup in spawn helpers
dipree Feb 12, 2026
ef43775
Document review prompt construction and context sources
dipree Feb 12, 2026
2bd71c8
Add --local flag to wingman enable/disable commands
dipree Feb 12, 2026
5fddc49
Fix hook response format to nest additionalContext under hookSpecific…
dipree Feb 12, 2026
b01e554
Restore user-visible SessionStart message and fix hook control flow
dipree Feb 12, 2026
5f69d94
Fix wingman auto-apply never triggering on session close
dipree Feb 12, 2026
ff06729
Add wingman status notifications visible in agent terminal
dipree Feb 12, 2026
118936d
Address PR review comments and update wingman documentation
dipree Feb 12, 2026
121565d
Restore prompt structure diagram in wingman docs
dipree Feb 12, 2026
0825633
Skip wingman lock file notifications when lock is stale
dipree Feb 12, 2026
d2ef783
Allow strategy.Strategy in ireturn lint config
dipree Feb 12, 2026
6808d51
Use tighter lock threshold for wingman status notifications
dipree Feb 12, 2026
69a35cf
Clean up ireturn nolint, add spawn comments, update stale session tests
dipree Feb 12, 2026
e794c91
Show pending review status in SessionStart wingman message
dipree Feb 12, 2026
0ce3f3f
Fix stale active sessions blocking wingman auto-apply
dipree Feb 12, 2026
2315a1e
Merge remote-tracking branch 'origin/main' into dipree/entire-wingman
dipree Feb 12, 2026
46815ae
Fix context param shadowing and log file FD leak in parent process
dipree Feb 12, 2026
2a1f8b4
Merge remote-tracking branch 'origin/main' into dipree/entire-wingman
dipree Feb 13, 2026
8161293
fix: strip CLAUDECODE env var in wingman/summarize subprocess calls
dipree Feb 13, 2026
6706f44
fix: auto-gitignore wingman runtime files
dipree Feb 13, 2026
576928c
Merge remote-tracking branch 'origin/main' into dipree/entire-wingman
dipree Feb 15, 2026
23371bf
chore: remove extra blank line in hooks_claudecode_handlers
dipree Feb 16, 2026
cb567b6
chore: use opus model for wingman reviews
dipree Feb 17, 2026
d702825
Merge remote-tracking branch 'origin/main' into dipree/entire-wingman
dipree Feb 17, 2026
20bf1c7
Merge remote-tracking branch 'origin/main' into dipree/entire-wingman
dipree Feb 18, 2026
4139581
feat: skip wingman injection when review has no actionable issues
dipree Feb 18, 2026
94a7206
feat: add shared CLI helpers for agent subprocess invocation
dipree Feb 18, 2026
981be7f
feat: add Prompter interface to Agent API
dipree Feb 18, 2026
77e7589
feat: implement Prompter interface for Claude Code agent
dipree Feb 18, 2026
f27090d
feat: add settings helpers for wingman/summarize agent and model
dipree Feb 18, 2026
a2fed1c
refactor: use Prompter interface for wingman reviews
dipree Feb 18, 2026
7e164bd
refactor: use Prompter interface for summarization
dipree Feb 18, 2026
5d7c576
feat: add --agent and --model flags to wingman enable
dipree Feb 18, 2026
01edf80
fix: inline JSON in prompter test to prevent shell injection
dipree Feb 19, 2026
37688c0
Merge remote-tracking branch 'origin/main' into dipree/entire-wingman
dipree Feb 19, 2026
9341f66
fix: preserve unparseable hook types in settings
dipree Feb 19, 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 .entire/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ settings.local.json
metadata/
current_session
logs/
wingman.lock
wingman-state.json
wingman-payload.json
REVIEW.md
1 change: 1 addition & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ linters:
- grpc.DialOption
- github.com/entireio/cli/cmd/entire/cli/agent.Agent
- github.com/entireio/cli/cmd/entire/cli/strategy.Strategy
- github.com/entireio/cli/cmd/entire/cli/summarize.Generator
- github.com/go-git/go-git/v6/plumbing/storer.ReferenceIter
- github.com/go-git/go-git/v6/plumbing.EncodedObject
- github.com/go-git/go-git/v6/storage.Storer
Expand Down
8 changes: 7 additions & 1 deletion cmd/entire/cli/agent/claudecode/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package claudecode

import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"time"
Expand All @@ -25,7 +27,11 @@ func init() {
// ClaudeCodeAgent implements the Agent interface for Claude Code.
//
//nolint:revive // ClaudeCodeAgent is clearer than Agent in this context
type ClaudeCodeAgent struct{}
type ClaudeCodeAgent struct {
// CommandRunner allows injection of the command execution for testing.
// If nil, uses exec.CommandContext directly.
CommandRunner func(ctx context.Context, name string, args ...string) *exec.Cmd
}

// NewClaudeCodeAgent creates a new Claude Code agent instance.
func NewClaudeCodeAgent() agent.Agent {
Expand Down
72 changes: 41 additions & 31 deletions cmd/entire/cli/agent/claudecode/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,15 @@ func (c *ClaudeCodeAgent) InstallHooks(localDev bool, force bool) (int, error) {
rawPermissions = make(map[string]json.RawMessage)
}

// Parse only the hook types we need to modify
// Parse only the hook types we need to modify.
// Track which types parsed successfully — unparseable types are left untouched.
var sessionStart, sessionEnd, stop, userPromptSubmit, preToolUse, postToolUse []ClaudeHookMatcher
parseHookType(rawHooks, "SessionStart", &sessionStart)
parseHookType(rawHooks, "SessionEnd", &sessionEnd)
parseHookType(rawHooks, "Stop", &stop)
parseHookType(rawHooks, "UserPromptSubmit", &userPromptSubmit)
parseHookType(rawHooks, "PreToolUse", &preToolUse)
parseHookType(rawHooks, "PostToolUse", &postToolUse)
parsedSessionStart := parseHookType(rawHooks, "SessionStart", &sessionStart)
parsedSessionEnd := parseHookType(rawHooks, "SessionEnd", &sessionEnd)
parsedStop := parseHookType(rawHooks, "Stop", &stop)
parsedUserPromptSubmit := parseHookType(rawHooks, "UserPromptSubmit", &userPromptSubmit)
parsedPreToolUse := parseHookType(rawHooks, "PreToolUse", &preToolUse)
parsedPostToolUse := parseHookType(rawHooks, "PostToolUse", &postToolUse)

// If force is true, remove all existing Entire hooks first
if force {
Expand Down Expand Up @@ -203,12 +204,12 @@ func (c *ClaudeCodeAgent) InstallHooks(localDev bool, force bool) (int, error) {
}

// Marshal modified hook types back to rawHooks
marshalHookType(rawHooks, "SessionStart", sessionStart)
marshalHookType(rawHooks, "SessionEnd", sessionEnd)
marshalHookType(rawHooks, "Stop", stop)
marshalHookType(rawHooks, "UserPromptSubmit", userPromptSubmit)
marshalHookType(rawHooks, "PreToolUse", preToolUse)
marshalHookType(rawHooks, "PostToolUse", postToolUse)
marshalHookType(rawHooks, "SessionStart", sessionStart, parsedSessionStart)
marshalHookType(rawHooks, "SessionEnd", sessionEnd, parsedSessionEnd)
marshalHookType(rawHooks, "Stop", stop, parsedStop)
marshalHookType(rawHooks, "UserPromptSubmit", userPromptSubmit, parsedUserPromptSubmit)
marshalHookType(rawHooks, "PreToolUse", preToolUse, parsedPreToolUse)
marshalHookType(rawHooks, "PostToolUse", postToolUse, parsedPostToolUse)

// Marshal hooks and update raw settings
hooksJSON, err := json.Marshal(rawHooks)
Expand Down Expand Up @@ -242,17 +243,25 @@ func (c *ClaudeCodeAgent) InstallHooks(localDev bool, force bool) (int, error) {
}

// parseHookType parses a specific hook type from rawHooks into the target slice.
// Silently ignores parse errors (leaves target unchanged).
func parseHookType(rawHooks map[string]json.RawMessage, hookType string, target *[]ClaudeHookMatcher) {
// Returns true if the hook type was successfully parsed (or didn't exist).
// Returns false if parsing failed — caller should NOT marshal this type back,
// to avoid replacing the original (unparseable) data with an empty array.
func parseHookType(rawHooks map[string]json.RawMessage, hookType string, target *[]ClaudeHookMatcher) bool {
if data, ok := rawHooks[hookType]; ok {
//nolint:errcheck,gosec // Intentionally ignoring parse errors - leave target as nil/empty
json.Unmarshal(data, target)
if err := json.Unmarshal(data, target); err != nil {
return false
}
}
return true
}

// marshalHookType marshals a hook type back to rawHooks.
// If parsed is false, the original data couldn't be parsed and is left untouched.
// If the slice is empty, removes the key from rawHooks.
func marshalHookType(rawHooks map[string]json.RawMessage, hookType string, matchers []ClaudeHookMatcher) {
func marshalHookType(rawHooks map[string]json.RawMessage, hookType string, matchers []ClaudeHookMatcher, parsed bool) {
if !parsed {
return // Don't overwrite unparseable data
}
if len(matchers) == 0 {
delete(rawHooks, hookType)
return
Expand Down Expand Up @@ -293,14 +302,15 @@ func (c *ClaudeCodeAgent) UninstallHooks() error {
rawHooks = make(map[string]json.RawMessage)
}

// Parse only the hook types we need to modify
// Parse only the hook types we need to modify.
// Track which types parsed successfully — unparseable types are left untouched.
var sessionStart, sessionEnd, stop, userPromptSubmit, preToolUse, postToolUse []ClaudeHookMatcher
parseHookType(rawHooks, "SessionStart", &sessionStart)
parseHookType(rawHooks, "SessionEnd", &sessionEnd)
parseHookType(rawHooks, "Stop", &stop)
parseHookType(rawHooks, "UserPromptSubmit", &userPromptSubmit)
parseHookType(rawHooks, "PreToolUse", &preToolUse)
parseHookType(rawHooks, "PostToolUse", &postToolUse)
parsedSessionStart := parseHookType(rawHooks, "SessionStart", &sessionStart)
parsedSessionEnd := parseHookType(rawHooks, "SessionEnd", &sessionEnd)
parsedStop := parseHookType(rawHooks, "Stop", &stop)
parsedUserPromptSubmit := parseHookType(rawHooks, "UserPromptSubmit", &userPromptSubmit)
parsedPreToolUse := parseHookType(rawHooks, "PreToolUse", &preToolUse)
parsedPostToolUse := parseHookType(rawHooks, "PostToolUse", &postToolUse)

// Remove Entire hooks from all hook types
sessionStart = removeEntireHooks(sessionStart)
Expand All @@ -311,12 +321,12 @@ func (c *ClaudeCodeAgent) UninstallHooks() error {
postToolUse = removeEntireHooksFromMatchers(postToolUse)

// Marshal modified hook types back to rawHooks
marshalHookType(rawHooks, "SessionStart", sessionStart)
marshalHookType(rawHooks, "SessionEnd", sessionEnd)
marshalHookType(rawHooks, "Stop", stop)
marshalHookType(rawHooks, "UserPromptSubmit", userPromptSubmit)
marshalHookType(rawHooks, "PreToolUse", preToolUse)
marshalHookType(rawHooks, "PostToolUse", postToolUse)
marshalHookType(rawHooks, "SessionStart", sessionStart, parsedSessionStart)
marshalHookType(rawHooks, "SessionEnd", sessionEnd, parsedSessionEnd)
marshalHookType(rawHooks, "Stop", stop, parsedStop)
marshalHookType(rawHooks, "UserPromptSubmit", userPromptSubmit, parsedUserPromptSubmit)
marshalHookType(rawHooks, "PreToolUse", preToolUse, parsedPreToolUse)
marshalHookType(rawHooks, "PostToolUse", postToolUse, parsedPostToolUse)

// Also remove the metadata deny rule from permissions
var rawPermissions map[string]json.RawMessage
Expand Down
89 changes: 89 additions & 0 deletions cmd/entire/cli/agent/claudecode/prompter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package claudecode

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"

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

// Compile-time assertion that ClaudeCodeAgent implements agent.Prompter.
var _ agent.Prompter = (*ClaudeCodeAgent)(nil)

// CLICommand returns the CLI executable name for Claude Code.
func (c *ClaudeCodeAgent) CLICommand() string {
return "claude"
}

// Prompt sends a prompt to the Claude CLI and returns the text response.
func (c *ClaudeCodeAgent) Prompt(ctx context.Context, prompt string, opts agent.PromptOptions) (*agent.PromptResult, error) {
args := []string{"--print", "--setting-sources", ""}

outputFormat := opts.OutputFormat
if outputFormat == "" {
outputFormat = "json"
}
args = append(args, "--output-format", outputFormat)

if opts.Model != "" {
args = append(args, "--model", opts.Model)
}
if opts.AllowedTools != "" {
args = append(args, "--allowedTools", opts.AllowedTools)
}
if opts.PermissionMode != "" {
args = append(args, "--permission-mode", opts.PermissionMode)
}

runner := c.CommandRunner
if runner == nil {
runner = exec.CommandContext
}

cmd := runner(ctx, c.CLICommand(), args...)

// Working directory
if opts.WorkDir != "" {
cmd.Dir = opts.WorkDir
} else {
cmd.Dir = os.TempDir()
}

// Environment isolation
isolate := true
if opts.IsolateFromGit != nil {
isolate = *opts.IsolateFromGit
}
if isolate {
cmd.Env = agent.StripGitEnv(os.Environ())
} else {
cmd.Env = os.Environ()
}
cmd.Env = append(cmd.Env, opts.ExtraEnv...)

cmd.Stdin = strings.NewReader(prompt)

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("claude prompt failed: %w", agent.FormatExecError(err, "claude", stderr.String()))
}

// Parse response based on output format
if outputFormat == "json" {
var cliResp agent.CLIResponse
if err := json.Unmarshal(stdout.Bytes(), &cliResp); err != nil {
return nil, fmt.Errorf("failed to parse claude CLI response: %w", err)
}
return &agent.PromptResult{Text: cliResp.Result}, nil
}

return &agent.PromptResult{Text: stdout.String()}, nil
}
Loading