Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion cmd/entire/cli/agent/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const (
AgentTypeFactoryAIDroid types.AgentType = "Factory AI Droid"
AgentTypeGemini types.AgentType = "Gemini CLI"
AgentTypeOpenCode types.AgentType = "OpenCode"
AgentTypeUnknown types.AgentType = "Agent" // Fallback for backwards compatibility
AgentTypeUnknown types.AgentType = "Unknown"
)

// DefaultAgentName is the registry key for the default agent.
Expand Down
3 changes: 3 additions & 0 deletions cmd/entire/cli/agent/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ func TestAgentNameConstants(t *testing.T) {
}

func TestDefaultAgentName(t *testing.T) {
// DefaultAgentName is for the `entire enable` setup flow when no agent is
// detected. It is NOT used for agent attribution fallbacks — those use
// AgentTypeUnknown ("Unknown") or "Unknown" in the DB.
if DefaultAgentName != AgentNameClaudeCode {
t.Errorf("expected DefaultAgentName %q, got %q", AgentNameClaudeCode, DefaultAgentName)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/entire/cli/commit_message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,10 @@ func TestGenerateCommitMessage(t *testing.T) {
expected: "OpenCode session updates",
},
{
name: "returns Agent fallback for empty agent type",
name: "returns Unknown fallback for empty agent type",
prompt: "",
agentType: "",
expected: "Agent session updates",
expected: "Unknown session updates",
},
}

Expand Down
39 changes: 31 additions & 8 deletions cmd/entire/cli/strategy/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,22 +689,45 @@ func ReadAgentTypeFromTree(tree *object.Tree, checkpointPath string) types.Agent
}
}

// Fall back to detecting agent from config files (shadow branches don't have metadata.json).
// Order: Gemini (most specific check), Claude (established default), OpenCode (newest/preview).
// Fall back to detecting agent from config markers (shadow branches don't have metadata.json).
// Multiple agent config markers may coexist when users configure multiple agents via
// `entire configure`. Only return a specific agent type when exactly one agent config
// marker (directory or file) is present; otherwise return Unknown since we can't
// determine which agent created the checkpoint.
var detected types.AgentType
detectedCount := 0

if _, err := tree.File(".gemini/settings.json"); err == nil {
return agent.AgentTypeGemini
detected = agent.AgentTypeGemini
detectedCount++
}
if _, err := tree.Tree(".claude"); err == nil {
return agent.AgentTypeClaudeCode
detected = agent.AgentTypeClaudeCode
detectedCount++
}
// OpenCode: .opencode directory or opencode.json config
if _, err := tree.Tree(".opencode"); err == nil {
return agent.AgentTypeOpenCode
detected = agent.AgentTypeOpenCode
detectedCount++
} else if _, err := tree.File("opencode.json"); err == nil {
detected = agent.AgentTypeOpenCode
detectedCount++
}
if _, err := tree.Tree(".codex"); err == nil {
detected = agent.AgentTypeCodex
detectedCount++
}
if _, err := tree.File("opencode.json"); err == nil {
return agent.AgentTypeOpenCode
if _, err := tree.Tree(".cursor"); err == nil {
detected = agent.AgentTypeCursor
detectedCount++
}
if _, err := tree.Tree(".factory"); err == nil {
detected = agent.AgentTypeFactoryAIDroid
detectedCount++
}

if detectedCount == 1 {
return detected
}
return agent.AgentTypeUnknown
}

Expand Down
150 changes: 150 additions & 0 deletions cmd/entire/cli/strategy/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import (
"sync"
"testing"

"github.com/entireio/cli/cmd/entire/cli/agent"
_ "github.com/entireio/cli/cmd/entire/cli/agent/claudecode"
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/testutil"

"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"
"github.com/go-git/go-git/v6/plumbing/object"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestOpenRepository(t *testing.T) {
Expand Down Expand Up @@ -1544,3 +1548,149 @@ func TestIsEmptyRepository(t *testing.T) {
}
})
}

// openRepoHeadTree opens the repo at dir and returns the HEAD commit tree.
func openRepoHeadTree(t *testing.T, dir string) *object.Tree {
t.Helper()
repo, err := git.PlainOpen(dir)
require.NoError(t, err)
head, err := repo.Head()
require.NoError(t, err)
commit, err := repo.CommitObject(head.Hash())
require.NoError(t, err)
tree, err := commit.Tree()
require.NoError(t, err)
return tree
}

func TestReadAgentTypeFromTree_OnlyClaude(t *testing.T) {
t.Parallel()

dir := t.TempDir()
testutil.InitRepo(t, dir)
testutil.WriteFile(t, dir, ".claude/settings.json", `{}`)
testutil.GitAdd(t, dir, ".claude/settings.json")
testutil.GitCommit(t, dir, "init")

tree := openRepoHeadTree(t, dir)
result := ReadAgentTypeFromTree(tree, "nonexistent-path")
assert.Equal(t, agent.AgentTypeClaudeCode, result)
}

func TestReadAgentTypeFromTree_OnlyGemini(t *testing.T) {
t.Parallel()

dir := t.TempDir()
testutil.InitRepo(t, dir)
testutil.WriteFile(t, dir, ".gemini/settings.json", `{}`)
testutil.GitAdd(t, dir, ".gemini/settings.json")
testutil.GitCommit(t, dir, "init")

tree := openRepoHeadTree(t, dir)
result := ReadAgentTypeFromTree(tree, "nonexistent-path")
assert.Equal(t, agent.AgentTypeGemini, result)
}

func TestReadAgentTypeFromTree_OnlyCodex(t *testing.T) {
t.Parallel()

dir := t.TempDir()
testutil.InitRepo(t, dir)
testutil.WriteFile(t, dir, ".codex/config.json", `{}`)
testutil.GitAdd(t, dir, ".codex/config.json")
testutil.GitCommit(t, dir, "init")

tree := openRepoHeadTree(t, dir)
result := ReadAgentTypeFromTree(tree, "nonexistent-path")
assert.Equal(t, agent.AgentTypeCodex, result)
}

func TestReadAgentTypeFromTree_OnlyCursor(t *testing.T) {
t.Parallel()

dir := t.TempDir()
testutil.InitRepo(t, dir)
testutil.WriteFile(t, dir, ".cursor/settings.json", `{}`)
testutil.GitAdd(t, dir, ".cursor/settings.json")
testutil.GitCommit(t, dir, "init")

tree := openRepoHeadTree(t, dir)
result := ReadAgentTypeFromTree(tree, "nonexistent-path")
assert.Equal(t, agent.AgentTypeCursor, result)
}

func TestReadAgentTypeFromTree_OnlyFactory(t *testing.T) {
t.Parallel()

dir := t.TempDir()
testutil.InitRepo(t, dir)
testutil.WriteFile(t, dir, ".factory/settings.json", `{}`)
testutil.GitAdd(t, dir, ".factory/settings.json")
testutil.GitCommit(t, dir, "init")

tree := openRepoHeadTree(t, dir)
result := ReadAgentTypeFromTree(tree, "nonexistent-path")
assert.Equal(t, agent.AgentTypeFactoryAIDroid, result)
}

func TestReadAgentTypeFromTree_ClaudeAndCodex_ReturnsUnknown(t *testing.T) {
t.Parallel()

dir := t.TempDir()
testutil.InitRepo(t, dir)
testutil.WriteFile(t, dir, ".claude/settings.json", `{}`)
testutil.GitAdd(t, dir, ".claude/settings.json")
testutil.WriteFile(t, dir, ".codex/config.json", `{}`)
testutil.GitAdd(t, dir, ".codex/config.json")
testutil.GitCommit(t, dir, "init")

tree := openRepoHeadTree(t, dir)
result := ReadAgentTypeFromTree(tree, "nonexistent-path")
assert.Equal(t, agent.AgentTypeUnknown, result)
}

func TestReadAgentTypeFromTree_ClaudeAndGemini_ReturnsUnknown(t *testing.T) {
t.Parallel()

dir := t.TempDir()
testutil.InitRepo(t, dir)
testutil.WriteFile(t, dir, ".claude/settings.json", `{}`)
testutil.GitAdd(t, dir, ".claude/settings.json")
testutil.WriteFile(t, dir, ".gemini/settings.json", `{}`)
testutil.GitAdd(t, dir, ".gemini/settings.json")
testutil.GitCommit(t, dir, "init")

tree := openRepoHeadTree(t, dir)
result := ReadAgentTypeFromTree(tree, "nonexistent-path")
assert.Equal(t, agent.AgentTypeUnknown, result)
}

func TestReadAgentTypeFromTree_NoAgentDirs_ReturnsUnknown(t *testing.T) {
t.Parallel()

dir := t.TempDir()
testutil.InitRepo(t, dir)
testutil.WriteFile(t, dir, "f.txt", "init")
testutil.GitAdd(t, dir, "f.txt")
testutil.GitCommit(t, dir, "init")

tree := openRepoHeadTree(t, dir)
result := ReadAgentTypeFromTree(tree, "nonexistent-path")
assert.Equal(t, agent.AgentTypeUnknown, result)
}

func TestReadAgentTypeFromTree_MetadataJSON_OverridesDir(t *testing.T) {
t.Parallel()

dir := t.TempDir()
testutil.InitRepo(t, dir)
testutil.WriteFile(t, dir, ".claude/settings.json", `{}`)
testutil.GitAdd(t, dir, ".claude/settings.json")
testutil.WriteFile(t, dir, "cp/metadata.json", `{"agent":"Cursor"}`)
testutil.GitAdd(t, dir, "cp/metadata.json")
testutil.GitCommit(t, dir, "init")

tree := openRepoHeadTree(t, dir)
result := ReadAgentTypeFromTree(tree, "cp")
assert.Equal(t, agent.AgentTypeCursor, result)
}
Loading