You are reviewing one semantic feature for clawpatch. · Entire

Home

Log in

You are reviewing one semantic feature for clawpatch.

Return strict JSON only. No markdown fences.

Project: { "name": "cli", "detected": { "languages": [
"go"
], "frameworks": [], "packageManagers": [], "commands": { "typecheck": "go test ./...", "lint": null, "format": null, "test": "go test ./..." } } }

Feature: { "schemaVersion": 1, "featureId": "feat_cli-command_d6bd094fc3", "title": "Go command entire", "summary": "Go command package cmd/entire with 1 source file(s).", "kind": "cli-command", "source": "go-cmd", "confidence": "medium", "entrypoints": [
{
"path": "cmd/entire/main.go",
"symbol": "main",
"route": null,
"command": "entire"
}
], "ownedFiles": [
{
"path": "cmd/entire/main.go",
"reason": "go package source"
}
], "contextFiles": [
{
"path": "cmd/entire/cli/activity_cmd.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/activity_render.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/activity_tui.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/activity_types.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/agent_group.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/aliascmd.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/api_client.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/attach.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/attach_transcript.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/auth.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/checkpoint_group.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/checkpoint_reader.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/clean.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/commit_message.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/config.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/constants.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/dispatch.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/dispatch_tui.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/dispatch_wizard.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/doctor.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/doctor_bundle.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/doctor_logs.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/errors.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
},
{
"path": "cmd/entire/cli/explain.go",
"reason": "imported package github.com/entireio/cli/cmd/entire/cli"
}
], "tests": [], "tags": [
"go",
"cli"
], "trustBoundaries": [
"user-input",
"filesystem",
"process-exec",
"network"
], "status": "claimed", "lock": { "lockedByRunId": "20260519T175303-e13c5e", "lockedAt": "2026-05-19T17:53:03.057Z", "hostname": "Peytons-MacBook-Pro.local", "pid": 43571 }, "findingIds": [], "patchAttemptIds": [], "analysisHistory": [], "createdAt": "2026-05-19T17:52:59.936Z", "updatedAt": "2026-05-19T17:53:03.058Z" }

Review categories:

Inspect owned files, context files, and linked tests. Treat included tests as first-class evidence of intended behavior. If tests contradict a suspected bug, either skip it or downgrade confidence and explain the uncertainty. Avoid reporting behavior as a bug solely because a helper name implies a broader contract. Deduplicate sibling/root-cause issues: when the same bug pattern appears in multiple owned files, emit one finding with multiple evidence refs instead of separate one-off findings.

Avoid speculative low-evidence findings. Evidence must point at included files.

JSON shape: { "findings": [
{
"title": "string",
"category": "bug|security|performance|concurrency|api-contract|data-loss|test-gap|docs-gap|build-release|maintainability",
"severity": "critical|high|medium|low",
"confidence": "high|medium|low",
"evidence": [{"path":"string","startLine":1,"endLine":1,"symbol":null,"quote":null}],
"reasoning": "string",
"reproduction": null,
"recommendation": "string",
"whyTestsDoNotAlreadyCoverThis": "string",
"suggestedRegressionTest": "string or null",
"minimumFixScope": "string"
}
], "inspected": {"files":["string"],"symbols":["string"],"notes":["string"]} }

Files: --- cmd/entire/main.go package main

import ( "context" "errors" "fmt" "os" "os/signal" "runtime" "strings" "syscall"

"github.com/entireio/cli/cmd/entire/cli"
"github.com/spf13/cobra"

)

func main() { // Create context that cancels on interrupt ctx, cancel := context.WithCancel(context.Background())

// Handle interrupt signals
sigChan := make(chan os.Signal, 1)
signals := []os.Signal{os.Interrupt}
if runtime.GOOS != "windows" {
    signals = append(signals, syscall.SIGTERM)
}
signal.Notify(sigChan, signals...)
go func() {
    <-sigChan
    cancel()
}()

// Create and execute root command
rootCmd := cli.NewRootCmd()

// Make managed-installed plugins discoverable by the kubectl-style
// dispatcher: prepend the managed bin dir to PATH before resolution.
// Idempotent and silent on failure (managed installs simply won't be
// found this run; PATH-installed plugins still work). The closure
// restores PATH so built-in commands and their subprocesses don't
// inherit the prepended dir. When a plugin runs, we skip the restore
// — the os.Exit ends the process, and the plugin child intentionally
// inherits the prepended PATH so it can spawn sibling managed plugins.
restorePATH := cli.PrependPluginBinDirToPATH(ctx)

if handled, code := cli.MaybeRunPlugin(ctx, rootCmd, os.Args[1:]); handled {
    cancel()
    os.Exit(code)
}
restorePATH()

err := rootCmd.ExecuteContext(ctx)
if err != nil {
    var silent *cli.SilentError

switch {
    case errors.As(err, &silent):
        // Command already printed the error
    case strings.Contains(err.Error(), "unknown command") || strings.Contains(err.Error(), "unknown flag"):
        showSuggestion(rootCmd, err)
    default:
        fmt.Fprintln(rootCmd.OutOrStderr(), err)
    }

cancel()
    os.Exit(1)
}
cancel() // Cleanup on successful exit

}

func showSuggestion(cmd *cobra.Command, err error) { // Print usage first (brew style) fmt.Fprint(cmd.OutOrStderr(), cmd.UsageString()) fmt.Fprintf(cmd.OutOrStderr(), "\nError: Invalid usage: %v\n", err) }

--- cmd/entire/cli/activity_cmd.go package cli

import ( "context" "fmt" "io" "net/url" "os" "sort" "strconv" "strings" "time"

"github.com/entireio/cli/cmd/entire/cli/api"
"github.com/entireio/cli/cmd/entire/cli/interactive"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"

)

const ( agentUnknown = "unknown" dateUnknown = "unknown" activityTimeframe = "last-month" activityLimit = 1000 )

// knownAgents maps normalized agent strings from the API to display IDs. // Used for the commit list, where per-checkpoint agent strings are free-form. // The /me/activity endpoint returns already-normalized canonical IDs. var knownAgents = map[string]string{ "claude": "claude", "claudecode": "claude", "gemini": "gemini", "geminicli": "gemini", "amp": "amp", "codex": "codex", "opencode": "opencode", "copilot": "copilot", "copilotcli": "copilot", "pi": "pi", "cursor": "cursor", "droid": "droid", "kiro": "kiro", }

func newActivityCmd() *cobra.Command { cmd := &cobra.Command{ Use: "activity", Short: "Show your activity overview", Long: "Display your activity overview, repository breakdown, and recent commits from entire.io", RunE: func(cmd *cobra.Command, _ []string) error { return runActivity(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr()) }, } return cmd }

func runActivity(ctx context.Context, w, errW io.Writer) error { client, err := NewAuthenticatedAPIClient(false) if err != nil { fmt.Fprintln(errW, "Not logged in. Run 'entire login' to authenticate.") return NewSilentError(err) }

// Non-interactive fallback: piped output or accessibility mode
if !interactive.IsTerminalWriter(w) || IsAccessibleMode() {
    return runActivityStatic(ctx, w, client)
}

return runActivityTUI(ctx, client)

}

func runActivityStatic(ctx context.Context, w io.Writer, client *api.Client) error { activity, commits, err := fetchActivityData(ctx, client) if err != nil { return err }

stats := contributionStats{
    Tasks:         activity.Stats.Tasks,
    Throughput:    activity.Stats.Throughput,
    Iteration:     activity.Stats.Iteration,
    ContinuityH:   activity.Stats.ContinuityHours,
    Streak:        activity.Stats.LifetimeStreak,
    CurrentStreak: activity.Stats.LifetimeCurrentStreak,
}
days := groupCommitsByDay(commits)

sty := newActivityStyles(w)
renderActivity(w, sty, stats, activity.Repos, activity.HourlyContributions, days)
return nil

}

// fetchActivityData fetches aggregated activity and commits concurrently. func fetchActivityData(ctx context.Context, client *api.Client) (*userActivityResponse, []userCommit, error) { var activity *userActivityResponse var commits []userCommit

g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
    var err error
    activity, err = fetchActivity(gCtx, client)
    return err
})
g.Go(func() error {
    var err error
    commits, err = fetchCommits(gCtx, client)
    return err
})
if err := g.Wait(); err != nil {
    return nil, nil, fmt.Errorf("fetch activity: %w", err)
}
return activity, commits, nil

}

func fetchActivity(ctx context.Context, client *api.Client) (*userActivityResponse, error) { q := url.Values{} q.Set("timezone", detectTimezone()) q.Set("timeframe", activityTimeframe) q.Set("limit", strconv.Itoa(activityLimit)) path := "/api/v1/me/activity?" + q.Encode()

resp, err := client.Get(ctx, path)
if err != nil {
    return nil, fmt.Errorf("GET activity: %w", err)
}
defer resp.Body.Close()

if err := api.CheckResponse(resp); err != nil {
    return nil, fmt.Errorf("activity response: %w", err)
}

var result userActivityResponse
if err := api.DecodeJSON(resp, &result); err != nil {
    return nil, fmt.Errorf("decode activity: %w", err)
}
return &result, nil

}

func fetchCommits(ctx context.Context, client *api.Client) ([]userCommit, error) { path := fmt.Sprintf("/api/v1/me/commits?timeframe=%s&limit=%d", activityTimeframe, activityLimit) resp, err := client.Get(ctx, path) if err != nil { return nil, fmt.Errorf("GET commits: %w", err) } defer resp.Body.Close()

if err := api.CheckResponse(resp); err != nil {
    return nil, fmt.Errorf("commits response: %w", err)
}

var result userCommitsResponse
if err := api.DecodeJSON(resp, &result); err != nil {
    return nil, fmt.Errorf("decode commits: %w", err)
}
return result.Commits, nil

}

// detectTimezone returns a best-effort timezone name for the current host. // Order: $TZ → /etc/localtime symlink → time.Local → "UTC" as last resort. // A candidate that fails normalization is skipped (not forwarded, not coerced // to UTC), so a bogus $TZ on a correctly-configured box still yields the // system timezone from /etc/localtime. The server is the canonical authority // for what counts as a valid zone and falls back to UTC for anything it // doesn't recognize, so we only do enough validation to avoid sending // obvious garbage (paths, POSIX forms Go can't load, the "Local" sentinel). func detectTimezone() string { if tz := normalizeTimezone(os.Getenv("TZ")); tz != "" { return tz } if link, err := os.Readlink("/etc/localtime"); err == nil { if tz := normalizeTimezone(link); tz != "" { return tz } } if tz := normalizeTimezone(time.Local.String()); tz != "" { return tz } return "UTC" }

// normalizeTimezone returns a name Go can load as a time zone, or "" if the // input can't be resolved. It strips the POSIX ":" prefix and zoneinfo path // prefix, then requires time.LoadLocation to succeed. // // This is not strict IANA-only validation: Go's LoadLocation accepts legacy // aliases like EST5EDT, GMT0, and PST8PDT in addition to Area/Location // names. Those may or may not be canonically understood by the server — if // the server doesn't recognize one, it falls back to UTC on its end. We // accept that mild mis-bucketing risk as the price of a simple check that // catches the common failure modes (paths, unknown POSIX forms like UTC0, // typos, the "Local" sentinel). func normalizeTimezone(raw string) string { name := strings.TrimPrefix(raw, ":") const marker = "/zoneinfo/" if idx := strings.LastIndex(name, marker); idx >= 0 { name = name[idx+len(marker):] } if name == "" || name == "Local" { return "" } if _, err := time.LoadLocation(name); err != nil { return "" } return name }

func groupCommitsByDay(commits []userCommit) []commitDay { byDate := make(map[string][]userCommit) var dateOrder []string

for _, c := range commits {
    date := dateUnknown
    if c.CommitDate != nil {
        if t, err := parseFlexibleTime(*c.CommitDate); err == nil {
            date = t.Local().Format("2006-01-02")
        }
    }
    if _, exists := byDate[date]; !exists {
        dateOrder = append(dateOrder, date)
    }
    byDate[date] = append(byDate[date], c)
}

// Sort dates newest first, with unknown dates pushed to the end
sort.Slice(dateOrder, func(i, j int) bool {
    if dateOrder[i] == dateUnknown {
        return false
    }
    if dateOrder[j] == dateUnknown {
        return true
    }
    return dateOrder[i] > dateOrder[j]
})

result := make([]commitDay, 0, len(dateOrder))
for _, d := range dateOrder {
    result = append(result, commitDay{Date: d, Commits: byDate[d]})
}
return result

}

func normalizeAgentString(s string) string { if s == "" { return agentUnknown }

var sb strings.Builder
for _, r := range s {
    if r == ' ' || r == '-' || r == '_' {
        continue
    }
    if r >= 'A' && r <= 'Z' {
        sb.WriteByte(byte(r + 32))
    } else {
        sb.WriteRune(r)
    }
}
lower := sb.String()

if id, ok := knownAgents[lower]; ok {
    return id
}

for _, suffix := range []string{"code", "cli"} {
    if len(lower) > len(suffix) && lower[len(lower)-len(suffix):] == suffix {
        if id, ok := knownAgents[lower[:len(lower)-len(suffix)]]; ok {
            return id
        }
    }
}

if strings.HasPrefix(lower, "factoryaidroid") {
    return "droid"
}

return agentUnknown

}

// parseFlexibleTime tries RFC3339, then RFC3339Nano. func parseFlexibleTime(s string) (time.Time, error) { t, err := time.Parse(time.RFC3339, s) if err != nil { t, err = time.Parse(time.RFC3339Nano, s) if err != nil { return time.Time{}, fmt.Errorf("parse time %q: %w", s, err) } } return t, nil }

--- cmd/entire/cli/activity_render.go package cli

import ( "fmt" "io" "math" "os" "sort" "strconv" "strings" "time"

"charm.land/lipgloss/v2"
"golang.org/x/term"

)

type activityStyles struct { colorEnabled bool width int

bold    lipgloss.Style
dim     lipgloss.Style
label   lipgloss.Style
value   lipgloss.Style
unit    lipgloss.Style
desc    lipgloss.Style
repoNm  lipgloss.Style
commitH lipgloss.Style
commitM lipgloss.Style
add     lipgloss.Style
del     lipgloss.Style
muted   lipgloss.Style

}

// getFullTerminalWidth returns the terminal width without the 80-char cap // used by other commands. Activity benefits from wide output for bar charts. func getFullTerminalWidth(w io.Writer) int { if f, ok := w.(*os.File); ok { if width, _, err := term.GetSize(int(f.Fd())); err == nil && width > 0 { //nolint:gosec // G115: uintptr->int is safe for fd return width } } for _, f := range []*os.File{os.Stdout, os.Stderr} { if f == nil { continue } if width, _, err := term.GetSize(int(f.Fd())); err == nil && width > 0 { //nolint:gosec // G115: uintptr->int is safe for fd return width } } return 80 }

func newActivityStyles(w io.Writer) activityStyles { useColor := shouldUseColor(w) width := getFullTerminalWidth(w)

s := activityStyles{
    colorEnabled: useColor,
    width:        width,
}

if useColor {
    s.bold = lipgloss.NewStyle().Bold(true)
    s.dim = lipgloss.NewStyle().Faint(true)
    s.label = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Bold(true)
    s.value = lipgloss.NewStyle().Bold(true)
    s.unit = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
    s.desc = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
    s.repoNm = lipgloss.NewStyle().Foreground(lipgloss.Color("7"))
    s.commitH = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
    s.commitM = lipgloss.NewStyle().Bold(true)
    s.add = lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
    s.del = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
    s.muted = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
}

return s

}

func (s activityStyles) render(style lipgloss.Style, text string) string { if !s.colorEnabled { return text } return style.Render(text) }

func (s activityStyles) renderAgent(agentID, text string) string { if !s.colorEnabled { return text } display := agentDisplayMap[agentID] return lipgloss.NewStyle().Foreground(lipgloss.Color(display.Color)).Render(text) }

type agentDisplay struct { Label string Color string // ANSI 256 color code Char rune // block character for bar charts }

// Agent colors match the dark-mode CSS variables from entire.io (Tailwind 400-level). // Lipgloss resolves hex to the best representation for the terminal's color profile. var agentDisplayMap = map[string]agentDisplay{ "claude": {Label: "Claude Code", Color: "#fb923c", Char: '▓'}, // orange-400 "gemini": {Label: "Gemini", Color: "#60a5fa", Char: '▓'}, // blue-400 "amp": {Label: "Amp", Color: "#f87171", Char: '▓'}, // red-400 "codex": {Label: "Codex", Color: "#818cf8", Char: '▓'}, // indigo-400 "opencode": {Label: "OpenCode", Color: "#22d3ee", Char: '▓'}, // cyan-400 "copilot": {Label: "Copilot", Color: "#a78bfa", Char: '▓'}, // violet-400 "pi": {Label: "Pi", Color: "#fbbf24", Char: '▓'}, // amber-400 "cursor": {Label: "Cursor", Color: "#38bdf8", Char: '▓'}, // sky-400 "droid": {Label: "Droid", Color: "#f472b6", Char: '▓'}, // pink-400 "kiro": {Label: "Kiro", Color: "#c084fc", Char: '▓'}, // purple-400 "unknown": {Label: "Unknown", Color: "245", Char: '░'}, }

var agentOrder = []string{ "claude", "codex", "gemini", "amp", "opencode", "copilot", "pi", "cursor", "droid", "kiro", "unknown", }

func renderActivity(w io.Writer, sty activityStyles, stats contributionStats, repos []repoContribution, hourly []hourlyPoint, days []commitDay) { fmt.Fprintln(w) renderStatCards(w, sty, stats) fmt.Fprintln(w) renderContributionChart(w, sty, hourly, repos) fmt.Fprintln(w) renderRepoChart(w, sty, repos) fmt.Fprintln(w) renderCommitList(w, sty, days) }

func renderStatCards(w io.Writer, sty activityStyles, stats contributionStats) { cards := []struct { label string value string unit string desc string }{ {"THROUGHPUT", fmt.Sprintf("%.1f", stats.Throughput), "k", "Avg. tokens/checkpoint"}, {"ITERATION", fmt.Sprintf("%.1f", stats.Iteration), "x", "Avg sessions/checkpoint"}, {"CONTINUITY", fmt.Sprintf("%.1f", stats.ContinuityH), "h", "Peak session length"}, {"STREAK", strconv.Itoa(stats.Streak), " day", fmt.Sprintf("%d current", stats.CurrentStreak)}, }

cardWidth := (sty.width - 3) / 4 // 3 separators
if cardWidth < 16 {
    cardWidth = 16
}

var topParts, midParts, botParts []string
for _, c := range cards {
    lbl := padOrTruncate(c.label, cardWidth)
    dsc := padOrTruncate(c.desc, cardWidth)

topParts = append(topParts, sty.render(sty.label, lbl))
    midParts = append(midParts, sty.render(sty.value, c.value)+sty.render(sty.unit, c.unit)+
        strings.Repeat(" ", max(0, cardWidth-len(c.value)-len(c.unit))))
    botParts = append(botParts, sty.render(sty.desc, dsc))
}

sep := sty.render(sty.dim, " │ ")
fmt.Fprintln(w, strings.Join(topParts, sep))
fmt.Fprintln(w, strings.Join(midParts, sep))
fmt.Fprintln(w, strings.Join(botParts, sep))

}

func renderContributionChart(w io.Writer, sty activityStyles, hourly []hourlyPoint, repos []repoContribution) { renderDotChart(w, sty, hourly, repos) }

func renderDotChart(w io.Writer, sty activityStyles, hourly []hourlyPoint, repos []repoContribution) { agentTotals := make(map[string]int) total := 0 for _, r := range repos { total += r.Total for agent, count := range r.Agents { agentTotals[agent] += count } }

totalLabel := ""
if total > 0 {
    totalLabel = sty.render(sty.muted, fmt.Sprintf("  %d checkpoints", total))
}
fmt.Fprintf(w, "%s%s\n", sty.render(sty.label, "CONTRIBUTIONS"), totalLabel)

if len(hourly) == 0 {
    fmt.Fprintln(w, sty.render(sty.muted, "  No activity data"))
    return
}

now := time.Now().Local()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
numDays := 30

// 4 hour bands: 0-5, 6-11, 12-17, 18-23
type hourBand struct {
    label  string
    lo, hi int
}
bands := []hourBand{
    {" 0", 0, 5},
    {" 6", 6, 11},
    {"12", 12, 17},
    {"18", 18, 23},
}

// Grid: [band][day] -> best point
type cell struct {
    value   int
    agentID string
}
grid := make([][]cell, len(bands))
for i := range grid {
    grid[i] = make([]cell, numDays)
}

for _, p := range hourly {
    pt, err := time.ParseInLocation("2006-01-02", p.Date, time.Local)
    if err != nil {
        continue
    }
    dayIdx := int(today.Sub(pt).Hours() / 24)
    col := numDays - 1 - dayIdx
    if col < 0 || col >= numDays {
        continue
    }
    band := -1
    for i, hb := range bands {
        if p.Hour >= hb.lo && p.Hour <= hb.hi {
            band = i
            break
        }
    }
    if band < 0 {
        continue
    }
    if p.Value > grid[band][col].value {
        grid[band][col] = cell{value: p.Value, agentID: p.AgentID}
    }
}

sizeChars := []string{" ", "·", "•", "●", "⬤"}
sizeFor := func(v int) int {
    switch {
    case v == 0:
        return 0
    case v <= 2:
        return 1
    case v <= 5:
        return 2
    case v <= 9:
        return 3
    default:
        return 4
    }
}

labelWidth := 3
availCols := sty.width - labelWidth - 2
colWidth := availCols / numDays
if colWidth < 1 {
    colWidth = 1
}

// Date axis
fmt.Fprint(w, strings.Repeat(" ", labelWidth+1))
startDate := today.AddDate(0, 0, -(numDays - 1))
lastMonth := ""
for d := 0; d < numDays; d++ {
    date := startDate.AddDate(0, 0, d)
    month := date.Format("Jan")
    if month != lastMonth {
        fmt.Fprint(w, sty.render(sty.muted, month))
        labelCols := int(math.Ceil(float64(len(month)) / float64(max(colWidth, 1))))
        if labelCols < 1 {
            labelCols = 1
        }
        d += labelCols - 1
        lastMonth = month
    } else {
        fmt.Fprint(w, strings.Repeat(" ", colWidth))
    }
}
fmt.Fprintln(w)

// Grid rows
for band, hb := range bands {
    fmt.Fprint(w, sty.render(sty.dim, hb.label)+" ")
    for col := range numDays {
        c := grid[band][col]
        sz := sizeFor(c.value)
        if sz == 0 {
            fmt.Fprint(w, strings.Repeat(" ", colWidth))
        } else {
            fmt.Fprint(w, sty.renderAgent(c.agentID, sizeChars[sz])+strings.Repeat(" ", colWidth-1))
        }
    }
    fmt.Fprintln(w)
}

// Size legend
fmt.Fprintln(w, sty.render(sty.dim, "     · 1-2   • 3-5   ● 6-9   ⬤ 10+"))

// Agent legend
if total > 0 {
    var parts []string
    for _, id := range agentOrder {
        count, ok := agentTotals[id]
        if !ok || count == 0 {
            continue
        }
        pct := float64(count) / float64(total) * 100
        display := agentDisplayMap[id]
        parts = append(parts, sty.renderAgent(id, fmt.Sprintf("● %s %d%%", display.Label, int(math.Round(pct)))))
    }
    fmt.Fprintln(w, strings.Join(parts, sty.render(sty.dim, "  ")))
}

}

// renderBrailleChart is an alternative contribution chart using Unicode braille // characters for higher resolution. Swap renderDotChart → renderBrailleChart in // renderContributionChart to enable it. var _ = renderBrailleChart // keep compiled while inactive

func renderBrailleChart(w io.Writer, sty activityStyles, hourly []hourlyPoint, repos []repoContribution) { // Agent breakdown header + total agentTotals := make(map[string]int) total := 0 for _, r := range repos { total += r.Total for agent, count := range r.Agents { agentTotals[agent] += count } }

// Header line: CONTRIBUTIONS  N checkpoints
totalLabel := ""
if total > 0 {
    totalLabel = sty.render(sty.muted, fmt.Sprintf("  %d checkpoints", total))
}
fmt.Fprintf(w, "%s%s\n", sty.render(sty.label, "CONTRIBUTIONS"), totalLabel)

if len(hourly) == 0 {
    fmt.Fprintln(w, sty.render(sty.muted, "  No activity data"))
    return
}

// Determine date range
now := time.Now().Local()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
numDays := 30

labelWidth := 3
chartCols := sty.width - labelWidth - 2
if chartCols < 20 {
    chartCols = 20
}

// 6 braille rows × 4 dots = 24 hours
chartRows := 6
dotsX := chartCols * 2
dotsY := chartRows * 4

type dotInfo struct {
    set     bool
    agentID string
    value   int
}
dotGrid := make([][]dotInfo, dotsY)
for i := range dotGrid {
    dotGrid[i] = make([]dotInfo, dotsX)
}

for _, p := range hourly {
    pt, err := time.ParseInLocation("2006-01-02", p.Date, time.Local)
    if err != nil {
        continue
    }
    dayIdx := int(today.Sub(pt).Hours() / 24)
    dayFromStart := numDays - 1 - dayIdx
    if dayFromStart < 0 || dayFromStart >= numDays {
        continue
    }

dx := int(float64(dayFromStart) / float64(numDays) * float64(dotsX))
    if dx >= dotsX {
        dx = dotsX - 1
    }

dy := p.Hour
    if dy >= dotsY {
        dy = dotsY - 1
    }

// Bubble radius scales with value
    radius := 0
    if p.Value >= 3 {
        radius = 1
    }
    if p.Value >= 8 {
        radius = 2
    }

for oy := -radius; oy <= radius; oy++ {
        for ox := -radius; ox <= radius; ox++ {
            if ox*ox+oy*oy > radius*radius+1 {
                continue
            }
            ny, nx := dy+oy, dx+ox
            if ny >= 0 && ny < dotsY && nx >= 0 && nx < dotsX {
                if p.Value > dotGrid[ny][nx].value {
                    dotGrid[ny][nx] = dotInfo{set: true, agentID: p.AgentID, value: p.Value}
                }
            }
        }
    }
}

// Date axis labels
fmt.Fprint(w, strings.Repeat(" ", labelWidth+1))
startDate := today.AddDate(0, 0, -(numDays - 1))
lastMonth := ""
for col := 0; col < chartCols; col++ {
    dayFrac := float64(col) / float64(chartCols) * float64(numDays)
    date := startDate.AddDate(0, 0, int(dayFrac))
    month := date.Format("Jan")
    if month != lastMonth {
        fmt.Fprint(w, sty.render(sty.muted, month))
        col += len(month) - 1
        lastMonth = month
    } else {
        fmt.Fprint(w, " ")
    }
}
fmt.Fprintln(w)

// Render braille grid
hourLabels := []string{" 0", " 4", " 8", "12", "16", "20"}

for cy := range chartRows {
    fmt.Fprint(w, sty.render(sty.dim, hourLabels[cy])+" ")

for cx := range chartCols {
        var brailleCode rune = 0x2800
        dominantAgent := ""
        maxVal := 0

// Braille dot layout: each char is 2 wide × 4 tall
        //   dot1 dot4     bit0 bit3
        //   dot2 dot5     bit1 bit4
        //   dot3 dot6     bit2 bit5
        //   dot7 dot8     bit6 bit7
        dotBits := [8][2]int{
            {0, 0}, {1, 0}, {2, 0}, {0, 1},
            {1, 1}, {2, 1}, {3, 0}, {3, 1},
        }

for bit, offset := range dotBits {
            dy := cy*4 + offset[0]
            dx := cx*2 + offset[1]
            if dy < dotsY && dx < dotsX && dotGrid[dy][dx].set {
                brailleCode |= 1 << bit
                if dotGrid[dy][dx].value > maxVal {
                    maxVal = dotGrid[dy][dx].value
                    dominantAgent = dotGrid[dy][dx].agentID
                }
            }
        }

if brailleCode == 0x2800 {
            fmt.Fprint(w, " ")
        } else {
            fmt.Fprint(w, sty.renderAgent(dominantAgent, string(brailleCode)))
        }
    }
    fmt.Fprintln(w)
}

}

func renderRepoChart(w io.Writer, sty activityStyles, repos \[\]repoContribution) {
if len(repos) == 0 {
return
}

fmt.Fprintln(w, sty.render(sty.label, "REPOSITORIES"))

maxRepos := 5 if len(repos) < maxRepos { maxRepos = len(repos) } display := repos[:maxRepos]

maxCount := display[0].Total

maxNameLen := 0 for _, r := range display { if len(r.Repo) > maxNameLen { maxNameLen = len(r.Repo) } } if maxNameLen > 30 { maxNameLen = 30 }

countWidth := len(strconv.Itoa(maxCount)) barWidth := sty.width - maxNameLen - countWidth - 4 if barWidth < 10 { barWidth = 10 }

for _, r := range display { name := r.Repo if len(name) > maxNameLen { name = name[:maxNameLen-1] + "…" } name = fmt.Sprintf("%-*s", maxNameLen, name)

bar := renderAgentBar(sty, r.Agents, maxCount, barWidth) count := fmt.Sprintf("%*d", countWidth, r.Total)

fmt.Fprintf(w, "%s %s %s\n", sty.render(sty.repoNm, name), bar, sty.render(sty.muted, count)) }


}

func renderAgentBar(sty activityStyles, agents map\[string\]int, maxCount, barWidth int) string {
if maxCount == 0 {
return strings.Repeat(" ", barWidth)
}

var b strings.Builder

filled := 0 for _, id := range agentOrder { count, ok := agents[id] if !ok || count == 0 { continue } segWidth := int(math.Round(float64(count) / float64(maxCount) * float64(barWidth))) if segWidth < 1 && count > 0 { segWidth = 1 } if filled+segWidth > barWidth { segWidth = barWidth - filled } if segWidth <= 0 { continue }

display := agentDisplayMap[id] seg := strings.Repeat(string(display.Char), segWidth) b.WriteString(sty.renderAgent(id, seg)) filled += segWidth }

if filled < barWidth { b.WriteString(sty.render(sty.dim, strings.Repeat("░", barWidth-filled))) }

return b.String()


}

func renderCommitList(w io.Writer, sty activityStyles, days \[\]commitDay) {
renderCommitListN(w, sty, days, 3)
}

func renderCommitListN(w io.Writer, sty activityStyles, days \[\]commitDay, maxDays int) {
if len(days) == 0 {
return
}

if maxDays <= 0 || maxDays > len(days) { maxDays = len(days) }

for _, day := range days[:maxDays] { displayDate := formatCommitDate(day.Date) commitWord := "commits" if len(day.Commits) == 1 { commitWord = "commit" }

fmt.Fprintf(w, "%s %s\n", sty.render(sty.bold, displayDate), sty.render(sty.muted, fmt.Sprintf("%d %s", len(day.Commits), commitWord)))

for _, c := range day.Commits { sha := c.CommitSHA if len(sha) > 7 { sha = sha[:7] }

msg := "(no message)" if c.CommitMsg != nil && *c.CommitMsg != "" { msg = firstLine(*c.CommitMsg) }

var badges []string for _, a := range uniqueCommitAgents(c) { display := agentDisplayMap[a] badges = append(badges, sty.renderAgent(a, display.Label)) }

fileStats := fmt.Sprintf("%d files", c.FilesChanged) if c.FilesChanged == 1 { fileStats = "1 file" }

cpCount := len(c.Checkpoints) cpStr := "" if cpCount == 1 { cpStr = "1 checkpoint" } else if cpCount > 1 { cpStr = fmt.Sprintf("%d checkpoints", cpCount) }

// Build right-aligned stats: +N / -N M files [K checkpoints] var statParts []string statParts = append(statParts, sty.render(sty.add, fmt.Sprintf("+%d", c.Additions))+ sty.render(sty.muted, " / ")+ sty.render(sty.del, fmt.Sprintf("-%d", c.Deletions))) statParts = append(statParts, sty.render(sty.muted, fileStats)) if cpStr != "" { statParts = append(statParts, sty.render(sty.muted, cpStr)) } rightSide := strings.Join(statParts, sty.render(sty.dim, " ")) // Plain length for padding (ANSI codes don't count) rightPlain := fmt.Sprintf("+%d / -%d %s", c.Additions, c.Deletions, fileStats) if cpStr != "" { rightPlain += " " + cpStr }

// Build left side: hash message repo [badges] left := sty.render(sty.commitH, sha) + " " + sty.render(sty.commitM, msg) + " " + sty.render(sty.muted, c.RepoFullName) leftPlain := sha + " " + msg + " " + c.RepoFullName var leftSb359 strings.Builder for _, badge := range badges { leftSb359.WriteString(" " + badge) } left += leftSb359.String() var leftPlainSb362 strings.Builder for _, a := range uniqueCommitAgents(c) { leftPlainSb362.WriteString(" " + agentDisplayMap[a].Label) } leftPlain += leftPlainSb362.String()

// Truncate message if line would exceed width maxMsg := sty.width - (lipgloss.Width(leftPlain) - lipgloss.Width(msg)) - lipgloss.Width(rightPlain) - 2 if maxMsg < 10 { maxMsg = 10 } if lipgloss.Width(msg) > maxMsg { msg = truncateDisplayWidth(msg, maxMsg, "...") // Rebuild left side with truncated message left = sty.render(sty.commitH, sha) + " " + sty.render(sty.commitM, msg) + " " + sty.render(sty.muted, c.RepoFullName) leftPlain = sha + " " + msg + " " + c.RepoFullName var leftSb378 strings.Builder for _, badge := range badges { leftSb378.WriteString(" " + badge) } left += leftSb378.String() var leftPlainSb381 strings.Builder for _, a := range uniqueCommitAgents(c) { leftPlainSb381.WriteString(" " + agentDisplayMap[a].Label) } leftPlain += leftPlainSb381.String() }

gap := sty.width - lipgloss.Width(leftPlain) - lipgloss.Width(rightPlain) if gap < 2 { gap = 2 } fmt.Fprintf(w, "%s%s%s\n", left, strings.Repeat(" ", gap), rightSide) } fmt.Fprintln(w) }


}

func uniqueCommitAgents(c userCommit) \[\]string {
seen := make(map\[string\]struct{})
var result \[\]string
for \_, cp := range c.Checkpoints {
agents := cp.Agents
// Fall back to singular Agent field when Agents slice is empty
if len(agents) == 0 && cp.Agent != "" {
agents = \[\]string{cp.Agent}
}
for \_, a := range agents {
id := normalizeAgentString(a)
if \_, ok := seen\[id\]; !ok {
seen\[id\] = struct{}{}
result = append(result, id)
}
}
}
sort.Strings(result)
return result
}

func formatCommitDate(dateStr string) string {
t, err := time.ParseInLocation("2006-01-02", dateStr, time.Local)
if err != nil {
return dateStr
}
now := time.Now().Local()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
days := int(today.Sub(t).Hours() / 24)

switch days { case 0: return t.Format("Monday 2 Jan") + " (today)" case 1: return t.Format("Monday 2 Jan") + " (yesterday)" default: return t.Format("Monday 2 Jan") }


}

func padOrTruncate(s string, width int) string {
runes := \[\]rune(s)
if len(runes) > width {
return string(runes\[:width-1\]) + "…"
}
return s + strings.Repeat(" ", width-len(runes))
}

func truncateDisplayWidth(s string, width int, tail string) string {
if lipgloss.Width(s) <= width {
return s
}
if width <= 0 {
return ""
}
if lipgloss.Width(tail) >= width {
return tail\[:width\]
}

var b strings.Builder remaining := width - lipgloss.Width(tail) for _, r := range s { if lipgloss.Width(b.String()+string(r)) > remaining { break } b.WriteRune(r) } return b.String() + tail


}

\-\-\- cmd/entire/cli/activity\_tui.go
package cli

import (
"bytes"
"context"
"fmt"
"os"
"strings"

"charm.land/bubbles/v2/key" "charm.land/bubbles/v2/spinner" "charm.land/bubbles/v2/viewport" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/entireio/cli/cmd/entire/cli/api"


)

// activityDataMsg is sent when API data has been fetched.
type activityDataMsg struct {
stats contributionStats
repos \[\]repoContribution
hourly \[\]hourlyPoint
days \[\]commitDay
}

// activityErrMsg is sent when fetching fails.
type activityErrMsg struct{ err error }

type activityModel struct {
// Data (nil until loaded)
stats \*contributionStats
repos \[\]repoContribution
hourly \[\]hourlyPoint
days \[\]commitDay

// Loading state loading bool loadErr error spinner spinner.Model

// Fetch context ctx context.Context client *api.Client

// View state viewport viewport.Model sty activityStyles useColor bool width int height int ready bool


}

func runActivityTUI(ctx context.Context, client \*api.Client) error {
sp := spinner.New()
sp.Spinner = spinner.Dot
sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))

m := activityModel{ loading: true, spinner: sp, ctx: ctx, client: client, useColor: shouldUseColor(os.Stdout), } p := tea.NewProgram(m) if _, err := p.Run(); err != nil { return fmt.Errorf("activity TUI: %w", err) } return nil


}

func (m activityModel) fetchData() tea.Msg { //nolint:ireturn // bubbletea Cmd signature requires tea.Msg return
activity, commits, err := fetchActivityData(m.ctx, m.client)
if err != nil {
return activityErrMsg{err: err}
}

return activityDataMsg{ stats: contributionStats{ Tasks: activity.Stats.Tasks, Throughput: activity.Stats.Throughput, Iteration: activity.Stats.Iteration, ContinuityH: activity.Stats.ContinuityHours, Streak: activity.Stats.LifetimeStreak, CurrentStreak: activity.Stats.LifetimeCurrentStreak, }, repos: activity.Repos, hourly: activity.HourlyContributions, days: groupCommitsByDay(commits), }


}

func (m activityModel) Init() tea.Cmd {
return tea.Batch(m.spinner.Tick, m.fetchData)
}

func (m activityModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case activityDataMsg:
m.loading = false
m.stats = &msg.stats
m.repos = msg.repos
m.hourly = msg.hourly
m.days = msg.days
if m.width > 0 {
m = m.withViewport()
}
return m, nil

case activityErrMsg: m.loading = false m.loadErr = msg.err return m, nil

case tea.KeyPressMsg: if key.Matches(msg, keys.Quit) || key.Matches(msg, keys.Back) { return m, tea.Quit } if key.Matches(msg, keys.Home) { if m.ready { m.viewport.GotoTop() } return m, nil } if key.Matches(msg, keys.End) { if m.ready { m.viewport.GotoBottom() } return m, nil }

case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height m.sty = newActivityStylesWithWidth(m.width, m.useColor) if m.stats != nil { m = m.withViewport() } return m, nil

case spinner.TickMsg: if m.loading { var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd } }

if m.ready { var cmd tea.Cmd m.viewport, cmd = m.viewport.Update(msg) return m, cmd }

return m, nil


}

func (m activityModel) withViewport() activityModel {
headerHeight := m.headerLineCount()
vpHeight := m.height - headerHeight - 1
if vpHeight < 1 {
vpHeight = 1
}

if !m.ready { m.viewport = viewport.New(viewport.WithWidth(m.width), viewport.WithHeight(vpHeight)) m.ready = true } else { m.viewport.SetWidth(m.width) m.viewport.SetHeight(vpHeight) } m.viewport.SetContent(m.renderCommits()) return m


}

func (m activityModel) View() tea.View {
v := tea.View{AltScreen: true}
if m.loadErr != nil {
v.SetContent(fmt.Sprintf("\\n Failed to load activity: %s\\n\\n Press q to quit.\\n", m.loadErr))
return v
}

if m.loading { v.SetContent(fmt.Sprintf("\n %s Loading activity...\n", m.spinner.View())) return v }

if !m.ready { return v }

var b strings.Builder b.WriteString(m.renderHeader()) b.WriteString(m.viewport.View()) b.WriteString("\n") b.WriteString(m.renderFooter()) v.SetContent(b.String()) return v


}

func (m activityModel) renderHeader() string {
if m.stats == nil {
return ""
}
var buf bytes.Buffer
buf.WriteString("\\n")
renderStatCards(&buf, m.sty, \*m.stats)
buf.WriteString("\\n")
renderContributionChart(&buf, m.sty, m.hourly, m.repos)
buf.WriteString("\\n")
renderRepoChart(&buf, m.sty, m.repos)
buf.WriteString("\\n")
return buf.String()
}

func (m activityModel) headerLineCount() int {
return strings.Count(m.renderHeader(), "\\n")
}

func (m activityModel) renderCommits() string {
var buf bytes.Buffer
renderCommitListN(&buf, m.sty, m.days, -1)
return buf.String()
}

func (m activityModel) renderFooter() string {
if !m.sty.colorEnabled {
return ""
}
helpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Bold(true)
sep := helpStyle.Render(" · ")

fullHelp := keyStyle.Render("↑/↓, j/k") + helpStyle.Render(" scroll") + sep + keyStyle.Render("home/end, g/G") + helpStyle.Render(" top/bottom") + sep + keyStyle.Render(keys.Quit.Help().Key) + helpStyle.Render(" "+keys.Quit.Help().Desc) standardHelp := keyStyle.Render("↑/↓") + helpStyle.Render(" scroll") + sep + keyStyle.Render("home/end") + helpStyle.Render(" top/bottom") + sep + keyStyle.Render(keys.Quit.Help().Key) + helpStyle.Render(" "+keys.Quit.Help().Desc) shortHelp := keyStyle.Render("↑/↓") + helpStyle.Render(" scroll") quitHelp := keyStyle.Render(keys.Quit.Help().Key) + helpStyle.Render(" "+keys.Quit.Help().Desc) helpChoices := []string{fullHelp, standardHelp, shortHelp, quitHelp}

if m.viewport.TotalLineCount() <= m.viewport.Height() { for _, help := range helpChoices { if m.width <= 0 || lipgloss.Width(help) <= m.width { return help } } return "" }

pct := helpStyle.Render(padLeft(int(m.viewport.ScrollPercent()*100)) + "%") for _, help := range helpChoices { gap := m.width - lipgloss.Width(help) - lipgloss.Width(sep) - lipgloss.Width(pct) if gap >= 1 { return help + sep + helpStyle.Render(strings.Repeat(" ", gap)) + pct } }

pctWidth := lipgloss.Width(pct) if m.width < pctWidth { return helpStyle.Render(strings.Repeat(" ", max(m.width, 0))) } return helpStyle.Render(strings.Repeat(" ", m.width-pctWidth)) + pct


}

func newActivityStylesWithWidth(width int, useColor bool) activityStyles {
return activityStyles{
colorEnabled: useColor,
width: width,
bold: lipgloss.NewStyle().Bold(true),
dim: lipgloss.NewStyle().Faint(true),
label: lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Bold(true),
value: lipgloss.NewStyle().Bold(true),
unit: lipgloss.NewStyle().Foreground(lipgloss.Color("8")),
desc: lipgloss.NewStyle().Foreground(lipgloss.Color("8")),
repoNm: lipgloss.NewStyle().Foreground(lipgloss.Color("7")),
commitH: lipgloss.NewStyle().Foreground(lipgloss.Color("8")),
commitM: lipgloss.NewStyle().Bold(true),
add: lipgloss.NewStyle().Foreground(lipgloss.Color("2")),
del: lipgloss.NewStyle().Foreground(lipgloss.Color("1")),
muted: lipgloss.NewStyle().Foreground(lipgloss.Color("8")),
}
}

func padLeft(n int) string {
s := strings.Builder{}
if n < 10 {
s.WriteString(" ")
} else if n < 100 {
s.WriteString(" ")
}
fmt.Fprintf(&s, "%d", n)
return s.String()
}

\-\-\- cmd/entire/cli/activity\_types.go
package cli

// API response types for the /api/v1/me/\* endpoints used by `entire activity`.

// activityAgentCounts maps the 11 canonical agent IDs to counts.
// The API always populates every key (zero for absent agents).
type activityAgentCounts map\[string\]int

// userActivityResponse is the API response for GET /api/v1/me/activity.
type userActivityResponse struct {
Stats activityStatsResponse `json:"stats"`
HourlyContributions \[\]hourlyPoint `json:"hourly_contributions"`
Repos \[\]repoContribution `json:"repos"`
// DailyContributions is returned but unused by the CLI.
}

type activityStatsResponse struct {
Tasks int `json:"tasks"`
Orchestration int `json:"orchestration"` // 0-100, percentage
Iteration float64 `json:"iteration"`
Throughput float64 `json:"throughput"`
ContinuityHours float64 `json:"continuity_hours"`
Streak int `json:"streak"` // scoped to timeframe
CurrentStreak int `json:"current_streak"` // scoped to timeframe
LifetimeStreak int `json:"lifetime_streak"` // last 365 days
LifetimeCurrentStreak int `json:"lifetime_current_streak"` // last 365 days
}

// userCommitCheckpoint is checkpoint info nested inside a commit.
type userCommitCheckpoint struct {
CheckpointID string `json:"checkpoint_id"`
Prompt \*string `json:"prompt"`
Agent string `json:"agent"`
Agents \[\]string `json:"agents"`
SessionCount int `json:"session_count"`
TotalSteps int `json:"total_steps"`
}

// userCommit represents a single commit returned by the commits API.
type userCommit struct {
CommitSHA string `json:"commit_sha"`
CommitMsg \*string `json:"commit_message"`
CommitAuthorUsername \*string `json:"commit_author_username"`
CommitDate \*string `json:"commit_date"`
Additions int `json:"additions"`
Deletions int `json:"deletions"`
FilesChanged int `json:"files_changed"`
Checkpoints \[\]userCommitCheckpoint `json:"checkpoints"`
RepoFullName string `json:"repo_full_name"`
IsPrivate bool `json:"is_private"`
CheckpointRepoFullName \*string `json:"checkpoint_repo_full_name"`
}

// userCommitsResponse is the API response for GET /api/v1/me/commits.
type userCommitsResponse struct {
Commits \[\]userCommit `json:"commits"`
Timeframe string `json:"timeframe"`
UpdatedAt string `json:"updated_at"`
}

// Computed types used for rendering.

type contributionStats struct {
Tasks int
Throughput float64 // avg tokens/checkpoint in thousands
Iteration float64 // avg session\_count per checkpoint
ContinuityH float64 // peak session length in hours (max(steps)\*2/60)
Streak int // longest consecutive days (last 365)
CurrentStreak int // current streak ending today (last 365)
}

// repoContribution matches the API's `repos[]` shape. Agents is keyed by the
// canonical agent ID (claude, gemini, …, unknown) with all 11 keys populated.
type repoContribution struct {
Repo string `json:"repo"`
Total int `json:"total"`
Agents activityAgentCounts `json:"agents"`
}

// hourlyPoint matches the API's `hourly_contributions[]` shape. AgentID is a
// canonical ID (no client-side normalization needed).
type hourlyPoint struct {
Date string `json:"date"` // "2006-01-02", in the caller's timezone
Hour int `json:"hour"`
AgentID string `json:"agent"`
Value int `json:"value"`
}

// commitDay groups commits by date for display.
type commitDay struct {
Date string
Commits \[\]userCommit
}

\-\-\- cmd/entire/cli/agent\_group.go
package cli

import (
"context"
"errors"
"fmt"
"io"

"github.com/entireio/cli/cmd/entire/cli/agent" "github.com/entireio/cli/cmd/entire/cli/agent/types" "github.com/entireio/cli/cmd/entire/cli/paths" "github.com/entireio/cli/cmd/entire/cli/settings" "github.com/spf13/cobra"


)

// newAgentGroupCmd builds `entire agent`. Replaces `entire configure`.
func newAgentGroupCmd() \*cobra.Command {
cmd := &cobra.Command{
Use: "agent",
Short: "Manage agent integrations (add, remove, list)",
Long: \`Manage agent integrations in this repository.

Commands:
list Show installed and available agents
add Install hooks for an agent
remove Uninstall hooks for an agent

Examples:
entire agent
entire agent list
entire agent add claude-code
entire agent remove claude-code\`,
PersistentPreRunE: func(cmd \*cobra.Command, \_ \[\]string) error {
if \_, err := paths.WorktreeRoot(cmd.Context()); err != nil {
return errors.New("not a git repository")
}
return nil
},
RunE: func(cmd \*cobra.Command, \_ \[\]string) error {
return runAgentMenu(cmd.Context(), cmd.OutOrStdout())
},
}

cmd.AddCommand(newAgentListCmd()) cmd.AddCommand(newAgentAddCmd()) cmd.AddCommand(newAgentRemoveCmd()) return cmd


}

func runAgentMenu(ctx context.Context, w io.Writer) error {
opts := EnableOptions{Telemetry: true}
if settings.IsSetUpAny(ctx) {
return runManageAgents(ctx, w, opts, nil)
}
return runSetupFlow(ctx, w, opts)
}

func newAgentListCmd() \*cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List installed and available agents",
RunE: func(cmd \*cobra.Command, \_ \[\]string) error {
return runAgentList(cmd.Context(), cmd.OutOrStdout())
},
}
}

func runAgentList(ctx context.Context, w io.Writer) error {
installed := GetAgentsWithHooksInstalled(ctx)
installedSet := make(map\[types.AgentName\]struct{}, len(installed))
for \_, name := range installed {
installedSet\[name\] = struct{}{}
}

all := agent.StringList()

fmt.Fprintln(w, "Agents:") for _, name := range all { marker := " " if _, ok := installedSet[types.AgentName(name)]; ok { marker = "✓ " } fmt.Fprintf(w, " %s%s\n", marker, name) } if len(installed) == 0 { fmt.Fprintln(w, "\nNo agents installed. Use 'entire agent add ' to install hooks.") } return nil


}

func newAgentAddCmd() \*cobra.Command {
var localDev bool
var forceHooks bool

cmd := &cobra.Command{ Use: "add ", Short: "Install hooks for an agent", Long: `Install hooks for the specified agent in this repository.


Examples:
entire agent add claude-code
entire agent add gemini\`,
Args: cobra.ExactArgs(1),
RunE: func(cmd \*cobra.Command, args \[\]string) error {
name := args\[0\]
ag, err := agent.Get(types.AgentName(name))
if err != nil {
printWrongAgentError(cmd.OutOrStdout(), name)
return NewSilentError(errors.New("wrong agent name"))
}
opts := EnableOptions{
LocalDev: localDev,
ForceHooks: forceHooks,
Telemetry: true,
}
return setupAgentHooksNonInteractive(cmd.Context(), cmd.OutOrStdout(), ag, opts)
},
}

cmd.Flags().BoolVar(&localDev, "local-dev", false, "Install hooks in local-dev mode") cmd.Flags().BoolVar(&forceHooks, "force", false, "Reinstall hooks even if already present") return cmd


}

func newAgentRemoveCmd() \*cobra.Command {
return &cobra.Command{
Use: "remove <agent-name>",
Short: "Uninstall hooks for an agent",
Long: \`Uninstall hooks for the specified agent in this repository.

Examples:
entire agent remove claude-code\`,
Args: cobra.ExactArgs(1),
RunE: func(cmd \*cobra.Command, args \[\]string) error {
return runRemoveAgent(cmd.Context(), cmd.OutOrStdout(), args\[0\])
},
}
}

\-\-\- cmd/entire/cli/aliascmd.go
package cli

import "github.com/spf13/cobra"

// hideAsAlias marks cmd as a hidden top-level shortcut that prints a one-line
// hint pointing at the canonical command. Cobra's Deprecated field renders the
// hint to stderr on every invocation while keeping the command functional.
func hideAsAlias(cmd \*cobra.Command, canonical string) \*cobra.Command {
cmd.Hidden = true
cmd.Deprecated = "use '" + canonical + "' instead"
return cmd
}

\-\-\- cmd/entire/cli/api\_client.go
package cli

import (
"errors"
"fmt"

"github.com/entireio/cli/cmd/entire/cli/api" "github.com/entireio/cli/cmd/entire/cli/auth"


)

// NewAuthenticatedAPIClient creates an API client using the bearer token
// from the CLI login flow. Returns an error if the user is not logged in.
// Pass insecureHTTP=true to allow plain HTTP base URLs (for local development).
func NewAuthenticatedAPIClient(insecureHTTP bool) (\*api.Client, error) {
token, err := auth.LookupCurrentToken()
if err != nil {
return nil, fmt.Errorf("lookup auth token: %w", err)
}
if token == "" {
return nil, errors.New("not logged in (run 'entire login' first)")
}

if !insecureHTTP { if err := api.RequireSecureURL(api.BaseURL()); err != nil { return nil, fmt.Errorf("base URL check: %w", err) } } return api.NewClient(token), nil


}

\-\-\- cmd/entire/cli/attach.go
package cli

import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"strings"
"time"

"github.com/entireio/cli/cmd/entire/cli/agent" "github.com/entireio/cli/cmd/entire/cli/agent/external" "github.com/entireio/cli/cmd/entire/cli/agent/geminicli" "github.com/entireio/cli/cmd/entire/cli/agent/types" cpkg "github.com/entireio/cli/cmd/entire/cli/checkpoint" "github.com/entireio/cli/cmd/entire/cli/checkpoint/id" "github.com/entireio/cli/cmd/entire/cli/checkpoint/remote" "github.com/entireio/cli/cmd/entire/cli/interactive" "github.com/entireio/cli/cmd/entire/cli/logging" "github.com/entireio/cli/cmd/entire/cli/paths" "github.com/entireio/cli/cmd/entire/cli/session" "github.com/entireio/cli/cmd/entire/cli/settings" "github.com/entireio/cli/cmd/entire/cli/strategy" "github.com/entireio/cli/cmd/entire/cli/trailers" "github.com/entireio/cli/cmd/entire/cli/transcript/compact" "github.com/entireio/cli/cmd/entire/cli/validation" "github.com/entireio/cli/cmd/entire/cli/versioninfo" "github.com/entireio/cli/perf" "github.com/entireio/cli/redact"

"charm.land/huh/v2" "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/spf13/cobra"


)

// attachOptions carries optional flags for runAttach. Force is the original
// flag; Review opts the attach into recording the session as an
// agent\_review in the checkpoint metadata.
type attachOptions struct {
Force bool
// Review, when true, tags the attached session as a review. Skills are
// resolved inside runAttach after the real agent is known (via session
// state or transcript auto-detection), not at the cobra layer — the
// --agent flag's default points at claude-code, which would otherwise
// make a Gemini session incorrectly look up review.claude-code config.
Review bool
// ReviewSkillsOverride, when non-empty, declares which review skills were
// run. Empty is valid: the session is still tagged as a review, with no
// structured skills list. Ignored when Review=false.
ReviewSkillsOverride \[\]string
// ReviewPromptOverride, when non-empty, is recorded instead of the
// transcript's first user prompt. Used by `entire review attach` when a
// pending-review marker has the exact prompt the user was asked to run.
ReviewPromptOverride string
}

func newAttachCmd() \*cobra.Command {
var (
force bool
agentFlag string
reviewFlag bool
skillsFlag \[\]string
)
cmd := &cobra.Command{
Use: "attach <session-id>",
Short: "Attach an existing agent session",
Long: \`Attach an existing agent session that wasn't captured by hooks.

This creates a checkpoint from the session's transcript and links it to the
last commit. Use this when hooks failed to fire or weren't installed when
the session started, or to attach a research session.

If the last commit already has a checkpoint, the session is added to it.
Otherwise a new checkpoint is created.

Use --review to tag the attached session as an agent review. The
first user prompt in the transcript is recorded as the review prompt.
Pass --skills to declare which skills were actually run; omit to
attach a review without a declared skills list.

Works with any registered agent, including external agents enabled via
external\_agents in settings. Run 'entire agent list' to see the full list.\`,
RunE: func(cmd \*cobra.Command, args \[\]string) error {
if len(args) != 1 {
return cmd.Help()
}
if checkDisabledGuard(cmd.Context(), cmd.OutOrStdout()) {
return nil
}
// Discover external agents so --agent <external-name> is recognized
// and so auto-detection can find transcripts from external agents.
external.DiscoverAndRegister(cmd.Context())
agentName := types.AgentName(agentFlag)
opts := attachOptions{
Force: force,
Review: reviewFlag,
ReviewSkillsOverride: skillsFlag,
}
return runAttachSurfaceReviewErrors(cmd, args\[0\], agentName, opts)
},
}
cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation and amend the last commit with the checkpoint trailer")
cmd.Flags().StringVarP(&agentFlag, "agent", "a", string(agent.DefaultAgentName), "Agent that created the session (see 'entire agent list' for registered agents, including external)")
cmd.Flags().BoolVar(&reviewFlag, "review", false, "Tag the attached session as an agent review")
cmd.Flags().StringSliceVar(&skillsFlag, "skills", nil, "Optional: declare which review skills were run in this session. Only used with --review")
return cmd
}

// resolveReviewSkills returns the skills list to record on an
// attach-as-review. Only the user's --skills flag counts: configured
// settings.Review\[agent\] is the spawn-path default ("what I'd run if I
// used 'entire review'"), not a claim about what actually happened in a
// given manual session. Silently attaching configured skills would
// misrepresent the session as having run skills it may not have.
//
// Empty is a valid result — the attach still tags the session as a
// review via Kind + ReviewPrompt (the session's first user prompt). The
// skills list is a queryable convenience, not the source of truth.
func resolveReviewSkills(flagSkills \[\]string) \[\]string {
if len(flagSkills) == 0 {
return nil
}
return flagSkills
}

// runAttachSurfaceReviewErrors wraps runAttach so review-mode errors reach
// the user as clear stderr messages rather than generic cobra error output.
// The non-review path preserves the existing runAttach return-err behavior.
func runAttachSurfaceReviewErrors(cmd \*cobra.Command, sessionID string, agentName types.AgentName, opts attachOptions) error {
err := runAttach(cmd.Context(), cmd.OutOrStdout(), sessionID, agentName, opts)
if err != nil && opts.Review {
cmd.SilenceUsage = true
fmt.Fprintln(cmd.ErrOrStderr(), err.Error())
return NewSilentError(err)
}
return err
}

func runAttach(ctx context.Context, w io.Writer, sessionID string, agentName types.AgentName, opts attachOptions) error {
// Initialize structured logger so logging.Warn/Info write to .entire/logs/ not stderr.
if err := logging.Init(ctx, sessionID); err != nil {
// Init failed — logging will use stderr fallback, non-fatal.
\_ = err
}
// Flush the 8KB buffered log writer on exit. Without this, any
// Warn/Info calls during attach (including the overwrite tripwire)
// get silently dropped when the process exits, matching the pattern
// already used by resume/clean/reset/rewind/migrate/explain.
defer logging.Close()

logCtx := logging.WithComponent(ctx, "attach")

// Open repository once — shared across all operations. repo, err := openRepository(ctx) if err != nil { return err }

existingState, err := validateAttachPreconditions(ctx, repo, sessionID) if err != nil { return err }

headCommit, err := getHeadCommit(repo) if err != nil { return err }

// If session already has a checkpoint, just offer to link it. if existingState != nil && !existingState.LastCheckpointID.IsEmpty() { // Review-upgrade isn't supported yet: the existing checkpoint's // metadata tree would need to be rewritten with Kind/ReviewSkills/ // ReviewPrompt set, and a new commit pushed onto entire/checkpoints/v1. // Error out with a concrete message rather than silently linking the // checkpoint without the review metadata. if opts.Review { return fmt.Errorf( "session %s already has checkpoint %s; rewriting an existing checkpoint as a review is not supported yet", sessionID, existingState.LastCheckpointID.String(), ) } cpID := existingState.LastCheckpointID.String() fmt.Fprintf(w, "Session %s already has checkpoint %s\n", sessionID, cpID) if err := promptAmendCommit(logCtx, w, headCommit, cpID, opts.Force); err != nil { logging.Warn(logCtx, "failed to amend commit", "error", err) fmt.Fprintf(w, "\nCopy to your commit message to attach:\n\n Entire-Checkpoint: %s\n", cpID) } return nil }

// Resolve agent and transcript path. ag, transcriptPath, err := resolveAgentAndTranscript(logCtx, w, sessionID, agentName, existingState) if err != nil { return err }

var reviewSkills []string if opts.Review { reviewSkills = resolveReviewSkills(opts.ReviewSkillsOverride) }

transcriptData, err := ag.ReadTranscript(transcriptPath) if err != nil { return fmt.Errorf("failed to read transcript: %w", err) }

// Normalize Gemini transcripts for storage. storedTranscript := transcriptData if ag.Type() == agent.AgentTypeGemini { if normalized, normErr := geminicli.NormalizeTranscript(transcriptData); normErr == nil { storedTranscript = normalized } else { logging.Warn(logCtx, "failed to normalize Gemini transcript, storing raw", "error", normErr) } }

meta := extractTranscriptMetadata(transcriptData)

// Determine checkpoint ID: reuse from HEAD if one exists, otherwise generate new. checkpointID, isExistingCheckpoint := resolveCheckpointID(headCommit)

// If HEAD references an existing checkpoint, make sure we have it locally // before writing — otherwise we'd create a fresh session 0 under the same // ID and overwrite the original on push. repo, err = ensureCheckpointAvailable(ctx, logCtx, repo, checkpointID, isExistingCheckpoint) if err != nil { return err }

// Write directly to entire/checkpoints/v1. store := cpkg.NewGitStore(repo)

// Defense-in-depth guard: the earlier existingState.LastCheckpointID // check only fires when the session's state file records its // checkpoint. A session already stored in the HEAD checkpoint but // whose state is missing/stale (state file deleted, never written, // condensed without LastCheckpointID update, or pulled from a remote // that wasn't reflected locally) would bypass that guard. // findSessionIndex matches by SessionID — without this check, a // review-attach on such a session silently overwrites the existing // session's metadata in the checkpoint. if opts.Review && isExistingCheckpoint { if existing, readErr := store.ReadSessionContentByID(ctx, checkpointID, sessionID); readErr == nil && existing != nil { return fmt.Errorf( "session %s is already recorded in checkpoint %s; rewriting an existing checkpoint as a review is not supported yet", sessionID, checkpointID.String(), ) } }

author, err := GetGitAuthor(ctx) if err != nil { return fmt.Errorf("failed to get git author: %w", err) }

var prompts []string if meta.FirstPrompt != "" { prompts = []string{meta.FirstPrompt} }

tokenUsage := agent.CalculateTokenUsage(logCtx, ag, transcriptData, 0, "")

_, redactSpan := perf.Start(ctx, "redact_transcript") redactedTranscript, redactErr := redact.JSONLBytes(storedTranscript) redactSpan.End() if redactErr != nil { return fmt.Errorf("failed to redact transcript: %w", redactErr) }

writeOpts := cpkg.WriteCommittedOptions{ CheckpointID: checkpointID, SessionID: sessionID, Strategy: strategy.StrategyNameManualCommit, Transcript: redactedTranscript, Prompts: prompts, AuthorName: author.Name, AuthorEmail: author.Email, Agent: ag.Type(), Model: meta.Model, TokenUsage: tokenUsage, } if opts.Review { writeOpts.Kind = string(session.KindAgentReview) writeOpts.ReviewSkills = reviewSkills writeOpts.ReviewPrompt = reviewPromptForAttach(meta, opts) writeOpts.HasReview = true }

if compacted := compactTranscriptForStartLine(logCtx, redactedTranscript.Bytes(), cpkg.CommittedMetadata{ CheckpointID: checkpointID, Agent: ag.Type(), }, 0); compacted != nil { writeOpts.CompactTranscript = compacted }

v2 := settings.CheckpointsVersion(logCtx) == 2 if !v2 { if err := store.WriteCommitted(ctx, writeOpts); err != nil { return fmt.Errorf("failed to write checkpoint: %w", err) } } // IsCheckpointsV2Enabled is true whenever v2 writes are enabled, including // both v2-only mode (checkpoints_version == 2) and dual-write mode. Only // v2-only mode propagates the error. if settings.IsCheckpointsV2Enabled(logCtx) { if err := writeAttachCheckpointV2(logCtx, repo, writeOpts); err != nil { if v2 { return fmt.Errorf("failed to write checkpoint to v2: %w", err) } logging.Warn(logCtx, "attach v2 dual-write failed", "error", err) } }

// Create or update session state. if err := saveAttachSessionState(logCtx, repo, existingState, sessionID, ag.Type(), transcriptPath, checkpointID, meta, tokenUsage, opts, reviewSkills); err != nil { logging.Warn(logCtx, "failed to save session state", "error", err) }

fmt.Fprintf(w, "Attached session %s\n", sessionID) if isExistingCheckpoint { fmt.Fprintf(w, " Added to existing checkpoint %s\n", checkpointID) return nil }

fmt.Fprintf(w, " Created checkpoint %s\n", checkpointID) cpIDStr := checkpointID.String() if err := promptAmendCommit(logCtx, w, headCommit, cpIDStr, opts.Force); err != nil { logging.Warn(logCtx, "failed to amend commit", "error", err) fmt.Fprintf(w, "\nCopy to your commit message to attach:\n\n Entire-Checkpoint: %s\n", cpIDStr) }

return nil


}

// writeAttachCheckpointV2 writes attach-created checkpoints into the v2 refs.
func writeAttachCheckpointV2(ctx context.Context, repo \*git.Repository, opts cpkg.WriteCommittedOptions) error {
v2URL, err := remote.FetchURL(ctx)
if err != nil {
logging.Debug(ctx, "attach: using origin for v2 store fetch remote",
slog.String("error", err.Error()),
)
}
v2Store := cpkg.NewV2GitStore(repo, v2URL)
if err := v2Store.WriteCommitted(ctx, opts); err != nil {
return fmt.Errorf("v2 write committed: %w", err)
}
return nil
}

// getHeadCommit returns the HEAD commit object.
func getHeadCommit(repo \*git.Repository) (\*object.Commit, error) {
headRef, err := repo.Head()
if err != nil {
return nil, fmt.Errorf("failed to get HEAD: %w", err)
}
commit, err := repo.CommitObject(headRef.Hash())
if err != nil {
return nil, fmt.Errorf("failed to get HEAD commit: %w", err)
}
return commit, nil
}

// ensureCheckpointAvailable makes sure the checkpoint referenced by HEAD is
// present locally before the attach writes to it. Without this guard, attach
// would create a fresh session 0 under the same ID and overwrite the original
// session data on push.
//
// Only the local branch counts — remote-tracking presence is not enough.
// If only the remote-tracking ref exists, a subsequent WriteCommitted creates
// a brand-new orphan local branch with an empty tree, which would clobber
// the remote on push.
//
// Fast path: check local refs directly — no network. If missing, trigger the
// metadata fetch fallback chain used by `entire resume` (which advances the
// local ref on success) and re-check. Returns a possibly-freshly-opened repo
// handle so go-git sees any newly fetched packfiles.
func ensureCheckpointAvailable(ctx, logCtx context.Context, repo \*git.Repository, checkpointID id.CheckpointID, isExistingCheckpoint bool) (\*git.Repository, error) {
if !isExistingCheckpoint {
return repo, nil
}

v2Only := settings.CheckpointsVersion(logCtx) == 2

present, readErr := checkpointPresentLocally(ctx, repo, checkpointID, v2Only) if readErr != nil { return repo, fmt.Errorf("failed to read checkpoint %s: %w", checkpointID, readErr) } if present { return repo, nil }

// Missing locally — try to refresh, then re-check. Use the same fetch // chain entire resume uses for the active storage version (v2 refs live // under refs/entire/, not refs/heads/, so v1 and v2 need different // refspecs). freshRepo, fetchErr := refreshCheckpointRefs(ctx, v2Only) if fetchErr != nil { logging.Warn(logCtx, "failed to refresh metadata branch before attach; proceeding with local state", slog.String("error", fetchErr.Error())) } else { repo = freshRepo present, readErr = checkpointPresentLocally(ctx, repo, checkpointID, v2Only) if readErr != nil { return repo, fmt.Errorf("failed to read checkpoint %s after refresh: %w", checkpointID, readErr) } if present { return repo, nil } }

branchDescription := "entire/checkpoints/v1 branch" if v2Only { branchDescription = "v2 /main ref" } return repo, fmt.Errorf( "checkpoint %s referenced by HEAD is missing from the local %s after a refresh attempt. Creating a fresh checkpoint here would overwrite the original session data on push. Run:\n\n %s\n\nthen re-run attach. If the colleague who made this commit hasn't pushed their checkpoint metadata yet, ask them to do so first", checkpointID.String(), branchDescription, suggestCheckpointFetchCommand(logCtx, v2Only), )


}

// refreshCheckpointRefs runs the resume-equivalent fetch chain for the storage
// version we're about to write to. Returns a freshly-opened repo so go-git
// sees any newly-fetched packfiles and ref updates.
func refreshCheckpointRefs(ctx context.Context, v2Only bool) (\*git.Repository, error) {
if v2Only {
\_, repo, err := getV2MetadataTree(ctx)
return repo, err
}
\_, repo, err := getMetadataTree(ctx)
return repo, err
}

// checkpointPresentLocally reports whether the checkpoint already exists on
// the local ref we would write to. For v1 / dual-write, that's the local
// entire/checkpoints/v1 branch (remote-tracking alone is not enough — see
// ensureCheckpointAvailable). For v2-only mode, it's the v2 /main ref, which
// has no remote-tracking analog and is therefore already local-only by
// construction.
func checkpointPresentLocally(ctx context.Context, repo \*git.Repository, checkpointID id.CheckpointID, v2Only bool) (bool, error) {
if v2Only {
v2URL, urlErr := remote.FetchURL(ctx)
if urlErr != nil {
logging.Debug(ctx, "attach: using origin for v2 store fetch remote",
slog.String("error", urlErr.Error()),
)
}
summary, err := cpkg.NewV2GitStore(repo, v2URL).ReadCommitted(ctx, checkpointID)
if err != nil {
return false, err //nolint:wrapcheck // Caller wraps with checkpoint ID context
}
return summary != nil, nil
}

localRef := plumbing.NewBranchReferenceName(paths.MetadataBranchName) if _, err := repo.Reference(localRef, true); err != nil { // Local branch ref doesn't exist — treat as "not present locally". // We deliberately do not fall back to remote-tracking: see // ensureCheckpointAvailable's docstring. return false, nil //nolint:nilerr // Missing ref is the "absent" signal, not an error. } summary, err := cpkg.NewGitStore(repo).ReadCommitted(ctx, checkpointID) if err != nil { return false, err //nolint:wrapcheck // Caller wraps with checkpoint ID context } return summary != nil, nil


}

// suggestCheckpointFetchCommand returns a git fetch command the user can
// paste to pull the missing metadata ref. v2 refs live under refs/entire/
// (not refs/heads/), so they need an explicit fully-qualified refspec;
// v1 lives on a regular branch and its short name is enough.
func suggestCheckpointFetchCommand(ctx context.Context, v2Only bool) string {
ref := "entire/checkpoints/v1:entire/checkpoints/v1"
if v2Only {
ref = paths.V2MainRefName + ":" + paths.V2MainRefName
}
if remote.Configured(ctx) {
if url, err := remote.FetchURL(ctx); err == nil && url != "" {
return fmt.Sprintf("git fetch %s %s", url, ref)
}
}
return "git fetch origin " + ref
}

func resolveCheckpointID(headCommit \*object.Commit) (id.CheckpointID, bool) {
existing := trailers.ParseAllCheckpoints(headCommit.Message)
if len(existing) > 0 {
return existing\[len(existing)-1\], true
}

cpID, err := id.Generate() if err != nil { // Generation only fails if crypto/rand fails — extremely unlikely. // Fall back to empty which will cause WriteCommitted to fail with a clear error. return id.EmptyCheckpointID, false } return cpID, false


}

// saveAttachSessionState creates or updates the session state file for the attached session.
// If existingState is non-nil, it is updated in place (avoids a redundant disk load).
// reviewSkills is the resolved skills list when opts.Review is true; ignored otherwise.
func saveAttachSessionState(ctx context.Context, repo \*git.Repository, existingState \*session.State, sessionID string, agentType types.AgentType, transcriptPath string, checkpointID id.CheckpointID, meta transcriptMetadata, tokenUsage \*agent.TokenUsage, opts attachOptions, reviewSkills \[\]string) error {
stateStore, err := session.NewStateStore(ctx)
if err != nil {
return fmt.Errorf("failed to open session store: %w", err)
}

now := time.Now() state := existingState if state == nil { state = &session.State{ SessionID: sessionID, StartedAt: now, } }

// Populate BaseCommit from HEAD if not already set, so the session becomes // active and future commits in the same session receive Entire-Checkpoint trailers. if state.BaseCommit == "" { if head, headErr := repo.Head(); headErr == nil { headHash := head.Hash().String() state.BaseCommit = headHash state.AttributionBaseCommit = headHash } }

state.CLIVersion = versioninfo.Version state.AttachedManually = true state.AgentType = agentType state.TranscriptPath = transcriptPath state.LastCheckpointID = checkpointID // Only transition to Ended if the session is not already active — avoid // breaking an ongoing session whose BaseCommit has just been restored above. if !state.Phase.IsActive() { state.Phase = session.PhaseEnded } state.LastInteractionTime = &now if meta.TurnCount > 0 { state.SessionTurnCount = meta.TurnCount } if meta.Model != "" { state.ModelName = meta.Model } if meta.FirstPrompt != "" { state.LastPrompt = meta.FirstPrompt } if tokenUsage != nil { state.TokenUsage = tokenUsage } if opts.Review { state.Kind = session.KindAgentReview state.ReviewSkills = reviewSkills state.ReviewPrompt = reviewPromptForAttach(meta, opts) }

if err := stateStore.Save(ctx, state); err != nil { return fmt.Errorf("failed to save session state: %w", err) } return nil


}

func reviewPromptForAttach(meta transcriptMetadata, opts attachOptions) string {
if opts.ReviewPromptOverride != "" {
return opts.ReviewPromptOverride
}
return meta.FirstPrompt
}

// validateAttachPreconditions checks session ID format and git repo state.
// Returns the existing session state if the session is already tracked (nil if new).
func validateAttachPreconditions(ctx context.Context, repo \*git.Repository, sessionID string) (\*session.State, error) {
if err := validation.ValidateSessionID(sessionID); err != nil {
return nil, fmt.Errorf("invalid session ID: %w", err)
}

if strategy.IsEmptyRepository(repo) { return nil, errors.New("repository has no commits yet — make an initial commit before running attach") }

store, err := session.NewStateStore(ctx) if err != nil { return nil, fmt.Errorf("failed to open session store: %w", err) } existing, err := store.Load(ctx, sessionID) if err != nil { return nil, fmt.Errorf("failed to check existing session: %w", err) }

return existing, nil


}

// resolveAgentAndTranscript resolves the agent and transcript path.
// For existing sessions, resolves the agent from session state's AgentType.
// For new sessions, uses the --agent flag with auto-detection fallback.
func resolveAgentAndTranscript(ctx context.Context, w io.Writer, sessionID string, agentName types.AgentName, existingState \*session.State) (agent.Agent, string, error) {
ag, err := resolveAgent(existingState, agentName)
if err != nil {
return nil, "", err
}

transcriptPath, err := resolveAndValidateTranscript(ctx, sessionID, ag) if err != nil { // Auto-detect: try all other agents. detectedAg, detectedPath, detectErr := detectAgentByTranscript(ctx, sessionID, agentName) if detectErr != nil { return nil, "", fmt.Errorf("%w (also tried auto-detecting other agents: %w)", err, detectErr) } ag = detectedAg transcriptPath = detectedPath logging.Info(ctx, "auto-detected agent from transcript", "agent", ag.Name()) fmt.Fprintf(w, "Auto-detected agent: %s\n", ag.Name()) }

return ag, transcriptPath, nil


}

// resolveAgent resolves the agent to use
...\[truncated\]

\-\-\- cmd/entire/cli/attach\_transcript.go
package cli

import (
"encoding/json"

"github.com/entireio/cli/cmd/entire/cli/agent/geminicli" "github.com/entireio/cli/cmd/entire/cli/transcript"


)

// transcriptMetadata holds metadata extracted from a single transcript parse pass.
type transcriptMetadata struct {
FirstPrompt string
TurnCount int
Model string
}

// extractTranscriptMetadata parses transcript bytes once and extracts the first user prompt,
// user turn count, and model name. Supports both JSONL (Claude Code, Cursor, OpenCode) and
// Gemini JSON format.
func extractTranscriptMetadata(data \[\]byte) transcriptMetadata {
var meta transcriptMetadata

// Try JSONL format first (Claude Code, Cursor, OpenCode, etc.) lines, err := transcript.ParseFromBytes(data) if err == nil { for _, line := range lines { if line.Type == transcript.TypeUser { if prompt := transcript.ExtractUserContent(line.Message); prompt != "" { meta.TurnCount++ if meta.FirstPrompt == "" { meta.FirstPrompt = prompt } } } if line.Type == transcript.TypeAssistant && meta.Model == "" { var msg struct { Model string json:"model" } if json.Unmarshal(line.Message, &msg) == nil && msg.Model != "" { meta.Model = msg.Model } } } if meta.TurnCount > 0 || meta.Model != "" { return meta } }

// Fallback: try Gemini JSON format {"messages": [...]} if prompts, gemErr := geminicli.ExtractAllUserPrompts(data); gemErr == nil && len(prompts) > 0 { meta.FirstPrompt = prompts[0] meta.TurnCount = len(prompts) }

return meta


}

\-\-\- cmd/entire/cli/auth.go
package cli

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"

"charm.land/lipgloss/v2" "github.com/entireio/cli/cmd/entire/cli/api" "github.com/entireio/cli/cmd/entire/cli/auth" "github.com/spf13/cobra"


)

// authTokenLister lists API tokens for the authenticated user.
type authTokenLister func(ctx context.Context, token string) (\[\]api.Token, error)

// authTokenRevoker revokes a single API token by id.
type authTokenRevoker func(ctx context.Context, callerToken, id string) error

// User-visible placeholder strings. Promoted to constants so tests and
// production share a single source of truth.
const (
placeholderDash = "-"
lastUsedNever = "never"
lastUsedJustNow = "just now"
)

// requireSecureBaseURL enforces TLS unless insecureHTTPAuth is set. Every
// command that sends a bearer token over the network (login, logout,
// auth status/list/revoke) must call this so credentials don't leak over
// plaintext HTTP without explicit opt-in.
func requireSecureBaseURL(insecureHTTPAuth bool) error {
if insecureHTTPAuth {
return nil
}
if err := api.RequireSecureURL(api.BaseURL()); err != nil {
return fmt.Errorf("base URL check: %w", err)
}
return nil
}

// addInsecureHTTPAuthFlag attaches the hidden --insecure-http-auth flag used
// by every authenticated command for local development.
func addInsecureHTTPAuthFlag(cmd \*cobra.Command, target \*bool) {
cmd.Flags().BoolVar(target, "insecure-http-auth", false, "Allow authentication over plain HTTP (insecure, for local development only)")
if err := cmd.Flags().MarkHidden("insecure-http-auth"); err != nil {
panic(fmt.Sprintf("hide insecure-http-auth flag: %v", err))
}
}

func newAuthCmd() \*cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Manage authentication and API tokens",
Long: "Authentication subcommands. Includes login, logout, status, listing tokens, and revoking tokens.",
RunE: func(cmd \*cobra.Command, \_ \[\]string) error {
return cmd.Help()
},
}

cmd.AddCommand(newLoginCmd()) cmd.AddCommand(newLogoutCmd()) cmd.AddCommand(newAuthStatusCmd()) cmd.AddCommand(newAuthListCmd()) cmd.AddCommand(newAuthRevokeCmd()) return cmd


}

// \-\-\- status -----------------------------------------------------------------

func newAuthStatusCmd() \*cobra.Command {
var insecureHTTPAuth bool
cmd := &cobra.Command{
Use: "status",
Short: "Show authentication status",
RunE: func(cmd \*cobra.Command, \_ \[\]string) error {
if err := requireSecureBaseURL(insecureHTTPAuth); err != nil {
return err
}
return runAuthStatus(cmd.Context(), cmd.OutOrStdout(),
auth.NewStore(), defaultListTokens, api.BaseURL())
},
}
addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth)
return cmd
}

func defaultListTokens(ctx context.Context, token string) (\[\]api.Token, error) {
return api.NewClient(token).ListTokens(ctx) //nolint:wrapcheck // ListTokens already wraps with action context
}

func runAuthStatus(ctx context.Context, w io.Writer, store tokenStore, list authTokenLister, baseURL string) error {
token, err := store.GetToken(baseURL)
if err != nil {
return fmt.Errorf("read keychain: %w", err)
}
if token == "" {
fmt.Fprintf(w, "Not logged in to %s\\n", baseURL)
fmt.Fprintln(w, "Run 'entire login' to authenticate.")
return nil
}

tokens, err := list(ctx, token) if err != nil { if api.IsHTTPErrorStatus(err, http.StatusUnauthorized) { fmt.Fprintf(w, "Token in keychain for %s is no longer valid.\n", baseURL) fmt.Fprintln(w, "Run 'entire login' to re-authenticate.") return nil } return fmt.Errorf("validate token: %w", err) }

fmt.Fprintf(w, "Logged in to %s\n", baseURL) fmt.Fprintln(w, " Token: stored in OS keychain") fmt.Fprintf(w, " Active tokens on this account: %d\n", len(tokens)) return nil


}

// \-\-\- list -------------------------------------------------------------------

func newAuthListCmd() \*cobra.Command {
var jsonOut bool
var insecureHTTPAuth bool
cmd := &cobra.Command{
Use: "list",
Short: "List active API tokens for the authenticated user",
RunE: func(cmd \*cobra.Command, \_ \[\]string) error {
if err := requireSecureBaseURL(insecureHTTPAuth); err != nil {
return err
}
return runAuthList(cmd.Context(), cmd.OutOrStdout(),
auth.NewStore(), defaultListTokens, api.BaseURL(), jsonOut)
},
}
cmd.Flags().BoolVar(&jsonOut, "json", false, "Print tokens as JSON")
addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth)
return cmd
}

func runAuthList(ctx context.Context, w io.Writer, store tokenStore, list authTokenLister, baseURL string, jsonOut bool) error {
token, err := store.GetToken(baseURL)
if err != nil {
return fmt.Errorf("read keychain: %w", err)
}
if token == "" {
return fmt.Errorf("not logged in to %s; run 'entire login' first", baseURL)
}

tokens, err := list(ctx, token) if err != nil { return err }

if jsonOut { enc := json.NewEncoder(w) enc.SetIndent("", " ") if err := enc.Encode(tokens); err != nil { return fmt.Errorf("encode JSON: %w", err) } return nil }

if len(tokens) == 0 { fmt.Fprintln(w, "No active tokens.") return nil }

// Deterministic order: most recently used first, then most recently // created, then by id as a final tie-breaker so the output is fully // specified regardless of the server's response order. sort.Slice(tokens, func(i, j int) bool { li := lastUsedSortKey(tokens[i]) lj := lastUsedSortKey(tokens[j]) if li != lj { return li > lj } if tokens[i].CreatedAt != tokens[j].CreatedAt { return tokens[i].CreatedAt > tokens[j].CreatedAt } return tokens[i].ID < tokens[j].ID })

sty := newAuthListStyles(w) renderAuthListTable(w, sty, tokens, time.Now()) return nil


}

// authListStyles holds the lipgloss styles for `entire auth list`. Mirrors the
// approach in activity\_render.go: keep style construction tied to color
// detection, and render plain text when color is disabled.
type authListStyles struct {
colorEnabled bool

header lipgloss.Style // bold + dim, used for column headers id lipgloss.Style // yellow accent name lipgloss.Style // bold value lipgloss.Style // default fg for scope/dates (no color) dim lipgloss.Style // "never", "-" warning lipgloss.Style // expires-soon expired lipgloss.Style // already expired


}

func newAuthListStyles(w io.Writer) authListStyles {
useColor := shouldUseColor(w)
s := authListStyles{colorEnabled: useColor}
if !useColor {
return s
}
s.header = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Bold(true)
s.id = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow
s.name = lipgloss.NewStyle().Bold(true)
s.value = lipgloss.NewStyle() // default fg
s.dim = lipgloss.NewStyle().Faint(true)
s.warning = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow
s.expired = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red
return s
}

func (s authListStyles) render(style lipgloss.Style, text string) string {
if !s.colorEnabled {
return text
}
return style.Render(text)
}

// renderAuthListTable prints a styled, column-aligned table of tokens. Column
// padding is computed via lipgloss.Width — it strips ANSI escapes, so a styled
// cell's visible width matches its plain text. tabwriter can't be used here
// once cells contain ANSI codes.
func renderAuthListTable(w io.Writer, sty authListStyles, tokens \[\]api.Token, now time.Time) {
headerCells := \[\]string{"ID", "NAME", "SCOPE", "CREATED", "LAST USED", "EXPIRES"}
header := make(\[\]string, len(headerCells))
for i, h := range headerCells {
header\[i\] = sty.render(sty.header, h)
}

rows := make([][]string, 0, len(tokens)) for _, t := range tokens { rows = append(rows, []string{ sty.render(sty.id, t.ID), styleName(sty, t.Name), sty.render(sty.value, fallback(t.Scope, placeholderDash)), sty.render(sty.value, formatAuthDate(t.CreatedAt)), styleLastUsed(sty, t.LastUsedAt, now), styleExpires(sty, t.ExpiresAt, now), }) }

widths := make([]int, len(headerCells)) for i, h := range header { widths[i] = lipgloss.Width(h) } for _, row := range rows { for i, c := range row { if cw := lipgloss.Width(c); cw > widths[i] { widths[i] = cw } } }

writeRow(w, header, widths) for _, row := range rows { writeRow(w, row, widths) }


}

func writeRow(w io.Writer, cells \[\]string, widths \[\]int) {
for i, c := range cells {
fmt.Fprint(w, c)
if i < len(cells)-1 {
fmt.Fprint(w, strings.Repeat(" ", widths\[i\]-lipgloss.Width(c)+2))
}
}
fmt.Fprintln(w)
}

func styleName(sty authListStyles, name string) string {
if name == "" {
return sty.render(sty.dim, placeholderDash)
}
return sty.render(sty.name, name)
}

func styleLastUsed(sty authListStyles, lastUsed \*string, now time.Time) string {
if lastUsed == nil {
return sty.render(sty.dim, lastUsedNever)
}
return sty.render(sty.value, formatAuthLastUsed(lastUsed, now))
}

func styleExpires(sty authListStyles, expiresAt string, now time.Time) string {
formatted := formatAuthDate(expiresAt)
switch classifyExpiresAt(expiresAt, now) {
case expiresExpired:
return sty.render(sty.expired, formatted)
case expiresSoon:
return sty.render(sty.warning, formatted)
case expiresNormal:
return sty.render(sty.value, formatted)
}
return sty.render(sty.value, formatted)
}

func lastUsedSortKey(t api.Token) string {
if t.LastUsedAt == nil {
return ""
}
return \*t.LastUsedAt
}

// formatAuthDate renders an RFC3339 timestamp as YYYY-MM-DD in local time.
func formatAuthDate(s string) string {
if s == "" {
return placeholderDash
}
if ts, err := time.Parse(time.RFC3339, s); err == nil {
return ts.Local().Format("2006-01-02")
}
return s
}

// formatAuthLastUsed renders a relative "last used" timestamp, with "yesterday"
// and absolute-date branches that the shared formatRelativeDuration helper
// doesn't cover.
func formatAuthLastUsed(s _string, now time.Time) string {_
_if s == nil \|\| s == "" {_
_return lastUsedNever_
_}_
_ts, err := time.Parse(time.RFC3339, s)_
_if err != nil {_
_return s_
_}_
_delta := now.Sub(ts)_
_switch {_
_case delta < 0, delta >= 3024time.Hour:_
_return ts.Local().Format("2006-01-02")_
_case delta >= 24time.Hour && delta < 48_ time.Hour:
return "yesterday"
default:
return formatRelativeDuration(delta)
}
}

type expiresState int

const (
expiresNormal expiresState = iota
expiresSoon
expiresExpired
)

// classifyExpiresAt classifies an RFC3339 expires-at relative to now. Used to
// color the EXPIRES column so tokens worth rotating stand out.
func classifyExpiresAt(s string, now time.Time) expiresState {
if s == "" {
return expiresNormal
}
ts, err := time.Parse(time.RFC3339, s)
if err != nil {
return expiresNormal
}
delta := ts.Sub(now)
switch {
case delta <= 0:
return expiresExpired
case delta < 7 _24_ time.Hour:
return expiresSoon
default:
return expiresNormal
}
}

func fallback(s, alt string) string {
if strings.TrimSpace(s) == "" {
return alt
}
return s
}

// \-\-\- revoke -----------------------------------------------------------------

func newAuthRevokeCmd() \*cobra.Command {
var revokeCurrent bool
var insecureHTTPAuth bool
cmd := &cobra.Command{
Use: "revoke \[id\]",
Short: "Revoke an API token by id",
Long: "Revoke a specific API token. Use --current to revoke the token used by this CLI (equivalent to 'entire logout').",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd \*cobra.Command, args \[\]string) error {
id := ""
if len(args) == 1 {
id = args\[0\]
}
if id == "" && !revokeCurrent {
return cmd.Help()
}
if id != "" && revokeCurrent {
return errors.New("cannot use both <id> and --current")
}
if err := requireSecureBaseURL(insecureHTTPAuth); err != nil {
return err
}
return runAuthRevoke(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(),
auth.NewStore(), defaultListTokens, defaultRevokeTokenByID, defaultRevokeCurrentToken,
api.BaseURL(), id, revokeCurrent)
},
}
cmd.Flags().BoolVar(&revokeCurrent, "current", false, "Revoke the token used by this CLI and remove the local copy")
addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth)
return cmd
}

func defaultRevokeTokenByID(ctx context.Context, callerToken, id string) error {
return api.NewClient(callerToken).RevokeToken(ctx, id) //nolint:wrapcheck // RevokeToken already wraps with action context
}

func runAuthRevoke(
ctx context.Context,
outW, errW io.Writer,
store tokenStore,
list authTokenLister,
revokeByID authTokenRevoker,
revokeCurrent revokeCurrentFunc,
baseURL, id string,
current bool,
) error {
token, err := store.GetToken(baseURL)
if err != nil {
return fmt.Errorf("read keychain: %w", err)
}
if token == "" {
return fmt.Errorf("not logged in to %s; run 'entire login' first", baseURL)
}

if current { // Revoking our own token is just logout — reuse that path so behavior // stays identical (best-effort revoke + local delete). return runLogout(ctx, outW, errW, store, revokeCurrent, baseURL) }

if err := revokeByID(ctx, token, id); err != nil { return err }

// The list endpoint requires bearer auth, so a 401 here means the id we // just revoked was the same one this CLI is using — the keychain entry is // now stale and would otherwise produce confusing 401s on every command. if _, listErr := list(ctx, token); listErr != nil && api.IsHTTPErrorStatus(listErr, http.StatusUnauthorized) { if delErr := store.DeleteToken(baseURL); delErr != nil { return fmt.Errorf("revoked token %s but failed to remove local copy: %w", id, delErr) } fmt.Fprintf(outW, "Revoked token %s (this was your local token; removed from keychain).\n", id) return nil }

fmt.Fprintf(outW, "Revoked token %s.\n", id) return nil


}

\-\-\- cmd/entire/cli/checkpoint\_group.go
package cli

import (
"errors"

"github.com/entireio/cli/cmd/entire/cli/paths" "github.com/spf13/cobra"


)

// newCheckpointGroupCmd builds the `entire checkpoint` parent command and
// registers list/explain/rewind/search as children.
func newCheckpointGroupCmd() \*cobra.Command {
cmd := &cobra.Command{
Use: "checkpoint",
Aliases: \[\]string{"cp", "checkpoints"},
Short: "Inspect, rewind, and search checkpoints",
Long: \`Operations on checkpoints — the persistent records of agent work tied to commits.

Commands:
list List checkpoints on the current branch
explain Explain a checkpoint, commit, or session
rewind Browse and rewind to a checkpoint
search Search checkpoints (semantic + keyword)

Examples:
entire checkpoint list
entire checkpoint explain <id\|sha>
entire checkpoint rewind --to <id>
entire checkpoint search "fix login"\`,
PersistentPreRunE: func(cmd \*cobra.Command, \_ \[\]string) error {
if \_, err := paths.WorktreeRoot(cmd.Context()); err != nil {
return errors.New("not a git repository")
}
return nil
},
}

cmd.AddCommand(newCheckpointListCmd()) cmd.AddCommand(newExplainCmd()) cmd.AddCommand(newRewindCmd()) cmd.AddCommand(newCheckpointSearchCmd())

return cmd


}

func newCheckpointSearchCmd() \*cobra.Command {
cmd := newSearchCmd()
cmd.Hidden = false
return cmd
}

// newCheckpointListCmd wraps the existing branch-default list view.
func newCheckpointListCmd() \*cobra.Command {
var sessionFlag string
var noPagerFlag bool

cmd := &cobra.Command{ Use: "list", Short: "List checkpoints on the current branch", Long: `List checkpoints on the current branch.


Optionally filter by session ID with --session.\`,
RunE: func(cmd \*cobra.Command, \_ \[\]string) error {
if checkDisabledGuard(cmd.Context(), cmd.OutOrStdout()) {
return nil
}
return runExplainBranchWithFilter(cmd.Context(), cmd.OutOrStdout(), noPagerFlag, sessionFlag)
},
}

cmd.Flags().StringVar(&sessionFlag, "session", "", "Filter checkpoints by session ID (or prefix)") cmd.Flags().BoolVar(&noPagerFlag, "no-pager", false, "Disable pager output") return cmd


}

\-\-\- cmd/entire/cli/checkpoint\_reader.go
package cli

import (
"context"
"log/slog"

"github.com/entireio/cli/cmd/entire/cli/checkpoint" "github.com/entireio/cli/cmd/entire/cli/checkpoint/remote" "github.com/entireio/cli/cmd/entire/cli/logging" "github.com/entireio/cli/cmd/entire/cli/settings"

git "github.com/go-git/go-git/v6"


)

type committedCheckpointReaderStores struct {
v1Store \*checkpoint.GitStore
v2Store \*checkpoint.V2GitStore
reader checkpoint.CommittedListReader
readMode checkpoint.CommittedReadMode
}

type committedCheckpointReaderOptions struct {
blobFetcher checkpoint.BlobFetchFunc
fetchRemoteLog string
}

func committedCheckpointReadMode(ctx context.Context) checkpoint.CommittedReadMode {
return checkpoint.CommittedReadModeForOptions(
settings.IsCheckpointsV2Enabled(ctx),
settings.CheckpointsVersion(ctx),
)
}

func newCommittedCheckpointReader(ctx context.Context, repo \*git.Repository, opts committedCheckpointReaderOptions) (\*committedCheckpointReaderStores, error) {
v1Store := checkpoint.NewGitStore(repo)
if opts.blobFetcher != nil {
v1Store.SetBlobFetcher(opts.blobFetcher)
}

readMode := committedCheckpointReadMode(ctx) var v2Store *checkpoint.V2GitStore if committedCheckpointReadUsesV2(readMode) { v2URL, err := remote.FetchURL(ctx) if err != nil { message := opts.fetchRemoteLog if message == "" { message = "checkpoint reader: using origin for v2 store fetch remote" } logging.Debug(ctx, message, slog.String("error", err.Error())) v2URL = "" } v2Store = checkpoint.NewV2GitStore(repo, v2URL) if opts.blobFetcher != nil { v2Store.SetBlobFetcher(opts.blobFetcher) } }

reader, err := checkpoint.NewCommittedReader(v1Store, v2Store, readMode) if err != nil { return nil, err //nolint:wrapcheck // Caller adds command-specific context. }

return &committedCheckpointReaderStores{ v1Store: v1Store, v2Store: v2Store, reader: reader, readMode: readMode, }, nil


}

func committedCheckpointReadUsesV2(mode checkpoint.CommittedReadMode) bool {
return mode != checkpoint.CommittedReadV1
}

\-\-\- cmd/entire/cli/clean.go
package cli

import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"strings"

"charm.land/huh/v2" "github.com/entireio/cli/cmd/entire/cli/checkpoint" "github.com/entireio/cli/cmd/entire/cli/logging" "github.com/entireio/cli/cmd/entire/cli/paths" "github.com/entireio/cli/cmd/entire/cli/session" "github.com/entireio/cli/cmd/entire/cli/settings" "github.com/entireio/cli/cmd/entire/cli/strategy" "github.com/go-git/go-git/v6/plumbing" "github.com/spf13/cobra" "golang.org/x/sync/errgroup"


)

func cleanLongDescription(ctx context.Context) string {
description := \`Clean up Entire session data for the current HEAD commit.

By default, cleans session state and shadow branches for the current HEAD:

- Session state files (.git/entire-sessions/<session-id>.json)
- Shadow branch (entire/<commit-hash>-<worktree-hash>)

Use --all to clean all Entire session data across the repository:

- All session state files (.git/entire-sessions/)

- All shadow branches

- Temporary files (.entire/tmp/)\`

s, err := settings.Load(ctx)
if err == nil && s.IsCheckpointsV2Enabled() {
description += fmt.Sprintf(\`

- Archived v2 full transcripts older than the configured %d-day retention window\`, s.GetFullTranscriptGenerationRetentionDays())
}

description += \`

Use --session <id> to clean a specific session only.

Without --force, prompts for confirmation before deleting.
Use --dry-run to preview what would be deleted without prompting.\`

return description


}

func newCleanCmd() \*cobra.Command {
var forceFlag bool
var allFlag bool
var dryRunFlag bool
var sessionFlag string

cmd := &cobra.Command{ Use: "clean", Short: "Clean up Entire session data", Long: cleanLongDescription(context.Background()), RunE: func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context()

// Validate mutually exclusive flags if allFlag && sessionFlag != "" { return errors.New("--all and --session cannot be used together") }

// Check if in git repository before initializing logging, // to avoid creating .entire/logs in arbitrary directories. if _, err := paths.WorktreeRoot(ctx); err != nil { return errors.New("not a git repository") }

// Initialize logging logging.SetLogLevelGetter(GetLogLevel) if err := logging.Init(ctx, ""); err == nil { defer logging.Close() }

if allFlag { return runCleanAll(ctx, cmd, forceFlag, dryRunFlag) }

if sessionFlag != "" { strat := GetStrategy(ctx) return runCleanSession(ctx, cmd, strat, sessionFlag, forceFlag, dryRunFlag, "Clean", "cleaned") }

return runCleanCurrentHead(ctx, cmd, forceFlag, dryRunFlag) }, }

cmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Skip confirmation prompt and override active session guard") cmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Clean all session data across the repository") cmd.Flags().BoolVarP(&dryRunFlag, "dry-run", "d", false, "Preview what would be deleted without deleting") cmd.Flags().StringVar(&sessionFlag, "session", "", "Clean a specific session by ID")

return cmd


}

// runCleanCurrentHead cleans session data for the current HEAD commit.
func runCleanCurrentHead(ctx context.Context, cmd \*cobra.Command, force, dryRun bool) error {
strat := GetStrategy(ctx)
w := cmd.OutOrStdout()

// Dry-run: show what would be cleaned if dryRun { return previewCurrentHead(ctx, w) }

// Check for active sessions before cleaning if !force { activeSessions, err := activeSessionsOnCurrentHead(ctx) if err != nil { fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not check for active sessions: %v\n", err) fmt.Fprintln(cmd.ErrOrStderr(), "Use --force to override.") return nil } if len(activeSessions) > 0 { fmt.Fprintln(cmd.ErrOrStderr(), "Active sessions detected on current HEAD:") for _, s := range activeSessions { fmt.Fprintf(cmd.ErrOrStderr(), " %s (phase: %s)\n", s.SessionID, s.Phase) } fmt.Fprintln(cmd.ErrOrStderr(), "Use --force to override or wait for sessions to finish.") return nil } }

// Prompt for confirmation if !force { var confirmed bool

form := NewAccessibleForm( huh.NewGroup( huh.NewConfirm(). Title("Clean session data for current HEAD?"). Value(&confirmed), ), )

if err := form.Run(); err != nil { if errors.Is(err, huh.ErrUserAborted) { return nil } return fmt.Errorf("failed to get confirmation: %w", err) }

if !confirmed { return nil } }

if err := strat.Reset(ctx, cmd.OutOrStdout(), cmd.ErrOrStderr()); err != nil { return fmt.Errorf("clean failed: %w", err) }

return nil


}

// previewCurrentHead shows what would be cleaned for the current HEAD.
func previewCurrentHead(ctx context.Context, w io.Writer) error {
repo, err := openRepository(ctx)
if err != nil {
return err
}

head, err := repo.Head() if err != nil { return fmt.Errorf("failed to get HEAD: %w", err) }

worktreePath, err := paths.WorktreeRoot(ctx) if err != nil { return fmt.Errorf("failed to get worktree path: %w", err) } worktreeID, err := paths.GetWorktreeID(worktreePath) if err != nil { return fmt.Errorf("failed to get worktree ID: %w", err) }

shadowBranchName := checkpoint.ShadowBranchNameForCommit(head.Hash().String(), worktreeID)

// Check if shadow branch exists refName := plumbing.NewBranchReferenceName(shadowBranchName) _, refErr := repo.Reference(refName, true) hasShadowBranch := refErr == nil

// Find sessions for this commit strat := GetStrategy(ctx) sessions, err := strat.FindSessionsForCommit(ctx, head.Hash().String()) if err != nil { sessions = nil }

if !hasShadowBranch && len(sessions) == 0 { fmt.Fprintln(w, "Nothing to clean for current HEAD.") return nil }

fmt.Fprint(w, "Would clean the following items:\n\n")

if len(sessions) > 0 { fmt.Fprintf(w, "Session states (%d):\n", len(sessions)) for _, s := range sessions { fmt.Fprintf(w, " %s (checkpoints: %d)\n", s.SessionID, s.StepCount) } fmt.Fprintln(w) }

if hasShadowBranch { fmt.Fprintf(w, "Shadow branch:\n %s\n\n", shadowBranchName) }

fmt.Fprintln(w, "Run without --dry-run to clean these items.") return nil


}

// runCleanSession handles the --session flag: clean/reset a single session.
// actionVerb is the capitalized verb (e.g., "Clean" or "Reset") and pastVerb
// is the past tense (e.g., "cleaned" or "reset") used in user-facing messages.
func runCleanSession(ctx context.Context, cmd \*cobra.Command, strat \*strategy.ManualCommitStrategy, sessionID string, force, dryRun bool, actionVerb, pastVerb string) error {
// Verify the session exists
state, err := strategy.LoadSessionState(ctx, sessionID)
if err != nil {
return fmt.Errorf("failed to load session: %w", err)
}
if state == nil {
return fmt.Errorf("session not found: %s", sessionID)
}

if dryRun { w := cmd.OutOrStdout() fmt.Fprintf(w, "Would %s session %s (phase: %s, checkpoints: %d)\n", strings.ToLower(actionVerb), sessionID, state.Phase, state.StepCount) return nil }

if !force { var confirmed bool

title := fmt.Sprintf("%s session %s?", actionVerb, sessionID) description := fmt.Sprintf("Phase: %s, Checkpoints: %d", state.Phase, state.StepCount)

form := NewAccessibleForm( huh.NewGroup( huh.NewConfirm(). Title(title). Description(description). Value(&confirmed), ), )

if err := form.Run(); err != nil { if errors.Is(err, huh.ErrUserAborted) { return nil } return fmt.Errorf("failed to get confirmation: %w", err) }

if !confirmed { return nil } }

if err := strat.ResetSession(ctx, cmd.OutOrStdout(), cmd.ErrOrStderr(), sessionID); err != nil { return fmt.Errorf("%s session failed: %w", strings.ToLower(actionVerb), err) }

fmt.Fprintf(cmd.OutOrStdout(), "Session %s has been %s. File changes remain in the working directory.\n", sessionID, pastVerb) return nil


}

// runCleanAll cleans all session data across the repository.
func runCleanAll(ctx context.Context, cmd \*cobra.Command, force, dryRun bool) error {
s, err := settings.Load(ctx)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to load settings: %v\\n", err)
s = &settings.EntireSettings{}
}

var ( items []strategy.CleanupItem v2Items []strategy.CleanupItem v2Warnings []string ) g, gCtx := errgroup.WithContext(ctx) g.Go(func() error { var err error items, err = strategy.ListAllItems(gCtx) if err != nil { return fmt.Errorf("failed to list items: %w", err) } return nil }) if s.IsCheckpointsV2Enabled() { g.Go(func() error { var err error v2Items, v2Warnings, err = strategy.ListEligibleV2Generations(gCtx, s) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err //nolint:wrapcheck // propagate cancellation unwrapped so callers map it to SilentError } return fmt.Errorf("failed to list v2 generations: %w", err) } return nil }) } if err := g.Wait(); err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return NewSilentError(err) } return err //nolint:wrapcheck // already wrapped inside the goroutine closures } items = append(items, v2Items...) for _, warning := range v2Warnings { fmt.Fprintf(cmd.ErrOrStderr(), "Warning: %s\n", warning) }

tempFiles, err := listAllTempFiles(ctx) if err != nil { fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to list temp files: %v\n", err) }

return runCleanAllWithItems(ctx, cmd, force, dryRun, items, tempFiles)


}

// printSection prints a titled list of items if the slice is non-empty.
func printSection(w io.Writer, title string, items \[\]string) {
if len(items) == 0 {
return
}
fmt.Fprintf(w, "%s (%d):\\n", title, len(items))
for \_, item := range items {
fmt.Fprintf(w, " %s\\n", item)
}
fmt.Fprintln(w)
}

// printResultSection prints a titled list with a leading newline, for post-deletion output.
func printResultSection(w io.Writer, title string, items \[\]string) {
if len(items) == 0 {
return
}
fmt.Fprintf(w, "\\n%s (%d):\\n", title, len(items))
for \_, item := range items {
fmt.Fprintf(w, " %s\\n", item)
}
}

// runCleanAllWithItems is the core logic for cleaning all items.
// Separated for testability — tests pass a cmd without a TTY and use force or dryRun to avoid prompts.
func runCleanAllWithItems(ctx context.Context, cmd \*cobra.Command, force, dryRun bool, items \[\]strategy.CleanupItem, tempFiles \[\]string) error {
w := cmd.OutOrStdout()
errW := cmd.ErrOrStderr()
// Handle no items case
if len(items) == 0 && len(tempFiles) == 0 {
fmt.Fprintln(w, "No items to clean up.")
return nil
}

// Group items by type for display var branches, states, checkpoints, v2Generations []strategy.CleanupItem for _, item := range items { switch item.Type { case strategy.CleanupTypeShadowBranch: branches = append(branches, item) case strategy.CleanupTypeSessionState: states = append(states, item) case strategy.CleanupTypeCheckpoint: checkpoints = append(checkpoints, item) case strategy.CleanupTypeV2Generation: v2Generations = append(v2Generations, item) } }

// Show preview when not in force mode if !force || dryRun { totalItems := len(items) + len(tempFiles) fmt.Fprintf(w, "Found %d %s to clean:\n\n", totalItems, itemWord(totalItems))

printSection(w, "Shadow branches", cleanupItemIDs(branches)) printSection(w, "Session states", cleanupItemIDs(states)) printSection(w, "Checkpoint metadata", cleanupItemIDs(checkpoints)) printSection(w, "Archived v2 generations", cleanupItemIDs(v2Generations)) printSection(w, "Temp files", tempFiles)

if dryRun { fmt.Fprintln(w, "Run without --dry-run to delete these items.") return nil }

// Prompt for confirmation var confirmed bool form := NewAccessibleForm( huh.NewGroup( huh.NewConfirm(). Title(fmt.Sprintf("Delete %d %s?", totalItems, itemWord(totalItems))). Value(&confirmed), ), ) if err := form.Run(); err != nil { if errors.Is(err, huh.ErrUserAborted) { return nil } return fmt.Errorf("failed to get confirmation: %w", err) } if !confirmed { return nil } }

// Force mode - delete items result, err := strategy.DeleteAllCleanupItems(ctx, items) if err != nil { return fmt.Errorf("failed to delete items: %w", err) }

// Delete temp files deletedTempFiles, failedTempFiles := deleteTempFiles(ctx, tempFiles)

// Report results totalDeleted := len(result.ShadowBranches) + len(result.SessionStates) + len(result.Checkpoints) + len(result.V2Generations) + len(deletedTempFiles) totalFailed := len(result.FailedBranches) + len(result.FailedStates) + len(result.FailedCheckpoints) + len(result.FailedV2Refs) + len(failedTempFiles)

if totalDeleted > 0 { fmt.Fprintf(w, "✓ Deleted %d %s:\n", totalDeleted, itemWord(totalDeleted))

printResultSection(w, "Shadow branches", result.ShadowBranches) printResultSection(w, "Session states", result.SessionStates) printResultSection(w, "Checkpoints", result.Checkpoints) printResultSection(w, "Archived v2 generations", result.V2Generations)

printResultSection(w, "Temp files", deletedTempFiles) }

if totalFailed > 0 { fmt.Fprintf(errW, "\nFailed to delete %d %s:\n", totalFailed, itemWord(totalFailed))

printResultSection(errW, "Shadow branches", result.FailedBranches) printResultSection(errW, "Session states", result.FailedStates) printResultSection(errW, "Checkpoints", result.FailedCheckpoints) printResultSection(errW, "Archived v2 generations", result.FailedV2Refs)

if len(failedTempFiles) > 0 { fmt.Fprintf(errW, "\nTemp files:\n") for _, fe := range failedTempFiles { fmt.Fprintf(errW, " %s: %v\n", fe.File, fe.Err) } }

return fmt.Errorf("failed to delete %d %s", totalFailed, itemWord(totalFailed)) }

return nil


}

// cleanupItemIDs extracts IDs from a slice of CleanupItems.
func cleanupItemIDs(items \[\]strategy.CleanupItem) \[\]string {
ids := make(\[\]string, len(items))
for i, item := range items {
ids\[i\] = item.ID
}
return ids
}

// listAllTempFiles returns all files in .entire/tmp/ without filtering.
// Used by --all since those sessions are being deleted anyway.
func listAllTempFiles(ctx context.Context) (\[\]string, error) {
absDir, err := paths.AbsPath(ctx, paths.EntireTmpDir)
if err != nil {
return nil, fmt.Errorf("failed to resolve temp dir: %w", err)
}
root, err := os.OpenRoot(absDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to open root: %w", err)
}
defer root.Close()

var files []string err = fs.WalkDir(root.FS(), ".", func(_ string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } files = append(files, d.Name()) return nil }) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, fmt.Errorf("failed to list temp dir: %w", err) } return files, nil


}

// TempFileDeleteError contains a file name and the error that occurred during deletion.
type TempFileDeleteError struct {
File string
Err error
}

// deleteTempFiles removes all files in .entire/tmp/.
// Uses os.Root to ensure deletions are confined to the temp directory.
// Returns successfully deleted files and any failures with their error reasons.
func deleteTempFiles(ctx context.Context, files \[\]string) (deleted \[\]string, failed \[\]TempFileDeleteError) {
absDir, err := paths.AbsPath(ctx, paths.EntireTmpDir)
if err != nil {
for \_, file := range files {
failed = append(failed, TempFileDeleteError{File: file, Err: err})
}
return nil, failed
}
root, err := os.OpenRoot(absDir)
if err != nil {
for \_, file := range files {
failed = append(failed, TempFileDeleteError{File: file, Err: err})
}
return nil, failed
}
defer root.Close()

for _, file := range files { if err := root.Remove(file); err != nil { failed = append(failed, TempFileDeleteError{File: file, Err: err}) } else { deleted = append(deleted, file) } } return deleted, failed


}

// activeSessionsOnCurrentHead returns sessions on the current HEAD
// that are in an active phase (ACTIVE).
func activeSessionsOnCurrentHead(ctx context.Context) (\[\]\*session.State, error) {
repo, err := openRepository(ctx)
if err != nil {
return nil, err
}

head, err := repo.Head() if err != nil { return nil, fmt.Errorf("failed to get HEAD: %w", err) } currentHead := head.Hash().String()

states, err := strategy.ListSessionStates(ctx) if err != nil { return nil, fmt.Errorf("failed to list session states: %w", err) }

var active []*session.State for _, state := range states { if state.BaseCommit != currentHead { continue } if state.Phase.IsActive() { active = append(active, state) } }

return active, nil


}

// itemWord returns "item" or "items" based on count.
func itemWord(n int) string {
if n == 1 {
return "item"
}
return "items"
}

\-\-\- cmd/entire/cli/commit\_message.go
package cli

import (
"fmt"
"strings"

"github.com/entireio/cli/cmd/entire/cli/agent" "github.com/entireio/cli/cmd/entire/cli/agent/types" "github.com/entireio/cli/cmd/entire/cli/stringutil"


)

// generateCommitMessage creates a commit message from the user's original prompt.
// If the prompt is empty or cleans to empty, falls back to "<agentType> session updates".
func generateCommitMessage(originalPrompt string, agentType types.AgentType) string {
if originalPrompt != "" {
cleaned := cleanPromptForCommit(originalPrompt)
if cleaned != "" {
return cleaned
}
}

if agentType == "" { agentType = agent.AgentTypeUnknown } return fmt.Sprintf("%s session updates", agentType)


}

// cleanPromptForCommit cleans up a user prompt to make it suitable as a commit message
// Uses a loop to remove all matching prefixes until none remain
func cleanPromptForCommit(prompt string) string {
cleaned := prompt

prefixes := []string{ "Can you ", "can you ", "Please ", "please ", "Let's ", "let's ", "Could you ", "could you ", "Would you ", "would you ", "I want you to ", "I'd like you to ", "I need you to ", }

// Loop until no prefix is found for { found := false for _, prefix := range prefixes { if strings.HasPrefix(cleaned, prefix) { cleaned = strings.TrimPrefix(cleaned, prefix) found = true break } } if !found { break } }

cleaned = strings.TrimSuffix(cleaned, "?") cleaned = strings.TrimSpace(cleaned)

// Truncate to 72 characters (rune-safe for multi-byte UTF-8) cleaned = stringutil.TruncateRunes(cleaned, 72, "") cleaned = strings.TrimSpace(cleaned)

// Capitalize first letter (rune-safe for multi-byte UTF-8) cleaned = stringutil.CapitalizeFirst(cleaned)

return cleaned


}

\-\-\- cmd/entire/cli/config.go
package cli

import (
"context"
"fmt"
"strings"

"github.com/entireio/cli/cmd/entire/cli/agent" "github.com/entireio/cli/cmd/entire/cli/agent/types" "github.com/entireio/cli/cmd/entire/cli/settings" "github.com/entireio/cli/cmd/entire/cli/strategy"

// Import agents to register them _ "github.com/entireio/cli/cmd/entire/cli/agent/claudecode" _ "github.com/entireio/cli/cmd/entire/cli/agent/codex" _ "github.com/entireio/cli/cmd/entire/cli/agent/factoryaidroid"


)

// Package-level aliases to avoid shadowing the settings package with local variables named "settings".
const (
EntireSettingsFile = settings.EntireSettingsFile
EntireSettingsLocalFile = settings.EntireSettingsLocalFile
)

// EntireSettings is an alias for settings.EntireSettings.
type EntireSettings = settings.EntireSettings

// LoadEntireSettings loads the Entire settings from .entire/settings.json,
// then applies any overrides from .entire/settings.local.json if it exists.
// Returns default settings if neither file exists.
// Works correctly from any subdirectory within the repository.
func LoadEntireSettings(ctx context.Context) (\*settings.EntireSettings, error) {
s, err := settings.Load(ctx)
if err != nil {
return nil, fmt.Errorf("loading settings: %w", err)
}
return s, nil
}

// SaveEntireSettings saves the Entire settings to .entire/settings.json.
func SaveEntireSettings(ctx context.Context, s \*settings.EntireSettings) error {
if err := settings.Save(ctx, s); err != nil {
return fmt.Errorf("saving settings: %w", err)
}
return nil
}

// SaveEntireSettingsLocal saves the Entire settings to .entire/settings.local.json.
func SaveEntireSettingsLocal(ctx context.Context, s \*settings.EntireSettings) error {
if err := settings.SaveLocal(ctx, s); err != nil {
return fmt.Errorf("saving local settings: %w", err)
}
return nil
}

// IsEnabled returns whether Entire is currently enabled.
// Returns true by default if settings cannot be loaded.
func IsEnabled(ctx context.Context) (bool, error) {
s, err := settings.Load(ctx)
if err != nil {
return true, err //nolint:wrapcheck // already present in codebase
}
return s.Enabled, nil
}

// GetStrategy returns the manual-commit strategy instance with blob fetching
// enabled so that checkpoint reads work after treeless fetches.
func GetStrategy(\_ context.Context) \*strategy.ManualCommitStrategy {
s := strategy.NewManualCommitStrategy()
s.SetBlobFetcher(FetchBlobsByHash)
return s
}

// GetLogLevel returns the configured log level from settings.
// Returns empty string if not configured (caller should use default).
// Note: ENTIRE\_LOG\_LEVEL env var takes precedence; check it first.
func GetLogLevel() string {
s, err := settings.Load(context.TODO()) //nolint:contextcheck // Called as a callback via SetLogLevelGetter, no ctx available
if err != nil {
return ""
}
return s.LogLevel
}

// GetAgentsWithHooksInstalled returns names of agents that have hooks installed.
func GetAgentsWithHooksInstalled(ctx context.Context) \[\]types.AgentName {
var installed \[\]types.AgentName
for \_, name := range agent.List() {
ag, err := agent.Get(name)
if err != nil {
continue
}
if hs, ok := agent.AsHookSupport(ag); ok && hs.AreHooksInstalled(ctx) {
installed = append(installed, name)
}
}
return installed
}

// InstalledAgentDisplayNames returns user-facing display names for agents with hooks installed.
func InstalledAgentDisplayNames(ctx context.Context) \[\]string {
installedNames := GetAgentsWithHooksInstalled(ctx)
displayNames := make(\[\]string, 0, len(installedNames))
for \_, name := range installedNames {
if ag, err := agent.Get(name); err == nil {
displayNames = append(displayNames, string(ag.Type()))
}
}
return displayNames
}

// JoinAgentNames joins agent names into a comma-separated string.
func JoinAgentNames(names \[\]types.AgentName) string {
strs := make(\[\]string, len(names))
for i, n := range names {
strs\[i\] = string(n)
}
return strings.Join(strs, ",")
}

\-\-\- cmd/entire/cli/constants.go
package cli

import "github.com/entireio/cli/cmd/entire/cli/paths"

// Note: Tool name constants (ToolWrite, ToolEdit, etc.) and FileModificationTools
// have been moved to the agent/claudecode package.

// Directory paths - re-exported from paths package for convenience
const (
EntireDir = paths.EntireDir
EntireTmpDir = paths.EntireTmpDir
EntireMetadataDir = paths.EntireMetadataDir
)

\-\-\- cmd/entire/cli/dispatch.go
package cli

import (
"context"
"errors"
"fmt"
"io"
"os"

dispatchpkg "github.com/entireio/cli/cmd/entire/cli/dispatch" "github.com/entireio/cli/cmd/entire/cli/interactive" "github.com/spf13/cobra" "golang.org/x/term"


)

var runDispatch = dispatchpkg.Run
var renderDispatchMarkdown = dispatchpkg.RenderMarkdown
var dispatchTerminalMode = interactive.IsTerminalWriter
var runInteractiveDispatch = defaultRunInteractiveDispatch
var renderTerminalMarkdown = defaultRenderTerminalMarkdown

func newDispatchCmd() \*cobra.Command {
var (
flagLocal bool
flagSince string
flagUntil string
flagAllBranches bool
flagRepos \[\]string
flagVoice string
flagInsecureHTTPAuth bool
)

cmd := &cobra.Command{ Use: "dispatch", Short: "Generate a dispatch summarizing recent agent work", Long: `Generate a dispatch summarizing recent agent work.


Examples:
entire dispatch
entire dispatch --local --all-branches
entire dispatch --repos entireio/cli
entire dispatch --voice neutral\`,
RunE: func(cmd \*cobra.Command, \_ \[\]string) error {
var (
opts dispatchpkg.Options
err error
)
    if shouldRunDispatchWizard(cmd.Flags().NFlag(), isTerminalStdin(os.Stdin), interactive.IsTerminalWriter(cmd.OutOrStdout())) {
        opts, err = runDispatchWizard(cmd)
    } else {
        opts, err = parseDispatchFlags(cmd, flagLocal, flagSince, flagUntil, flagAllBranches, flagRepos, flagVoice, flagInsecureHTTPAuth)
    }
    if err != nil {
        if errors.Is(err, errDispatchCancelled) {
            return nil
        }
        return err
    }

if err := runDispatchCommand(cmd.Context(), cmd.OutOrStdout(), opts); err != nil { if errors.Is(err, errDispatchCancelled) { return nil } return err } return nil }, }

cmd.Flags().BoolVar(&flagLocal, "local", false, "generate via the locally-installed agent CLI instead of the Entire server") cmd.Flags().StringVar(&flagSince, "since", "7d", "time window (Go duration, relative time, or ISO date)") cmd.Flags().StringVar(&flagUntil, "until", "", "window end time (defaults to now)") cmd.Flags().BoolVar(&flagAllBranches, "all-branches", false, "include every existing local branch (--local only; renamed or deleted branches are skipped)") cmd.Flags().StringSliceVar(&flagRepos, "repos", nil, fmt.Sprintf("cloud repo slugs, up to %d (for example entireio/cli)", dispatchpkg.CloudRepoLimit)) cmd.Flags().StringVar(&flagVoice, "voice", "", "voice preset name or literal description") cmd.Flags().BoolVar(&flagInsecureHTTPAuth, "insecure-http-auth", false, "Allow authentication over plain HTTP (insecure, for local development only)") if err := cmd.Flags().MarkHidden("insecure-http-auth"); err != nil { panic(fmt.Sprintf("hide insecure-http-auth flag: %v", err)) }

return cmd


}

func runDispatchCommand(ctx context.Context, outW io.Writer, opts dispatchpkg.Options) error {
if dispatchTerminalMode(outW) && !IsAccessibleMode() {
markdown, err := runInteractiveDispatch(ctx, outW, opts)
if err != nil {
return err
}
rendered, err := renderTerminalMarkdown(outW, markdown)
if err != nil {
return err
}
if \_, err := fmt.Fprint(outW, rendered); err != nil {
return fmt.Errorf("write dispatch output: %w", err)
}
return nil
}

result, err := runDispatch(ctx, opts) if err != nil { return err } if _, err := fmt.Fprint(outW, renderDispatchMarkdown(result)); err != nil { return fmt.Errorf("write dispatch output: %w", err) } return nil


}

func isTerminalStdin(file \*os.File) bool {
return term.IsTerminal(int(file.Fd())) //nolint:gosec // G115: uintptr->int is safe for fd
}

func shouldRunDispatchWizard(flagCount int, stdinIsTerminal bool, stdoutIsTerminal bool) bool {
return flagCount == 0 && stdinIsTerminal && stdoutIsTerminal
}

func parseDispatchFlags(
cmd \*cobra.Command,
flagLocal bool,
flagSince string,
flagUntil string,
flagAllBranches bool,
flagRepos \[\]string,
flagVoice string,
flagInsecureHTTPAuth bool,
) (dispatchpkg.Options, error) {
return resolveDispatchOptions(
flagLocal,
flagSince,
flagUntil,
flagAllBranches,
flagRepos,
flagVoice,
flagInsecureHTTPAuth,
func() (string, error) {
return GetCurrentBranch(cmd.Context())
},
)
}

//nolint:wrapcheck // passthrough glue to keep CLI error text unchanged while option logic lives in dispatch package
func resolveDispatchOptions(
flagLocal bool,
flagSince string,
flagUntil string,
flagAllBranches bool,
flagRepos \[\]string,
flagVoice string,
flagInsecureHTTPAuth bool,
currentBranch func() (string, error),
) (dispatchpkg.Options, error) {
return dispatchpkg.ResolveOptions(
flagLocal,
flagSince,
flagUntil,
flagAllBranches,
flagRepos,
flagVoice,
flagInsecureHTTPAuth,
currentBranch,
)
}

\-\-\- cmd/entire/cli/dispatch\_tui.go
package cli

import (
"context"
"errors"
"fmt"
"io"
"strings"

"charm.land/bubbles/v2/key" "charm.land/bubbles/v2/spinner" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/muesli/termenv"

dispatchpkg "github.com/entireio/cli/cmd/entire/cli/dispatch" "github.com/entireio/cli/cmd/entire/cli/mdrender"


)

type dispatchRenderResult struct {
markdown string
err error
}

type dispatchStatusModel struct {
ctx context.Context
cancel context.CancelFunc
spinner spinner.Model
styles dispatchStatusStyles
title string
subtitle string
details \[\]string
footer string
width int
height int
run func(context.Context) (string, error)
result dispatchRenderResult
}

type dispatchStatusStyles struct {
card lipgloss.Style
title lipgloss.Style
subtitle lipgloss.Style
detail lipgloss.Style
footer lipgloss.Style
spinner lipgloss.Style
}

type dispatchProgram interface {
Run() (tea.Model, error)
}

// newDispatchProgram is overridden by tests via assignment. Tests that mutate
// it cannot use t.Parallel() — they would race each other's factory.
// altScreen is unused in v2 (set on tea.View instead) but retained for backward
// compatibility with existing test fakes.
var newDispatchProgram = func(model tea.Model, outW io.Writer, \_ bool) dispatchProgram {
return tea.NewProgram(model, tea.WithOutput(outW))
}

func defaultRunInteractiveDispatch(ctx context.Context, outW io.Writer, opts dispatchpkg.Options) (string, error) {
runCtx, cancel := context.WithCancel(ctx)
defer cancel()

model := newDispatchStatusModel(outW, opts, func(runCtx context.Context) (string, error) { result, err := runDispatch(runCtx, opts) if err != nil { return "", err } return renderDispatchMarkdown(result), nil }) model.ctx = runCtx model.cancel = cancel

program := newDispatchProgram(model, outW, false) finalModel, err := program.Run() if err != nil { return "", fmt.Errorf("run dispatch tui: %w", err) }

finished, ok := finalModel.(dispatchStatusModel) if !ok { return "", errors.New("unexpected dispatch loading state") } clearDispatchInlineView(outW, finished.View().Content) if finished.result.err != nil { return "", finished.result.err } return finished.result.markdown, nil


}

// defaultRenderTerminalMarkdown renders dispatch's LLM markdown output via
// the shared mdrender palette. Always renders (no TTY check) — dispatch's
// existing behavior is to emit ANSI codes even when redirected so that
// `entire dispatch | less -R` still shows colors.
func defaultRenderTerminalMarkdown(w io.Writer, markdown string) (string, error) {
return mdrender.Render(markdown, getTerminalWidth(w), termenv.HasDarkBackground()) //nolint:wrapcheck // mdrender already wraps glamour's errors with package context
}

func newDispatchStatusModel(
w io.Writer,
opts dispatchpkg.Options,
run func(context.Context) (string, error),
) dispatchStatusModel {
ss := newStatusStyles(w)
styles := newDispatchStatusStyles(ss)
sp := spinner.New(spinner.WithSpinner(spinner.MiniDot))
if ss.colorEnabled {
sp.Style = styles.spinner
}

title := "Generating dispatch" subtitle := "This can take a moment."

return dispatchStatusModel{ spinner: sp, styles: styles, title: title, subtitle: subtitle, details: dispatchStatusDetails(opts), footer: "Press ctrl+c to cancel", width: ss.width, height: 12, run: run, }


}

func newDispatchStatusStyles(ss statusStyles) dispatchStatusStyles {
styles := dispatchStatusStyles{
card: lipgloss.NewStyle(),
title: lipgloss.NewStyle().Bold(true),
subtitle: lipgloss.NewStyle(),
detail: lipgloss.NewStyle(),
footer: lipgloss.NewStyle(),
spinner: lipgloss.NewStyle().Bold(true),
}
if !ss.colorEnabled {
return styles
}

styles.title = styles.title.Foreground(lipgloss.Color("#fb923c")) styles.subtitle = lipgloss.NewStyle().Foreground(lipgloss.Color("245")) styles.detail = lipgloss.NewStyle().Foreground(lipgloss.Color("245")) styles.footer = lipgloss.NewStyle().Foreground(lipgloss.Color("245")) styles.spinner = lipgloss.NewStyle().Foreground(lipgloss.Color("#fb923c")).Bold(true) return styles


}

func dispatchStatusDetails(opts dispatchpkg.Options) \[\]string {
scope := "Scope: current repo"
if len(opts.RepoPaths) > 0 {
scope = "Scope: " + strings.Join(opts.RepoPaths, ", ")
}

var branches string switch { case opts.AllBranches: branches = "Branches: all local branches" case opts.Mode == dispatchpkg.ModeLocal: branches = "Branches: current branch" default: branches = "Branches: default branches" }

window := "Window: " + strings.TrimSpace(opts.Since) if strings.TrimSpace(opts.Until) != "" { window += " → " + strings.TrimSpace(opts.Until) }

return []string{scope, branches, window}


}

func (m dispatchStatusModel) Init() tea.Cmd {
return tea.Batch(m.spinner.Tick, m.runDispatch())
}

func (m dispatchStatusModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
return m, nil
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case dispatchRenderResult:
m.result = msg
return m, tea.Quit
case tea.KeyPressMsg:
if key.Matches(msg, keys.Quit) \|\| key.Matches(msg, keys.Back) {
if m.cancel != nil {
m.cancel()
}
m.result.err = errDispatchCancelled
return m, tea.Quit
}
}
return m, nil
}

func (m dispatchStatusModel) View() tea.View {
cardWidth := min(max(m.width-8, 44), 76)

lines := []string{ m.styles.spinner.Render(m.spinner.View()) + " " + m.styles.title.Render(m.title), m.styles.subtitle.Render(m.subtitle), "", } for _, detail := range m.details { lines = append(lines, m.styles.detail.Render(detail)) } lines = append(lines, "", m.styles.footer.Render(m.footer))

return tea.NewView("\n" + m.styles.card.Width(cardWidth).Render(strings.Join(lines, "\n")))


}

func (m dispatchStatusModel) runDispatch() tea.Cmd {
return func() tea.Msg {
markdown, err := m.run(m.ctx)
return dispatchRenderResult{markdown: markdown, err: err}
}
}

func clearDispatchInlineView(w io.Writer, view string) {
lineCount := renderedLineCount(view)
for range lineCount {
\_, \_ = io.WriteString(w, "\\x1b\[1A\\x1b\[2K\\r") //nolint:errcheck // terminal escape sequence, ignore write errors\
}\
}\
\
func renderedLineCount(view string) int {\
if view == "" {\
return 0\
}\
return strings.Count(view, "\\n") + 1\
}\
\
\-\-\- cmd/entire/cli/dispatch\_wizard.go\
package cli\
\
import (\
"context"\
"errors"\
"fmt"\
"os"\
"os/exec"\
"path/filepath"\
"sort"\
"strings"\
"sync"\
\
```\
"charm.land/huh/v2"\
"github.com/entireio/cli/cmd/entire/cli/api"\
dispatchpkg "github.com/entireio/cli/cmd/entire/cli/dispatch"\
"github.com/entireio/cli/cmd/entire/cli/logging"\
"github.com/entireio/cli/cmd/entire/cli/paths"\
searchpkg "github.com/entireio/cli/cmd/entire/cli/search"\
"github.com/go-git/go-git/v6"\
"github.com/spf13/cobra"\
```\
\
)\
\
var errDispatchCancelled = errors.New("dispatch cancelled")\
var listDispatchWizardRepos = discoverAuthenticatedDispatchWizardRepos\
var listDispatchWizardRepoResources = defaultListDispatchWizardRepoResources\
var resolveDispatchWizardTopLevel = resolveGitTopLevel\
var getDispatchWizardCurrentBranch = GetCurrentBranch\
var runDispatchWizardForm = func(form \*huh.Form) error { return form.Run() }\
\
func defaultListDispatchWizardRepoResources(ctx context.Context) (\[\]api.Repository, error) {\
client, err := NewAuthenticatedAPIClient(false)\
if err != nil {\
return nil, err\
}\
repos, err := client.ListRepositories(ctx, api.RepositorySortRecent)\
if err != nil {\
return nil, fmt.Errorf("list dispatch repos: %w", err)\
}\
return repos, nil\
}\
\
const (\
dispatchWizardRepoDiscoveryConcurrencyLimit = 8\
\
```\
dispatchWizardModeLocal  = "local"\
dispatchWizardModeServer = "server"\
\
dispatchWizardBranchCurrent = "current"\
dispatchWizardBranchAll     = "all"\
\
dispatchWizardVoiceCustom = "custom"\
```\
\
)\
\
type dispatchWizardState struct {\
modeChoice string\
timeWindowPreset string\
localBranchMode string\
currentBranch string\
currentBranchErr error\
selectedRepos \[\]string\
voicePreset string\
voiceCustom string\
confirmRun bool\
}\
\
func newDispatchWizardState() dispatchWizardState {\
return dispatchWizardState{\
modeChoice: dispatchWizardModeLocal,\
timeWindowPreset: "7d",\
localBranchMode: dispatchWizardBranchCurrent,\
voicePreset: "neutral",\
confirmRun: true,\
}\
}\
\
func (s dispatchWizardState) isLocal() bool {\
return s.modeChoice != dispatchWizardModeServer\
}\
\
func (s dispatchWizardState) voiceValue() string {\
switch strings.TrimSpace(s.voicePreset) {\
case "marvin":\
return "marvin"\
case dispatchWizardVoiceCustom:\
if value := strings.TrimSpace(s.voiceCustom); value != "" {\
return value\
}\
}\
return "neutral"\
}\
\
func (s dispatchWizardState) showCustomVoiceInput() bool {\
return strings.TrimSpace(s.voicePreset) == dispatchWizardVoiceCustom\
}\
\
func (s dispatchWizardState) selectedReposList() \[\]string {\
return normalizeDispatchWizardSelections(s.selectedRepos)\
}\
\
func (s dispatchWizardState) resolveCloudRepos() \[\]string {\
if s.isLocal() {\
return nil\
}\
return s.selectedReposList()\
}\
\
func (s dispatchWizardState) showRepoPicker() bool {\
return !s.isLocal()\
}\
\
func (s dispatchWizardState) showLocalBranchMode() bool {\
return s.isLocal()\
}\
\
func (s dispatchWizardState) resolve() (dispatchpkg.Options, error) {\
allBranches := s.isLocal() && s.localBranchMode == dispatchWizardBranchAll\
if s.isLocal() && !allBranches && s.currentBranchErr != nil {\
return dispatchpkg.Options{}, fmt.Errorf("resolve current branch for local dispatch: %w", s.currentBranchErr)\
}\
opts, err := resolveDispatchOptions(\
s.isLocal(),\
s.timeWindowPreset,\
"",\
allBranches,\
s.resolveCloudRepos(),\
s.voiceValue(),\
false,\
func() (string, error) {\
return s.currentBranch, nil\
},\
)\
if err != nil {\
return dispatchpkg.Options{}, err\
}\
return opts, nil\
}\
\
func (s dispatchWizardState) localBranchModeOptions() \[\]huh.Option\[string\] {\
return \[\]huh.Option\[string\]{\
huh.NewOption("Current branch", dispatchWizardBranchCurrent),\
huh.NewOption("All branches", dispatchWizardBranchAll),\
}\
}\
\
func buildDispatchWizardSummary(opts dispatchpkg.Options, scope string) string {\
if strings.TrimSpace(scope) == "" {\
scope = resolvedDispatchScope(opts)\
}\
\
```\
var branches string\
switch {\
case opts.AllBranches:\
    branches = "all local branches"\
case opts.Mode == dispatchpkg.ModeLocal:\
    branches = "current branch"\
default:\
    branches = "default branches"\
}\
\
mode := "cloud"\
if opts.Mode == dispatchpkg.ModeLocal {\
    mode = "local"\
}\
\
return strings.Join([]string{\
    "Mode: " + mode,\
    "Scope: " + scope,\
    "Branches: " + branches,\
}, "\n")\
```\
\
}\
\
func resolvedDispatchScope(opts dispatchpkg.Options) string {\
if len(opts.RepoPaths) > 0 {\
return "repos:" + strings.Join(opts.RepoPaths, ", ")\
}\
return "current repo"\
}\
\
func (s dispatchWizardState) previewScope(opts dispatchpkg.Options) string {\
if s.isLocal() {\
return resolvedDispatchScope(opts)\
}\
selectedRepos := s.selectedReposList()\
if len(selectedRepos) > 0 {\
return "repos:" + strings.Join(selectedRepos, ", ")\
}\
return resolvedDispatchScope(opts)\
}\
\
func buildDispatchCommand(opts dispatchpkg.Options) string {\
return strings.Join(compactStrings(\[\]string{\
"entire dispatch",\
mapBoolToFlag(opts.Mode == dispatchpkg.ModeLocal, "--local"),\
renderStringFlag("--since", strings.TrimSpace(opts.Since)),\
mapBoolToFlag(opts.AllBranches, "--all-branches"),\
renderStringFlag("--repos", strings.Join(opts.RepoPaths, ",")),\
renderStringFlag("--voice", strings.TrimSpace(opts.Voice)),\
}), " ")\
}\
\
func compactStrings(values \[\]string) \[\]string {\
result := make(\[\]string, 0, len(values))\
for \_, value := range values {\
if value != "" {\
result = append(result, value)\
}\
}\
return result\
}\
\
func mapBoolToFlag(enabled bool, flag string) string {\
if enabled {\
return flag\
}\
return ""\
}\
\
func renderStringFlag(name string, value string) string {\
if value == "" {\
return ""\
}\
return name + " " + quoteShellValue(value)\
}\
\
func quoteShellValue(value string) string {\
if value == "" {\
return `""`\
}\
if strings.ContainsAny(value, " ,:\\t") {\
return fmt.Sprintf("%q", value)\
}\
return value\
}\
\
func runDispatchWizard(cmd \*cobra.Command) (dispatchpkg.Options, error) {\
ctx := cmd.Context()\
\
```\
currentRepo, err := paths.WorktreeRoot(ctx)\
if err != nil {\
    return dispatchpkg.Options{}, fmt.Errorf("not in a git repository: %w", err)\
}\
\
loadRepos := newLazyOptions(func() []huh.Option[string] {\
    slugs, listErr := listDispatchWizardRepos(ctx)\
    if listErr != nil || len(slugs) == 0 {\
        slugs = discoverLocalRepoSlugs(ctx, currentRepo)\
    }\
    return buildDispatchRepoOptions(slugs)\
})\
\
state := newDispatchWizardState()\
state.currentBranch, state.currentBranchErr = getDispatchWizardCurrentBranch(ctx)\
\
form := NewAccessibleForm(\
    huh.NewGroup(\
        huh.NewSelect[string]().\
            Options(\
                huh.NewOption("Local", dispatchWizardModeLocal),\
                huh.NewOption("Cloud", dispatchWizardModeServer),\
            ).\
            Value(&state.modeChoice),\
    ).Title("Mode").Description("Choose where the dispatch should run."),\
    huh.NewGroup(\
        huh.NewMultiSelect[string]().\
            Title("Repos").\
            Description(fmt.Sprintf("Press / to filter. Up to %d repos.", dispatchpkg.CloudRepoLimit)).\
            Filterable(true).\
            OptionsFunc(loadRepos, nil).\
            Value(&state.selectedRepos).\
            Validate(func(value []string) error {\
                selected := normalizeDispatchWizardSelections(value)\
                if len(selected) == 0 {\
                    return errors.New("select at least one repo")\
                }\
                if len(selected) > dispatchpkg.CloudRepoLimit {\
                    return fmt.Errorf("select at most %d repos", dispatchpkg.CloudRepoLimit)\
                }\
                return nil\
            }),\
    ).WithHideFunc(func() bool {\
        return !state.showRepoPicker()\
    }),\
    huh.NewGroup(\
        huh.NewSelect[string]().\
            Options(\
                huh.NewOption("1 day", "1d"),\
                huh.NewOption("7 days", "7d"),\
                huh.NewOption("14 days", "14d"),\
                huh.NewOption("30 days", "30d"),\
            ).\
            Value(&state.timeWindowPreset),\
    ).Title("Window").Description("Choose the time window."),\
    huh.NewGroup(\
        huh.NewSelect[string]().\
            Options(state.localBranchModeOptions()...).\
            Height(0).\
            Value(&state.localBranchMode),\
    ).Title("Branch mode").Description("Choose how local dispatch should interpret branch scope.").\
        WithHideFunc(func() bool {\
            return !state.showLocalBranchMode()\
        }),\
    huh.NewGroup(\
        huh.NewSelect[string]().\
            Options(\
                huh.NewOption("Neutral", "neutral"),\
                huh.NewOption("Marvin", "marvin"),\
                huh.NewOption("Custom", dispatchWizardVoiceCustom),\
            ).\
            Value(&state.voicePreset),\
    ).Title("Voice").Description("Choose a preset voice."),\
    huh.NewGroup(\
        huh.NewInput().\
            Placeholder("Dry, skeptical release note narrator").\
            Value(&state.voiceCustom).\
            Validate(func(value string) error {\
                if state.showCustomVoiceInput() && strings.TrimSpace(value) == "" {\
                    return errors.New("enter a custom voice")\
                }\
                return nil\
            }),\
    ).Title("Custom voice").Description("Describe the dispatch voice.").\
        WithHideFunc(func() bool {\
            return !state.showCustomVoiceInput()\
        }),\
    huh.NewGroup(\
        huh.NewNote().\
            Title("Resolved options").\
            DescriptionFunc(func() string {\
                opts, resolveErr := state.resolve()\
                if resolveErr != nil {\
                    return "Validation error: " + resolveErr.Error()\
                }\
                return buildDispatchWizardSummary(opts, state.previewScope(opts))\
            }, &state),\
        huh.NewNote().\
            Title("Command").\
            DescriptionFunc(func() string {\
                opts, resolveErr := state.resolve()\
                if resolveErr != nil {\
                    return "Validation error: " + resolveErr.Error()\
                }\
                return buildDispatchCommand(opts)\
            }, &state),\
        huh.NewConfirm().\
            Title("Run dispatch?").\
            Affirmative("Run").\
            Negative("Cancel").\
            Value(&state.confirmRun),\
    ).Title("Confirm").Description("Review the resolved command and run it."),\
)\
\
fmt.Fprintln(cmd.OutOrStdout())\
\
if err := runDispatchWizardForm(form); err != nil {\
    if handled := handleFormCancellation(cmd.OutOrStdout(), "dispatch", err); handled == nil {\
        return dispatchpkg.Options{}, errDispatchCancelled\
    }\
    return dispatchpkg.Options{}, fmt.Errorf("run dispatch wizard: %w", err)\
}\
if !state.confirmRun {\
    fmt.Fprintln(cmd.OutOrStdout(), "dispatch cancelled.")\
    return dispatchpkg.Options{}, errDispatchCancelled\
}\
\
return state.resolve()\
```\
\
}\
\
// newLazyOptions returns a func that runs loader once (under sync.Once) and\
// returns the cached result on subsequent calls. Safe for concurrent use.\
func newLazyOptions(loader func() \[\]huh.Option\[string\]) func() \[\]huh.Option\[string\] {\
var (\
once sync.Once\
options \[\]huh.Option\[string\]\
)\
return func() \[\]huh.Option\[string\] {\
once.Do(func() {\
options = loader()\
})\
return options\
}\
}\
\
// buildDispatchRepoOptions dedupes but preserves the caller's order so each\
// source can pick its own order: the API path surfaces recent-first, and the\
// local-discovery fallback surfaces the current repo first.\
func buildDispatchRepoOptions(slugs \[\]string) \[\]huh.Option\[string\] {\
options := make(\[\]huh.Option\[string\], 0, len(slugs))\
seen := make(map\[string\]struct{}, len(slugs))\
for \_, slug := range slugs {\
if slug == "" {\
continue\
}\
if \_, ok := seen\[slug\]; ok {\
continue\
}\
seen\[slug\] = struct{}{}\
options = append(options, huh.NewOption(slug, slug))\
}\
return options\
}\
\
func normalizeDispatchWizardSelections(values \[\]string) \[\]string {\
normalized := make(\[\]string, 0, len(values))\
seen := make(map\[string\]struct{}, len(values))\
for \_, value := range values {\
value = strings.TrimSpace(value)\
if value == "" {\
continue\
}\
if \_, ok := seen\[value\]; ok {\
continue\
}\
seen\[value\] = struct{}{}\
normalized = append(normalized, value)\
}\
return normalized\
}\
\
func discoverLocalRepoRoots(ctx context.Context, currentRepo string) \[\]string {\
rootSet := map\[string\]struct{}{currentRepo: {}}\
parent := filepath.Dir(currentRepo)\
\
```\
entries, err := os.ReadDir(parent)\
if err == nil {\
    candidates := make([]string, 0, len(entries))\
    for _, entry := range entries {\
        if !entry.IsDir() {\
            continue\
        }\
        candidate := filepath.Join(parent, entry.Name())\
        if _, statErr := os.Stat(filepath.Join(candidate, ".git")); statErr != nil {\
            continue\
        }\
        candidates = append(candidates, candidate)\
    }\
\
    resolved := make([]string, len(candidates))\
    sem := make(chan struct{}, dispatchWizardRepoDiscoveryConcurrencyLimit)\
    var wg sync.WaitGroup\
    wg.Add(len(candidates))\
    for i, candidate := range candidates {\
        sem <- struct{}{}\
        go func(i int, candidate string) {\
            defer wg.Done()\
            defer func() { <-sem }()\
            if repoRoot, resolveErr := resolveDispatchWizardTopLevel(ctx, candidate); resolveErr == nil {\
                resolved[i] = repoRoot\
            }\
        }(i, candidate)\
    }\
    wg.Wait()\
    for _, repoRoot := range resolved {\
        if repoRoot != "" {\
            rootSet[repoRoot] = struct{}{}\
        }\
    }\
}\
\
repoRoots := make([]string, 0, len(rootSet))\
for repoRoot := range rootSet {\
    repoRoots = append(repoRoots, repoRoot)\
}\
sort.Slice(repoRoots, func(i, j int) bool {\
    if repoRoots[i] == currentRepo {\
        return true\
    }\
    if repoRoots[j] == currentRepo {\
        return false\
    }\
    return filepath.Base(repoRoots[i]) < filepath.Base(repoRoots[j])\
})\
return repoRoots\
```\
\
}\
\
func discoverLocalRepoSlugs(ctx context.Context, currentRepo string) \[\]string {\
repoRoots := discoverLocalRepoRoots(ctx, currentRepo)\
repoSlugs := make(\[\]string, 0, len(repoRoots))\
seenRepoSlugs := make(map\[string\]struct{}, len(repoRoots))\
for \_, repoRoot := range repoRoots {\
repoSlug := discoverRepoSlug(repoRoot)\
if repoSlug == "" {\
continue\
}\
if \_, ok := seenRepoSlugs\[repoSlug\]; ok {\
continue\
}\
seenRepoSlugs\[repoSlug\] = struct{}{}\
repoSlugs = append(repoSlugs, repoSlug)\
}\
return repoSlugs\
}\
\
func resolveGitTopLevel(ctx context.Context, path string) (string, error) {\
cmd := exec.CommandContext(ctx, "git", "-C", path, "rev-parse", "--show-toplevel")\
output, err := cmd.Output()\
if err != nil {\
return "", fmt.Errorf("git rev-parse --show-toplevel: %w", err)\
}\
return strings.TrimSpace(string(output)), nil\
}\
\
// discoverAuthenticatedDispatchWizardRepos drops repos with zero checkpoints —\
// dispatching them would produce nothing. Server order (recent-first) is\
// preserved.\
func discoverAuthenticatedDispatchWizardRepos(ctx context.Context) (\[\]string, error) {\
repos, err := listDispatchWizardRepoResources(ctx)\
if err != nil {\
logging.Warn(ctx, "dispatch wizard repo list failed", "error", err)\
return nil, err\
}\
\
```\
slugs := make([]string, 0, len(repos))\
for _, repo := range repos {\
    if repo.CheckpointCount <= 0 {\
        continue\
    }\
    slug := strings.TrimSpace(repo.FullName)\
    if slug == "" {\
        continue\
    }\
    slugs = append(slugs, slug)\
}\
return slugs, nil\
```\
\
}\
\
func discoverRepoSlug(repoRoot string) string {\
repo, err := git.PlainOpenWithOptions(repoRoot, &git.PlainOpenOptions{DetectDotGit: true})\
if err != nil {\
return ""\
}\
remote, err := repo.Remote("origin")\
if err != nil \|\| len(remote.Config().URLs) == 0 {\
return ""\
}\
owner, repoName, err := searchpkg.ParseGitHubRemote(remote.Config().URLs\[0\])\
if err != nil {\
return ""\
}\
return owner + "/" + repoName\
}\
\
\-\-\- cmd/entire/cli/doctor.go\
package cli\
\
import (\
"context"\
"errors"\
"fmt"\
"io"\
"os"\
"path/filepath"\
"strconv"\
"time"\
\
```\
"charm.land/huh/v2"\
"github.com/entireio/cli/cmd/entire/cli/agent/codex"\
"github.com/entireio/cli/cmd/entire/cli/checkpoint"\
"github.com/entireio/cli/cmd/entire/cli/checkpoint/remote"\
"github.com/entireio/cli/cmd/entire/cli/paths"\
"github.com/entireio/cli/cmd/entire/cli/session"\
"github.com/entireio/cli/cmd/entire/cli/settings"\
"github.com/entireio/cli/cmd/entire/cli/strategy"\
\
"github.com/go-git/go-git/v6"\
"github.com/go-git/go-git/v6/plumbing"\
"github.com/spf13/cobra"\
```\
\
)\
\
func newDoctorCmd() \*cobra.Command {\
var forceFlag bool\
\
```\
cmd := &cobra.Command{\
    Use:   "doctor",\
    Short: "Diagnose and fix session issues",\
    Long: `Scan for session issues and offer to fix them.\
```\
\
Checks performed:\
\
1. Disconnected metadata branches: detects when local and remote\
entire/checkpoints/v1 branches share no common ancestor (caused by a\
previous bug). Fixes by cherry-picking local checkpoints onto remote tip.\
\
When checkpoints\_v2 is enabled:\
2\. Disconnected v2 /main ref: same detection for v2 refs under refs/entire/.\
3\. v2 ref existence: verifies /main and /full/current refs exist consistently.\
4\. v2 checkpoint counts: verifies /main and /full/current checkpoint counts are consistent.\
5\. v2 generation health: checks archived generations for valid metadata.\
\
When Codex hooks are installed:\
6\. Codex hook trust: warn when hooks declared in .codex/hooks.json\
lack a trusted\_hash entry in the user's Codex config (i.e. /hooks\
review hasn't run yet on this machine, or a newer entire release\
added a hook the user hasn't approved yet).\
\
1. Stuck sessions: sessions stuck in ACTIVE or ENDED phase that need cleanup.\
\
A session is considered stuck if:\
\
- It is in ACTIVE phase with no interaction for over 1 hour\
- It is in ENDED phase with uncondensed checkpoint data on a shadow branch\
\
For each stuck session, you can choose to:\
\
- Condense: Save session data to permanent storage\
- Discard: Remove the session state and shadow branch data\
- Skip: Leave the session as-is\
\
Use --force to condense all fixable sessions without prompting. Sessions that can't\
be condensed will be discarded.\`,\
PreRun: func(\_ \*cobra.Command, \_ \[\]string) {\
strategy.EnsureRedactionConfigured()\
},\
RunE: func(cmd \*cobra.Command, \_ \[\]string) error {\
return runSessionsFix(cmd, forceFlag)\
},\
}\
\
```\
cmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Auto-fix all issues without prompting")\
\
// Diagnostic subcommands.\
cmd.AddCommand(newTraceCmd())\
cmd.AddCommand(newDoctorLogsCmd())\
cmd.AddCommand(newDoctorBundleCmd())\
\
return cmd\
```\
\
}\
\
// stuckSession holds a session state along with diagnostic info.\
type stuckSession struct {\
State \*strategy.SessionState\
Reason string\
ShadowBranch string\
HasShadowBranch bool\
CheckpointCount int\
FilesTouchedCount int\
}\
\
func runSessionsFix(cmd \*cobra.Command, force bool) error {\
var finalErr error\
\
```\
// Check 1: Disconnected metadata branches\
metadataErr := checkDisconnectedMetadata(cmd, force)\
if metadataErr != nil {\
    fmt.Fprintf(cmd.ErrOrStderr(), "Error: metadata check failed: %v\n", metadataErr)\
    finalErr = NewSilentError(fmt.Errorf("metadata check failed: %w", metadataErr))\
}\
fmt.Fprintln(cmd.OutOrStdout())\
\
// v2 checks (only when checkpoints_v2 is enabled)\
ctx := cmd.Context()\
if settings.IsCheckpointsV2Enabled(ctx) {\
    // Check 2: Disconnected v2 /main ref\
    v2DisconnectedErr := checkDisconnectedV2Main(cmd, force)\
    if v2DisconnectedErr != nil {\
        fmt.Fprintf(cmd.ErrOrStderr(), "Error: v2 /main check failed: %v\n", v2DisconnectedErr)\
        if finalErr == nil {\
            finalErr = NewSilentError(fmt.Errorf("v2 /main check failed: %w", v2DisconnectedErr))\
        }\
    }\
\
    repo, repoErr := openRepository(ctx)\
    if repoErr != nil {\
        fmt.Fprintf(cmd.ErrOrStderr(), "Error: could not open repository for v2 checks: %v\n", repoErr)\
        if finalErr == nil {\
            finalErr = NewSilentError(fmt.Errorf("v2 checks failed: %w", repoErr))\
        }\
    } else {\
        // Check 3: v2 ref existence\
        if refErr := checkV2RefExistence(cmd, repo); refErr != nil {\
            fmt.Fprintf(cmd.ErrOrStderr(), "Error: v2 ref existence check failed: %v\n", refErr)\
            if finalErr == nil {\
                finalErr = NewSilentError(fmt.Errorf("v2 ref check failed: %w", refErr))\
            }\
        }\
\
        // Check 4: v2 checkpoint count consistency\
        if countErr := checkV2CheckpointCounts(cmd, repo); countErr != nil {\
            fmt.Fprintf(cmd.ErrOrStderr(), "Error: v2 checkpoint count check failed: %v\n", countErr)\
            if finalErr == nil {\
                finalErr = NewSilentError(fmt.Errorf("v2 count check failed: %w", countErr))\
            }\
        }\
\
        // Check 5: v2 generation health\
        if genErr := checkV2GenerationHealth(cmd, repo); genErr != nil {\
            fmt.Fprintf(cmd.ErrOrStderr(), "Error: v2 generation health check failed: %v\n", genErr)\
            if finalErr == nil {\
                finalErr = NewSilentError(fmt.Errorf("v2 generation check failed: %w", genErr))\
            }\
        }\
    }\
\
    fmt.Fprintln(cmd.OutOrStdout())\
}\
\
// Agent-specific: Codex hook trust state.\
checkCodexHookTrust(cmd)\
\
// Stuck sessions\
// Load all session states\
states, err := strategy.ListSessionStates(ctx)\
if err != nil {\
    return fmt.Errorf("failed to list session states: %w", err)\
}\
\
if len(states) == 0 {\
    fmt.Fprintln(cmd.OutOrStdout(), "No stuck sessions found.")\
    if finalErr != nil {\
        return finalErr\
    }\
    return nil\
}\
\
// Open repository to check shadow branches (uses worktree-aware helper)\
repo, err := openRepository(ctx)\
if err != nil {\
    return fmt.Errorf("failed to open repository: %w", err)\
}\
\
// Identify stuck sessions\
now := time.Now()\
var stuck []stuckSession\
\
for _, state := range states {\
    ss := classifySession(state, repo, now)\
    if ss != nil {\
        stuck = append(stuck, *ss)\
    }\
}\
\
if len(stuck) == 0 {\
    fmt.Fprintln(cmd.OutOrStdout(), "No stuck sessions found.")\
    if finalErr != nil {\
        return finalErr\
    }\
    return nil\
}\
\
// Get the current strategy for condense operations\
strat := GetStrategy(ctx)\
\
fmt.Fprintf(cmd.OutOrStdout(), "Found %d stuck session(s):\n\n", len(stuck))\
\
for _, ss := range stuck {\
    displayStuckSession(cmd, ss)\
\
    if force {\
        if ss.HasShadowBranch && ss.CheckpointCount > 0 {\
            if err := strat.CondenseSessionByID(ctx, ss.State.SessionID); err != nil {\
                fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to condense session %s: %v\n", ss.State.SessionID, err)\
            } else {\
                fmt.Fprintf(cmd.OutOrStdout(), "  ✓ Condensed session %s\n\n", ss.State.SessionID)\
            }\
        } else {\
            // Discard if we can't condense\
            if err := discardSession(ctx, ss, repo, cmd.ErrOrStderr()); err != nil {\
                fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to discard session %s: %v\n", ss.State.SessionID, err)\
            } else {\
                fmt.Fprintf(cmd.OutOrStdout(), "  ✓ Discarded session %s\n\n", ss.State.SessionID)\
            }\
        }\
        continue\
    }\
\
    // Interactive: prompt for action\
    action, err := promptSessionAction(ss)\
    if err != nil {\
        if errors.Is(err, huh.ErrUserAborted) {\
            return nil\
        }\
        return fmt.Errorf("failed to get action: %w", err)\
    }\
\
    switch action {\
    case "condense":\
        if err := strat.CondenseSessionByID(ctx, ss.State.SessionID); err != nil {\
            fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to condense session %s: %v\n", ss.State.SessionID, err)\
        } else {\
            fmt.Fprintf(cmd.OutOrStdout(), "  ✓ Condensed session %s\n\n", ss.State.SessionID)\
        }\
    case "discard":\
        if err := discardSession(ctx, ss, repo, cmd.ErrOrStderr()); err != nil {\
            fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to discard session %s: %v\n", ss.State.SessionID, err)\
        } else {\
            fmt.Fprintf(cmd.OutOrStdout(), "  ✓ Discarded session %s\n\n", ss.State.SessionID)\
        }\
    case "skip":\
        fmt.Fprintf(cmd.OutOrStdout(), "  -> Skipped\n\n")\
    }\
}\
\
if finalErr != nil {\
    return finalErr\
}\
\
return nil\
```\
\
}\
\
// classifySession determines if a session is stuck and returns diagnostic info.\
// Returns nil if the session is healthy.\
func classifySession(state \*strategy.SessionState, repo \*git.Repository, now time.Time) \*stuckSession {\
// Determine shadow branch info\
shadowBranch := checkpoint.ShadowBranchNameForCommit(state.BaseCommit, state.WorktreeID)\
refName := plumbing.NewBranchReferenceName(shadowBranch)\
\_, refErr := repo.Reference(refName, true)\
hasShadowBranch := refErr == nil\
\
```\
switch {\
case state.Phase.IsActive():\
    if !state.IsStuckActive() {\
        return nil\
    }\
\
    var reason string\
    if state.LastInteractionTime != nil {\
        reason = fmt.Sprintf("active, last interaction %s ago", now.Sub(*state.LastInteractionTime).Truncate(time.Minute))\
    } else {\
        reason = fmt.Sprintf("active, started %s ago with no recorded interaction", now.Sub(state.StartedAt).Truncate(time.Minute))\
    }\
\
    return &stuckSession{\
        State:             state,\
        Reason:            reason,\
        ShadowBranch:      shadowBranch,\
        HasShadowBranch:   hasShadowBranch,\
        CheckpointCount:   state.StepCount,\
        FilesTouchedCount: len(state.FilesTouched),\
    }\
\
case state.Phase == session.PhaseEnded:\
    // Ended sessions are stuck if they have uncondensed data\
    if state.StepCount <= 0 || !hasShadowBranch {\
        return nil\
    }\
\
    return &stuckSession{\
        State:             state,\
        Reason:            "ended with uncondensed checkpoint data",\
        ShadowBranch:      shadowBranch,\
        HasShadowBranch:   hasShadowBranch,\
        CheckpointCount:   state.StepCount,\
        FilesTouchedCount: len(state.FilesTouched),\
    }\
\
default:\
    return nil\
}\
```\
\
}\
\
// displayStuckSession prints diagnostic info for a stuck session.\
func displayStuckSession(cmd \*cobra.Command, ss stuckSession) {\
w := cmd.OutOrStdout()\
\
```\
fmt.Fprintf(w, "  Session: %s\n", ss.State.SessionID)\
fmt.Fprintf(w, "  Phase:   %s\n", ss.State.Phase)\
fmt.Fprintf(w, "  Reason:  %s\n", ss.Reason)\
\
if ss.State.AgentType != "" {\
    fmt.Fprintf(w, "  Agent:   %s\n", ss.State.AgentType)\
}\
\
if ss.State.LastInteractionTime != nil {\
    fmt.Fprintf(w, "  Last interaction: %s\n", ss.State.LastInteractionTime.Format(time.RFC3339))\
}\
\
shadowStatus := "not found"\
if ss.HasShadowBranch {\
    shadowStatus = fmt.Sprintf("exists (%s)", ss.ShadowBranch)\
}\
fmt.Fprintf(w, "  Shadow branch: %s\n", shadowStatus)\
fmt.Fprintf(w, "  Checkpoints: %d, Files touched: %d\n", ss.CheckpointCount, ss.FilesTouchedCount)\
```\
\
}\
\
// promptSessionAction asks the user what to do with a stuck session.\
func promptSessionAction(ss stuckSession) (string, error) {\
var action string\
\
```\
options := make([]huh.Option[string], 0, 3)\
if ss.HasShadowBranch && ss.CheckpointCount > 0 {\
    options = append(options, huh.NewOption("Condense (save to permanent storage)", "condense"))\
}\
options = append(options,\
    huh.NewOption("Discard (remove session data)", "discard"),\
    huh.NewOption("Skip (leave as-is)", "skip"),\
)\
\
form := NewAccessibleForm(\
    huh.NewGroup(\
        huh.NewSelect[string]().\
            Title(fmt.Sprintf("Fix session %s?", ss.State.SessionID)).\
            Options(options...).\
            Value(&action),\
    ),\
)\
\
if err := form.Run(); err != nil {\
    return "", fmt.Errorf("session fix prompt failed: %w", err)\
}\
\
return action, nil\
```\
\
}\
\
// discardSession removes session state and cleans up the shadow branch.\
func discardSession(ctx context.Context, ss stuckSession, \_ \*git.Repository, errW io.Writer) error {\
// Clear session state file\
if err := strategy.ClearSessionState(ctx, ss.State.SessionID); err != nil {\
return fmt.Errorf("failed to clear session state: %w", err)\
}\
\
```\
// Delete shadow branch if it exists and no other sessions need it\
if ss.HasShadowBranch {\
    if shouldDelete, err := canDeleteShadowBranch(ctx, ss.ShadowBranch, ss.State.SessionID); err != nil {\
        fmt.Fprintf(errW, "Warning: could not check other sessions for shadow branch: %v\n", err)\
    } else if shouldDelete {\
        if err := strategy.DeleteBranchCLI(ctx, ss.ShadowBranch); err != nil {\
            // Branch already gone is not an error — keeps discard idempotent\
            if !errors.Is(err, strategy.ErrBranchNotFound) {\
                return fmt.Errorf("failed to delete shadow branch: %w", err)\
            }\
        }\
    }\
}\
\
return nil\
```\
\
}\
\
// checkDisconnectedMetadata detects and optionally repairs disconnected\
// local/remote metadata branches (the "empty-orphan bug").\
func checkDisconnectedMetadata(cmd \*cobra.Command, force bool) error {\
repo, err := openRepository(cmd.Context())\
if err != nil {\
return fmt.Errorf("failed to open repository: %w", err)\
}\
\
```\
ctx := cmd.Context()\
remoteRefName := plumbing.NewRemoteReferenceName("origin", paths.MetadataBranchName)\
disconnected, err := strategy.IsMetadataDisconnected(ctx, repo, remoteRefName)\
if err != nil {\
    return fmt.Errorf("could not check metadata branch state: %w", err)\
}\
\
w := cmd.OutOrStdout()\
\
if !disconnected {\
    fmt.Fprintln(w, "✓ Metadata branches: OK")\
    return nil\
}\
\
fmt.Fprintln(w, "Metadata branches: DISCONNECTED")\
fmt.Fprintln(w, "  Local and remote entire/checkpoints/v1 branches share no common ancestor.")\
fmt.Fprintln(w, "  Some remote checkpoints may not be visible locally.")\
fmt.Fprintln(w, "  Fix: cherry-pick local checkpoints onto remote tip (preserves all data).")\
\
if !force {\
    var confirmed bool\
    form := NewAccessibleForm(\
        huh.NewGroup(\
            huh.NewConfirm().\
                Title("Fix disconnected metadata branches?").\
                Value(&confirmed),\
        ),\
    )\
    if formErr := form.Run(); formErr != nil {\
        if errors.Is(formErr, huh.ErrUserAborted) {\
            return nil\
        }\
        return fmt.Errorf("prompt failed: %w", formErr)\
    }\
    if !confirmed {\
        fmt.Fprintln(w, "  -> Skipped")\
        return nil\
    }\
}\
\
if fixErr := strategy.ReconcileDisconnectedMetadataBranch(ctx, repo, remoteRefName, cmd.ErrOrStderr()); fixErr != nil {\
    return fmt.Errorf("failed to reconcile metadata branches: %w", fixErr)\
}\
\
fmt.Fprintln(w, "  ✓ Fixed: metadata branches reconciled")\
return nil\
```\
\
}\
\
// checkDisconnectedV2Main detects and optionally repairs disconnected\
// local/remote v2 /main refs.\
func checkDisconnectedV2Main(cmd \*cobra.Command, force bool) error {\
repo, err := openRepository(cmd.Context())\
if err != nil {\
return fmt.Errorf("failed to open repository: %w", err)\
}\
\
```\
ctx := cmd.Context()\
configured := remote.Configured(ctx)\
remoteName := "origin"\
if configured {\
    resolvedRemote, resolveErr := remote.FetchURL(ctx)\
    if resolveErr != nil {\
        return fmt.Errorf("checkpoint_remote is configured but could not be resolved: %w", resolveErr)\
    }\
    remoteName = resolvedRemote\
}\
\
disconnected, err := strategy.IsV2MainDisconnected(ctx, repo, remoteName)\
if err != nil {\
    // If no checkpoint_remote is configured and origin doesn't exist or is\
    // unreachable, treat as "can't check" rather than a hard failure — mirrors\
    // the v1 behavior which no-ops when the remote-tracking ref is absent.\
    if !configured {\
        fmt.Fprintln(cmd.OutOrStdout(), "✓ v2 /main ref: OK (no remote to compare)")\
        return nil\
    }\
    return fmt.Errorf("could not check v2 /main ref state: %w", err)\
}\
\
w := cmd.OutOrStdout()\
\
if !disconnected {\
    fmt.Fprintln(w, "✓ v2 /main ref: OK")\
    return nil\
}\
\
fmt.Fprintln(w, "v2 /main ref: DISCONNECTED")\
fmt.Fprintln(w, "  Local and remote v2 /main refs share no common ancestor.")\
fmt.Fprintln(w, "  Fix: cherry-pick local checkpoints onto remote tip (preserves all data).")\
\
if !force {\
    var confirmed bool\
    form := NewAccessibleForm(\
        huh.NewGroup(\
            huh.NewConfirm().\
                Title("Fix disconnected v2 /main ref?").\
                Value(&confirmed),\
        ),\
    )\
    if formErr := form.Run(); formErr != nil {\
        if errors.Is(formErr, huh.ErrUserAborted) {\
            return nil\
        }\
        return fmt.Errorf("prompt failed: %w", formErr)\
    }\
    if !confirmed {\
        fmt.Fprintln(w, "  -> Skipped")\
        return nil\
    }\
}\
\
if fixErr := strategy.ReconcileDisconnectedV2Ref(ctx, repo, remoteName, cmd.ErrOrStderr()); fixErr != nil {\
    return fmt.Errorf("failed to reconcile v2 /main ref: %w", fixErr)\
}\
\
fmt.Fprintln(w, "  ✓ Fixed: v2 /main ref reconciled")\
return nil\
```\
\
}\
\
// checkV2GenerationHealth verifies that archived /full/\* generations are well-formed.\
// Checks: generation.json exists and is valid, timestamps are sane, generation has checkpoints,\
// and generation sequence numbers are contiguous.\
func checkV2GenerationHealth(cmd \*cobra.Command, repo \*git.Repository) error {\
ctx := cmd.Context()\
w := cmd.OutOrStdout()\
\
```\
v2Store := checkpoint.NewV2GitStore(repo, "origin")\
\
archived, err := v2Store.ListArchivedGenerations()\
if err != nil {\
    return fmt.Errorf("failed to list archived generations: %w", err)\
}\
\
if len(archived) == 0 {\
    fmt.Fprintln(w, "✓ v2 generations: OK (no archived generations)")\
    return nil\
}\
\
var warnings []string\
\
for _, genName := range archived {\
    refName := plumbing.ReferenceName(paths.V2FullRefPrefix + genName)\
\
    _, treeHash, refErr := v2Store.GetRefState(refName)\
    if refErr != nil {\
        warnings = append(warnings, fmt.Sprintf("generation %s: cannot read ref: %v", genName, refErr))\
        continue\
    }\
\
    gen, genErr := v2Store.ReadGeneration(treeHash)\
    if genErr != nil {\
        warnings = append(warnings, fmt.Sprintf("generation %s: failed to read generation.json: %v", genName, genErr))\
        continue\
    }\
\
    hasOldest := !gen.OldestCheckpointAt.IsZero()\
    hasNewest := !gen.NewestCheckpointAt.IsZero()\
\
    switch {\
    case !hasOldest && !hasNewest:\
        // ReadGeneration returns zero-value when the file is absent\
        warnings = append(warnings, fmt.Sprintf("generation %s: WARNING — missing generation.json", genName))\
    case hasOldest != hasNewest:\
        warnings = append(warnings, fmt.Sprintf("generation %s: WARNING — incomplete generation.json (partial timestamps)", genName))\
    case gen.OldestCheckpointAt.After(gen.NewestCheckpointAt):\
        warnings = append(warnings, fmt.Sprintf("generation %s: WARNING — invalid timestamps (oldest > newest)", genName))\
    }\
\
    cpCount, countErr := v2Store.CountCheckpointsInTree(ctx, treeHash)\
    if countErr != nil {\
        warnings = append(warnings, fmt.Sprintf("generation %s: failed to count checkpoints: %v", genName, countErr))\
        continue\
    }\
    if cpCount == 0 {\
        warnings = append(warnings, fmt.Sprintf("generation %s: WARNING — empty (no checkpoint shards)", genName))\
    }\
}\
\
if len(archived) > 1 {\
    for i := 1; i < len(archived); i++ {\
        prev, prevErr := strconv.ParseInt(archived[i-1], 10, 64)\
        curr, currErr := strconv.ParseInt(archived[i], 10, 64)\
        if prevErr != nil || currErr != nil {\
            continue\
        }\
        if curr-prev > 1 {\
            first := prev + 1\
            last := curr - 1\
            if first == last {\
                warnings = append(warnings, fmt.Sprintf("INFO — gap in generation sequence (%013d missing)", first))\
            } else {\
                warnings = append(warnings, fmt.Sprintf("INFO — gap in generation sequence (%013d–%013d missing)", first, last))\
            }\
        }\
    }\
}\
\
if len(warnings) > 0 {\
    fmt.Fprintf(w, "v2 generations: %d issue(s) found in %d archived generation(s):\n", len(warnings), len(archived))\
    for _, warning := range warnings {\
        fmt.Fprintf(w, "  %s\n", warning)\
    }\
    return fmt.Errorf("v2 generation health: %d issue(s) found", len(warnings))\
}\
\
fmt.Fprintf(w, "✓ v2 generations: OK (%d archived)\n", len(archived))\
return nil\
```\
\
}\
\
// checkV2CheckpointCounts verifies checkpoint count consistency between /main and /full/current.\
// /main is permanent (accumulates all checkpoints), /full/current holds only the current generation.\
// So main count >= full/current count. If full/current exceeds main, a dual-write partially failed.\
// Skips silently if either ref doesn't exist (already covered by checkV2RefExistence).\
func checkV2CheckpointCounts(cmd \*cobra.Command, repo \*git.Repository) error {\
ctx := cmd.Context()\
w := cmd.OutOrStdout()\
\
```\
v2Store := checkpoint.NewV2GitStore(repo, "origin")\
\
mainRefName := plumbing.ReferenceName(paths.V2MainRefName)\
fullRefName := plumbing.ReferenceName(paths.V2FullCurrentRefName)\
\
_, mainTreeHash, mainErr := v2Store.GetRefState(mainRefName)\
_, fullTreeHash, fullErr := v2Store.GetRefState(fullRefName)\
\
// Skip only when ref is missing (already covered by checkV2RefExistence).\
if mainErr != nil {\
    if errors.Is(mainErr, plumbing.ErrReferenceNotFound) {\
        return nil\
    }\
    return fmt.Errorf("failed to read /main ref: %w", mainErr)\
}\
if fullErr != nil {\
    if errors.Is(fullErr, plumbing.ErrReferenceNotFound) {\
        return nil\
    }\
    return fmt.Errorf("failed to read /full/current ref: %w", fullErr)\
}\
\
mainCount, err := v2Store.CountCheckpointsInTree(ctx, mainTreeHash)\
if err != nil {\
    return fmt.Errorf("failed to count /main checkpoints: %w", err)\
}\
\
fullCount, err := v2Store.CountCheckpointsInTree(ctx, fullTreeHash)\
if err != nil {\
    return fmt.Errorf("failed to count /full/current checkpoints: %w", err)\
}\
\
if fullCount > mainCount {\
    fmt.Fprintf(w, "v2 checkpoint counts: INCONSISTENT — /full/current has %d checkpoints but /main has only %d\n", fullCount, mainCount)\
    return fmt.Errorf("v2 checkpoint counts inconsistent: /full/current (%d) exceeds /main (%d)", fullCount, mainCount)\
}\
\
fmt.Fprintf(w, "✓ v2 checkpoint counts: OK (main: %d, full/current: %d)\n", mainCount, fullCount)\
return nil\
```\
\
}\
\
// checkV2RefExistence verifies that v2 refs exist (or both are absent for a fresh repo).\
// One ref without the other suggests a partial initialization.\
func checkV2RefExistence(cmd \*cobra.Command, repo \*git.Repository) error {\
w := cmd.OutOrStdout()\
\
```\
mainRefName := plumbing.ReferenceName(paths.V2MainRefName)\
fullRefName := plumbing.ReferenceName(paths.V2FullCurrentRefName)\
\
_, mainErr := repo.Reference(mainRefName, true)\
_, fullErr := repo.Reference(fullRefName, true)\
if mainErr != nil && !errors.Is(mainErr, plumbing.ErrReferenceNotFound) {\
    return fmt.Errorf("failed to read /main ref: %w", mainErr)\
}\
if fullErr != nil && !errors.Is(fullErr, plumbing.ErrReferenceNotFound) {\
    return fmt.Errorf("failed to read /full/current ref: %w", fullErr)\
}\
\
hasMain := mainErr == nil\
hasFull := fullErr == nil\
\
switch {\
case hasMain && hasFull:\
    fmt.Fprintln(w, "✓ v2 refs: OK")\
case !hasMain && !hasFull:\
    fmt.Fprintln(w, "✓ v2 refs: OK (no checkpoints written yet)")\
case hasMain && !hasFull:\
    fmt.Fprintln(w, "v2 refs: INCONSISTENT — /main exists but /full/current is missing")\
    return errors.New("v2 refs inconsistent: /main exists but /full/current is missing")\
case !hasMain && hasFull:\
    fmt.Fprintln(w, "v2 refs: INCONSISTENT — /full/current exists but /main is missing")\
    return errors.New("v2 refs inconsistent: /full/current exists but /main is missing")\
}\
\
return nil\
```\
\
}\
\
// checkCodexHookTrust warns about two kinds of drift in the Codex hook\
// setup:\
//\
// 1\. .codex/hooks.json is stale relative to what the CLI installs\
// today (e.g. a release added PostToolUse after the user enabled\
// Codex). Fix: re-run `entire enable`.\
//\
// 2\. A declared hook lacks a `trusted_hash` entry in the user's Codex\
// config — either a fresh clone or a newer hook on the file the\
// user hasn't approved yet. Fix: open /hooks in Codex.\
//\
// Both checks are structural (file/key presence). Stays silent when\
// this repo doesn't have codex hooks installed or when we can't\
// resolve the worktree root. Warn-only.\
func checkCodexHookTrust(cmd \*cobra.Command) {\
repoRoot, err := paths.WorktreeRoot(cmd.Context())\
if err != nil {\
return\
}\
if \_, statErr := os.Stat(filepath.Join(repoRoot, ".codex", "hooks.json")); statErr != nil {\
return\
}\
\
```\
w := cmd.OutOrStdout()\
missing := codex.MissingEntireHooks(repoRoot)\
gaps := codex.HookTrustGaps(repoRoot)\
\
if len(missing) == 0 && len(gaps) == 0 {\
    fmt.Fprintln(w, "✓ Codex hook trust: OK")\
    return\
}\
\
if len(missing) > 0 {\
    fmt.Fprintln(w, "Codex hooks: OUT OF DATE")\
    fmt.Fprintf(w, "  %d hook(s) the CLI installs today aren't declared in .codex/hooks.json:\n", len(missing))\
    for _, ev := range missing {\
        fmt.Fprintf(w, "    - %s\n", ev)\
    }\
    fmt.Fprintln(w, "  Run `entire enable` to refresh the hooks file.")\
}\
\
if len(gaps) > 0 {\
    fmt.Fprintln(w, "Codex hook trust:\
```\
\
...\[truncated\]\
\
\-\-\- cmd/entire/cli/doctor\_bundle.go\
package cli\
\
import (\
"archive/zip"\
"context"\
"errors"\
"fmt"\
"io"\
"os"\
"os/exec"\
"path"\
"path/filepath"\
"runtime"\
"strings"\
"time"\
\
```\
"github.com/entireio/cli/cmd/entire/cli/logging"\
"github.com/entireio/cli/cmd/entire/cli/paths"\
"github.com/entireio/cli/cmd/entire/cli/versioninfo"\
"github.com/entireio/cli/redact"\
"github.com/spf13/cobra"\
```\
\
)\
\
func newDoctorBundleCmd() \*cobra.Command {\
var outFlag string\
var rawFlag bool\
\
```\
cmd := &cobra.Command{\
    Use:   "bundle",\
    Short: "Produce a diagnostic bundle (zip) for bug reports — secrets are redacted by default",\
    Long: `Produce a zip archive containing logs, settings, and a git snapshot suitable\
```\
\
for attaching to bug reports.\
\
The archive includes:\
\
- logs/ (operational logs from .entire/logs/)\
- settings/settings.json and settings/settings.local.json (if present)\
- git-status.txt, git-log.txt, git-remote.txt\
- version.txt with CLI version, Go version, OS/Arch\
\
Redaction:\
By default the bundle redacts known secrets (API keys, credentialed URIs,\
database connection strings, bounded KEY=value credentials) from log files,\
settings JSON, and git command output before zipping. Pass --raw to skip\
redaction; use it only when support has explicitly requested an unredacted\
bundle.\
\
By default the archive is written to a path inside the OS temp directory and\
that path is printed to stdout. Use --out to choose a specific path.\`,\
RunE: func(cmd \*cobra.Command, \_ \[\]string) error {\
ctx := cmd.Context()\
repoRoot, err := paths.WorktreeRoot(ctx)\
if err != nil {\
cmd.SilenceUsage = true\
return errors.New("not a git repository")\
}\
\
```\
        outPath := outFlag\
        if outPath == "" {\
            outPath = filepath.Join(os.TempDir(), fmt.Sprintf("entire-bundle-%s.zip", time.Now().UTC().Format("20060102-150405")))\
        }\
\
        if err := writeDoctorBundle(ctx, repoRoot, outPath, rawFlag); err != nil {\
            return err\
        }\
\
        if rawFlag {\
            fmt.Fprintf(cmd.ErrOrStderr(), "Bundle written (RAW — contains unredacted contents): %s\n", outPath)\
        } else {\
            fmt.Fprintf(cmd.ErrOrStderr(), "Bundle written (redacted): %s\n", outPath)\
        }\
        fmt.Fprintln(cmd.OutOrStdout(), outPath)\
        return nil\
    },\
}\
\
cmd.Flags().StringVarP(&outFlag, "out", "o", "", "Path to write the bundle archive (default: OS temp dir)")\
cmd.Flags().BoolVar(&rawFlag, "raw", false, "Skip secret redaction. The archive will contain raw log lines, settings, and git output. Use only when support has asked for an unredacted bundle.")\
return cmd\
```\
\
}\
\
func writeDoctorBundle(ctx context.Context, repoRoot, outPath string, raw bool) error {\
out, err := os.OpenFile(outPath, os.O\_WRONLY\|os.O\_CREATE\|os.O\_TRUNC, 0o600) //nolint:gosec // user-provided output path is intentional\
if err != nil {\
return fmt.Errorf("create bundle: %w", err)\
}\
if err := out.Chmod(0o600); err != nil {\
\_ = out.Close()\
return fmt.Errorf("set bundle permissions: %w", err)\
}\
fileClosed := false\
defer func() {\
if !fileClosed {\
\_ = out.Close()\
}\
}()\
\
```\
zw := zip.NewWriter(out)\
zipClosed := false\
defer func() {\
    if !zipClosed {\
        _ = zw.Close()\
    }\
}()\
\
logsDir := filepath.Join(repoRoot, logging.LogsDir)\
if err := addDirToZip(zw, logsDir, "logs", raw); err != nil {\
    return err\
}\
\
for _, name := range []string{"settings.json", "settings.local.json"} {\
    src := filepath.Join(repoRoot, ".entire", name)\
    if err := addFileToZip(zw, src, path.Join("settings", name), raw); err != nil {\
        return err\
    }\
}\
\
if err := addCommandOutput(ctx, zw, "git-status.txt", repoRoot, raw, "git", "status", "--short", "--branch"); err != nil {\
    return err\
}\
if err := addCommandOutput(ctx, zw, "git-log.txt", repoRoot, raw, "git", "log", "-n", "50", "--oneline"); err != nil {\
    return err\
}\
if err := addCommandOutput(ctx, zw, "git-remote.txt", repoRoot, raw, "git", "remote", "-v"); err != nil {\
    return err\
}\
\
if err := addStringToZip(zw, "version.txt", versionInfoString(), raw); err != nil {\
    return err\
}\
\
if err := zw.Close(); err != nil {\
    return fmt.Errorf("finalize bundle: %w", err)\
}\
zipClosed = true\
\
if err := out.Close(); err != nil {\
    return fmt.Errorf("close bundle: %w", err)\
}\
fileClosed = true\
\
return nil\
```\
\
}\
\
func versionInfoString() string {\
var sb strings.Builder\
fmt.Fprintf(&sb, "Entire CLI %s (%s)\\n", versioninfo.Version, versioninfo.Commit)\
fmt.Fprintf(&sb, "Go: %s\\n", runtime.Version())\
fmt.Fprintf(&sb, "OS/Arch: %s/%s\\n", runtime.GOOS, runtime.GOARCH)\
return sb.String()\
}\
\
func addDirToZip(zw \*zip.Writer, srcDir, archivePrefix string, raw bool) error {\
info, err := os.Stat(srcDir)\
if err != nil {\
if errors.Is(err, os.ErrNotExist) {\
return nil\
}\
return fmt.Errorf("stat %s: %w", srcDir, err)\
}\
if !info.IsDir() {\
return nil\
}\
walkErr := filepath.Walk(srcDir, func(path string, fi os.FileInfo, werr error) error {\
if werr != nil {\
return werr\
}\
if fi.IsDir() {\
return nil\
}\
rel, err := filepath.Rel(srcDir, path)\
if err != nil {\
return fmt.Errorf("rel: %w", err)\
}\
return addFileToZip(zw, path, zipEntryName(archivePrefix, rel), raw)\
})\
if walkErr != nil {\
return fmt.Errorf("walk %s: %w", srcDir, walkErr)\
}\
return nil\
}\
\
func zipEntryName(parts ...string) string {\
cleanParts := make(\[\]string, 0, len(parts))\
for \_, part := range parts {\
if part == "" {\
continue\
}\
cleanParts = append(cleanParts, filepath.ToSlash(part))\
}\
return path.Join(cleanParts...)\
}\
\
func addFileToZip(zw \*zip.Writer, src, archivePath string, raw bool) error {\
f, err := os.Open(src) //nolint:gosec // path comes from repo-internal walk\
if err != nil {\
if errors.Is(err, os.ErrNotExist) {\
return nil\
}\
return fmt.Errorf("open %s: %w", src, err)\
}\
defer f.Close()\
\
```\
entryName := zipEntryName(archivePath)\
w, err := zw.Create(entryName)\
if err != nil {\
    return fmt.Errorf("zip create %s: %w", entryName, err)\
}\
\
if raw {\
    if _, err := io.Copy(w, f); err != nil {\
        return fmt.Errorf("zip copy %s: %w", entryName, err)\
    }\
    return nil\
}\
\
contents, err := io.ReadAll(f)\
if err != nil {\
    return fmt.Errorf("read %s: %w", src, err)\
}\
redacted := redactBundleEntry(entryName, contents)\
if _, err := w.Write(redacted); err != nil {\
    return fmt.Errorf("zip write %s: %w", entryName, err)\
}\
return nil\
```\
\
}\
\
func addStringToZip(zw \*zip.Writer, archivePath, contents string, raw bool) error {\
entryName := zipEntryName(archivePath)\
w, err := zw.Create(entryName)\
if err != nil {\
return fmt.Errorf("zip create %s: %w", entryName, err)\
}\
body := contents\
if !raw {\
body = string(redactBundleEntry(entryName, \[\]byte(contents)))\
}\
if \_, err := io.WriteString(w, body); err != nil {\
return fmt.Errorf("zip write %s: %w", entryName, err)\
}\
return nil\
}\
\
func addCommandOutput(ctx context.Context, zw \*zip.Writer, archivePath, dir string, raw bool, name string, args ...string) error {\
cmd := exec.CommandContext(ctx, name, args...)\
cmd.Dir = dir\
out, err := cmd.CombinedOutput()\
if err != nil {\
out = append(out, \[\]byte(fmt.Sprintf("\\n\[error: %v\]\\n", err))...)\
}\
// addStringToZip applies redaction when raw=false; pass through verbatim otherwise.\
return addStringToZip(zw, archivePath, string(out), raw)\
}\
\
// redactBundleEntry chooses a redaction strategy per file shape. JSON / JSONL\
// entries get the field-aware redactor (preserves structure, skips ID fields);\
// everything else uses the byte-level scrubber.\
func redactBundleEntry(entryName string, contents \[\]byte) \[\]byte {\
ext := strings.ToLower(path.Ext(entryName))\
if ext == ".json" \|\| ext == ".jsonl" {\
out, err := redact.JSONLContent(string(contents))\
if err == nil {\
return \[\]byte(out)\
}\
// Fall through to plain redaction if the JSON redactor refuses (malformed input, etc.)\
}\
return redact.Bytes(contents)\
}\
\
\-\-\- cmd/entire/cli/doctor\_logs.go\
package cli\
\
import (\
"bufio"\
"context"\
"errors"\
"fmt"\
"io"\
"os"\
"path/filepath"\
"time"\
\
```\
"github.com/entireio/cli/cmd/entire/cli/logging"\
"github.com/entireio/cli/cmd/entire/cli/paths"\
"github.com/spf13/cobra"\
```\
\
)\
\
func newDoctorLogsCmd() \*cobra.Command {\
var tail int\
var follow bool\
\
```\
cmd := &cobra.Command{\
    Use:   "logs",\
    Short: "Show recent operational logs",\
    Long: `Print operational logs from .entire/logs/entire.log.\
```\
\
By default, prints the last 100 lines. Use --tail N to change.\
Use --follow to stream new lines as they are written (Ctrl+C to exit).\`,\
RunE: func(cmd \*cobra.Command, \_ \[\]string) error {\
repoRoot, err := paths.WorktreeRoot(cmd.Context())\
if err != nil {\
cmd.SilenceUsage = true\
return errors.New("not a git repository")\
}\
logFile := filepath.Join(repoRoot, logging.LogsDir, "entire.log")\
if \_, err := os.Stat(logFile); errors.Is(err, os.ErrNotExist) {\
fmt.Fprintf(cmd.OutOrStdout(), "No log file at %s yet.\\n", logFile)\
return nil\
}\
if err := printTail(cmd.OutOrStdout(), logFile, tail); err != nil {\
return err\
}\
if !follow {\
return nil\
}\
return followFile(cmd.Context(), cmd.OutOrStdout(), logFile)\
},\
}\
\
```\
cmd.Flags().IntVar(&tail, "tail", 100, "Show the last N lines (use 0 for all)")\
cmd.Flags().BoolVarP(&follow, "follow", "f", false, "Stream new log lines as they are written")\
return cmd\
```\
\
}\
\
func printTail(w io.Writer, path string, n int) error {\
f, err := os.Open(path) //nolint:gosec // path is .entire/logs/entire.log under repo root\
if err != nil {\
return fmt.Errorf("open log: %w", err)\
}\
defer f.Close()\
\
```\
if n <= 0 {\
    _, err := io.Copy(w, f)\
    if err != nil {\
        return fmt.Errorf("read log: %w", err)\
    }\
    return nil\
}\
\
lines, err := readLastNLines(f, n)\
if err != nil {\
    return err\
}\
for _, line := range lines {\
    if _, err := io.WriteString(w, line); err != nil {\
        return fmt.Errorf("write line: %w", err)\
    }\
    if !endsWithNewline(line) {\
        if _, err := io.WriteString(w, "\n"); err != nil {\
            return fmt.Errorf("write newline: %w", err)\
        }\
    }\
}\
return nil\
```\
\
}\
\
func endsWithNewline(s string) bool {\
return len(s) > 0 && s\[len(s)-1\] == '\\n'\
}\
\
// readLastNLines reads the file as a stream and returns up to n trailing lines.\
// For typical log sizes this is fast enough; large files would benefit from a\
// reverse-seek implementation but the current logger rotates so files stay small.\
func readLastNLines(r io.Reader, n int) (\[\]string, error) {\
scanner := bufio.NewScanner(r)\
scanner.Buffer(make(\[\]byte, 1024 _64), 1024_ 1024)\
ring := make(\[\]string, 0, n)\
for scanner.Scan() {\
line := scanner.Text() + "\\n"\
if len(ring) < n {\
ring = append(ring, line)\
} else {\
ring = append(ring\[1:\], line)\
}\
}\
if err := scanner.Err(); err != nil {\
return nil, fmt.Errorf("scan log: %w", err)\
}\
return ring, nil\
}\
\
// followFile polls the log file for appended bytes. It exits cleanly when the\
// command's context is cancelled (Ctrl+C in a TTY).\
func followFile(ctx context.Context, w io.Writer, path string) error {\
f, err := os.Open(path) //nolint:gosec // path is .entire/logs/entire.log under repo root\
if err != nil {\
return fmt.Errorf("open log: %w", err)\
}\
defer f.Close()\
\
```\
if _, err := f.Seek(0, io.SeekEnd); err != nil {\
    return fmt.Errorf("seek log: %w", err)\
}\
\
buf := make([]byte, 4096)\
tick := time.NewTicker(250 * time.Millisecond)\
defer tick.Stop()\
for {\
    select {\
    case <-ctx.Done():\
        return nil\
    case <-tick.C:\
        for {\
            n, err := f.Read(buf)\
            if n > 0 {\
                if _, werr := w.Write(buf[:n]); werr != nil {\
                    return fmt.Errorf("write: %w", werr)\
                }\
            }\
            if errors.Is(err, io.EOF) {\
                break\
            }\
            if err != nil {\
                return fmt.Errorf("read: %w", err)\
            }\
        }\
    }\
}\
```\
\
}\
\
\-\-\- cmd/entire/cli/errors.go\
package cli\
\
// SilentError wraps an error to signal that the error message has already been\
// printed to the user. main.go checks for this type to avoid duplicate output.\
type SilentError struct {\
Err error\
}\
\
func (e \*SilentError) Error() string {\
return e.Err.Error()\
}\
\
func (e \*SilentError) Unwrap() error {\
return e.Err\
}\
\
// NewSilentError creates a SilentError wrapping the given error.\
// Use this when you've already printed a user-friendly error message\
// and don't want main.go to print the error again.\
func NewSilentError(err error) \*SilentError {\
return &SilentError{Err: err}\
}\
\
\-\-\- cmd/entire/cli/explain.go\
package cli\
\
import (\
"context"\
"encoding/hex"\
"errors"\
"fmt"\
"io"\
"log/slog"\
"os"\
"os/exec"\
"runtime"\
"sort"\
"strconv"\
"strings"\
"time"\
\
```\
"github.com/entireio/cli/cmd/entire/cli/agent"\
"github.com/entireio/cli/cmd/entire/cli/agent/claudecode"\
"github.com/entireio/cli/cmd/entire/cli/agent/external"\
"github.com/entireio/cli/cmd/entire/cli/agent/geminicli"\
"github.com/entireio/cli/cmd/entire/cli/agent/opencode"\
"github.com/entireio/cli/cmd/entire/cli/agent/types"\
"github.com/entireio/cli/cmd/entire/cli/checkpoint"\
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"\
"github.com/entireio/cli/cmd/entire/cli/interactive"\
"github.com/entireio/cli/cmd/entire/cli/logging"\
"github.com/entireio/cli/cmd/entire/cli/paths"\
"github.com/entireio/cli/cmd/entire/cli/settings"\
"github.com/entireio/cli/cmd/entire/cli/strategy"\
"github.com/entireio/cli/cmd/entire/cli/summarize"\
"github.com/entireio/cli/cmd/entire/cli/trailers"\
"github.com/entireio/cli/cmd/entire/cli/transcript"\
transcriptcompact "github.com/entireio/cli/cmd/entire/cli/transcript/compact"\
"github.com/entireio/cli/redact"\
\
"charm.land/lipgloss/v2"\
"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/go-git/go-git/v6/plumbing/storer"\
"github.com/go-git/go-git/v6/storage/filesystem"\
"github.com/spf13/cobra"\
"golang.org/x/term"\
```\
\
)\
\
const defaultCheckpointSummaryTimeout = 5 \* time.Minute\
\
const (\
pagerEnvVar = "PAGER"\
lessEnvVar = "LESS"\
lessPagerName = "less"\
lessRawControlEnv = "LESS=-R"\
windowsGOOS = "windows"\
)\
\
var checkpointSummaryTimeout = defaultCheckpointSummaryTimeout\
\
var generateTranscriptSummary = summarize.GenerateFromTranscript\
\
// resolveSummaryTimeout picks the effective deadline for `explain --generate`\
// using the precedence: per-run flag > settings.summary\_timeout\_seconds >\
// package default. Zero or negative values at any layer mean "unset; consult\
// the next layer down" — matching SummaryTimeoutValue() semantics.\
//\
// Settings load failures are logged at debug and fall through to the default;\
// a parsing hiccup must not break summary generation.\
func resolveSummaryTimeout(ctx context.Context, flagSeconds int) time.Duration {\
if flagSeconds > 0 {\
return time.Duration(flagSeconds) \* time.Second\
}\
s, err := settings.Load(ctx)\
if err != nil {\
logging.Debug(ctx, "summary timeout: settings load failed, using default",\
slog.String("error", err.Error()))\
return checkpointSummaryTimeout\
}\
if v := s.SummaryTimeoutValue(); v > 0 {\
return v\
}\
return checkpointSummaryTimeout\
}\
\
// errCannotGenerateTemporaryCheckpoint is returned by runExplainCheckpoint when\
// --generate is requested for a target that does not match any committed\
// checkpoint. runExplainAuto uses errors.Is to detect this case and fall back\
// to resolving the target as a git commit ref.\
var errCannotGenerateTemporaryCheckpoint = errors.New("cannot generate summary for temporary checkpoint")\
\
type explainCheckpointLookup struct {\
repo \*git.Repository\
v1Store \*checkpoint.GitStore\
v2Store \*checkpoint.V2GitStore\
reader checkpoint.CommittedListReader\
readMode checkpoint.CommittedReadMode\
committed \[\]checkpoint.CommittedInfo\
}\
\
// generateOrRawLabel returns the user-facing verb for the action the user\
// requested, used in error messages when a commit target has no trailer.\
func generateOrRawLabel(generate bool) string {\
if generate {\
return "generate summary"\
}\
return "show raw transcript"\
}\
\
// printNoTrailerMessage renders the friendly message shown when a resolved\
// commit has no Entire-Checkpoint trailer in read-only modes. Takes the\
// repo so the hash can be abbreviated to the minimum unique length for\
// this repo's object set (matching git's --abbrev behavior).\
func printNoTrailerMessage(w io.Writer, repo \*git.Repository, hash plumbing.Hash) {\
styles := newStatusStyles(w)\
rows := \[\]explainRow{\
{Label: "commit", Value: abbreviateCommitHash(repo, hash)},\
{Label: "reason", Value: "no Entire-Checkpoint trailer"},\
{Label: "hint", Value: "this commit was not created during an Entire session,"},\
{Label: "", Value: "or the trailer was removed"},\
}\
fmt.Fprint(w, styles.renderFailure("No associated Entire checkpoint", rows))\
}\
\
// errAmbiguousCommitPrefix is returned by resolveCommitUnambiguous when a\
// hex prefix matches more than one commit. Callers use errors.Is to detect\
// this case and surface the full wrapped message verbatim.\
var errAmbiguousCommitPrefix = errors.New("ambiguous commit prefix")\
\
// commitHashesWithPrefix enumerates all commit hashes in the repo whose\
// SHA starts with the given hex prefix. Returns nil when the storer is not\
// a \*filesystem.Storage or the prefix isn't decodable as hex.\
//\
// Per PR review (discussion\_r3113804961): the reviewer specifically\
// suggested repo.Storer.(\*filesystem.Storage).HashesWithPrefix followed by\
// commit filtering. Using this primitive both in resolution (detect\
// ambiguous user input) and in display (dynamically abbreviate shown\
// hashes to the minimum unique length).\
func commitHashesWithPrefix(repo \*git.Repository, prefix string) \[\]plumbing.Hash {\
s, ok := repo.Storer.(\*filesystem.Storage)\
if !ok {\
return nil\
}\
// Truncate to even length for byte-aligned hex decoding.\
evenHex := prefix\[:len(prefix)&^1\]\
decoded, err := hex.DecodeString(evenHex)\
if err != nil \|\| len(decoded) == 0 {\
return nil\
}\
candidates, err := s.HashesWithPrefix(decoded)\
if err != nil {\
return nil\
}\
var commits \[\]plumbing.Hash\
for \_, h := range candidates {\
// HashesWithPrefix matches on even byte boundaries; filter the\
// dangling nybble for odd-length prefixes.\
if len(evenHex) != len(prefix) && !strings.HasPrefix(h.String(), prefix) {\
continue\
}\
if \_, err := repo.CommitObject(h); err != nil {\
continue\
}\
commits = append(commits, h)\
}\
return commits\
}\
\
// resolveCommitUnambiguous resolves a ref to a commit hash, returning\
// errAmbiguousCommitPrefix (and the matching hashes) when a hex-prefix input\
// matches more than one commit. go-git v6's ResolveRevision silently picks\
// the first candidate in ambiguous cases (its source explicitly says "for\
// speed purposes don't bother to detect the ambiguity"), which could pick\
// the wrong commit. Non-hex refs (HEAD, branch names, HEAD~1) bypass the\
// ambiguity check via commitHashesWithPrefix returning nil.\
//\
// The structured ambiguous return lets callers render a styled failure\
// block (with each match's timestamp/session) without re-resolving the\
// matches themselves.\
func resolveCommitUnambiguous(repo \*git.Repository, ref string) (plumbing.Hash, \[\]plumbing.Hash, error) {\
hash, err := repo.ResolveRevision(plumbing.Revision(ref))\
if err != nil {\
return plumbing.ZeroHash, nil, err //nolint:wrapcheck // caller contextualizes\
}\
matches := commitHashesWithPrefix(repo, ref)\
if len(matches) <= 1 {\
return \*hash, nil, nil\
}\
return plumbing.ZeroHash, matches, errAmbiguousCommitPrefix\
}\
\
// abbreviateCommitHash returns the shortest prefix of hash unique among\
// commit objects in the repo, matching git's --abbrev-commit auto-growth\
// so displayed short SHAs stay unambiguous as the repo grows. Falls back\
// to a fixed 12-char prefix if the storer doesn't support fast prefix\
// lookup, or to the full hash if somehow never unique.\
func abbreviateCommitHash(repo \*git.Repository, hash plumbing.Hash) string {\
full := hash.String()\
for length := 7; length < len(full); length++ {\
matches := commitHashesWithPrefix(repo, full\[:length\])\
if matches == nil {\
return full\[:12\]\
}\
if len(matches) <= 1 {\
return full\[:length\]\
}\
}\
return full\
}\
\
// interaction holds a single prompt and its responses for display.\
type interaction struct {\
Prompt string\
Responses \[\]string // Multiple responses can occur between tool calls\
Files \[\]string\
}\
\
// associatedCommit holds information about a git commit associated with a checkpoint.\
type associatedCommit struct {\
SHA string\
ShortSHA string\
Message string\
Author string\
Email string\
Date time.Time\
}\
\
// checkpointDetail holds detailed information about a checkpoint for display.\
type checkpointDetail struct {\
Index int\
ShortID string\
Timestamp time.Time\
IsTaskCheckpoint bool\
Message string\
// Interactions contains all prompt/response pairs in this checkpoint.\
// Most strategies have one, but shadow condensations may have multiple.\
Interactions \[\]interaction\
// Files is the aggregate list of all files modified (for backwards compat)\
Files \[\]string\
}\
\
func newExplainCmd() \*cobra.Command {\
var sessionFlag string\
var commitFlag string\
var checkpointFlag string\
var noPagerFlag bool\
var shortFlag bool\
var fullFlag bool\
var rawTranscriptFlag bool\
var generateFlag bool\
var forceFlag bool\
var searchAllFlag bool\
var jsonFlag bool\
var transcriptFlag bool\
var summaryTimeoutSecondsFlag int\
sessionIndex := -1\
listLimit := 0 // 0 means "use default (branchCheckpointsLimit)"\
\
```\
cmd := &cobra.Command{\
    Use:   "explain [checkpoint-id | commit-sha]",\
    Short: "Explain a session, commit, or checkpoint",\
    Long: `Explain provides human-readable context about sessions, commits, and checkpoints.\
```\
\
Use this command to understand what happened during agent-driven development,\
either for self-review or to understand a teammate's work.\
\
By default, shows checkpoints on the current branch. Pass a checkpoint ID or\
commit SHA as a positional argument to explain a specific item, or use flags.\
\
Viewing specific items:\
entire explain <id-or-sha> Auto-detects checkpoint ID or commit SHA\
entire explain --checkpoint <id> Force interpretation as checkpoint ID\
entire explain --commit <ref> Force interpretation as commit ref\
\
Filtering the list view:\
--session Filter checkpoints by session ID (or prefix)\
\
Output verbosity levels (when explaining a specific item):\
Default: Detailed view with scoped prompts (ID, session, tokens, intent, prompts, files)\
--short Summary only (ID, session, timestamp, tokens, intent)\
--full Parsed full transcript (all prompts/responses from entire session)\
--raw-transcript Raw transcript file (JSONL format)\
\
Machine-readable export modes (additive surface for external consumers):\
--json Metadata-only JSON. Lists checkpoints when no target is given;\
emits a single checkpoint envelope when a target is supplied.\
Transcript bytes are NEVER embedded in the JSON envelope.\
--transcript Stream the normalized compact transcript bytes (JSONL on\
/main) to stdout for the selected session. Pair with\
--raw-transcript for the per-agent raw transcript instead.\
--session-index Pick a session within a multi-session checkpoint (0-based).\
Defaults to the latest session. Only meaningful with\
--transcript or --raw-transcript.\
--limit Cap the number of checkpoints returned by the list view.\
Defaults to 100. When the cap is hit, a stderr note\
says how many were skipped. Only meaningful with --json.\
\
Summary generation:\
--generate Generate an AI summary for the checkpoint\
--force Regenerate even if a summary already exists (requires --generate)\
\
Performance options:\
--search-all Remove branch/depth limits when searching for commits (may be slow)\
\
Checkpoint detail view shows:\
\
- Author of the checkpoint\
- Associated git commits that reference the checkpoint\
- Prompts and responses from the session\
\
Note: --session filters the list view; the positional arg, --commit, and --checkpoint are mutually exclusive.\`,\
Args: func(\_ \*cobra.Command, args \[\]string) error {\
if len(args) > 1 {\
return fmt.Errorf("accepts at most 1 argument (checkpoint ID or commit SHA), received %d\\nHint: use --session to filter the list view, or pass a single checkpoint ID / commit SHA", len(args))\
}\
return nil\
},\
RunE: func(cmd \*cobra.Command, args \[\]string) error {\
// Check if Entire is disabled\
if checkDisabledGuard(cmd.Context(), cmd.OutOrStdout()) {\
return nil\
}\
\
```\
        // Only initialize logging when inside a git worktree to avoid\
        // creating .entire/logs/ in arbitrary directories.\
        if _, err := paths.WorktreeRoot(cmd.Context()); err == nil {\
            logging.SetLogLevelGetter(GetLogLevel)\
            if err := logging.Init(cmd.Context(), ""); err == nil {\
                defer logging.Close()\
            }\
        }\
\
        // Positional arg is mutually exclusive with --checkpoint, --commit, --session\
        var positional string\
        if len(args) > 0 {\
            positional = args[0]\
            if checkpointFlag != "" || commitFlag != "" || sessionFlag != "" {\
                return errors.New("cannot combine positional argument with --checkpoint, --commit, or --session")\
            }\
        }\
\
        // --generate and --raw-transcript need a specific target — either the\
        // positional arg, --checkpoint/-c, or --commit (which forwards to\
        // the checkpoint path via the commit's Entire-Checkpoint trailer).\
        hasCheckpointTarget := checkpointFlag != "" || commitFlag != "" || positional != ""\
        if generateFlag && !hasCheckpointTarget {\
            return errors.New("--generate requires a checkpoint ID or commit SHA (positional), --checkpoint/-c, or --commit flag")\
        }\
        if forceFlag && !generateFlag {\
            return errors.New("--force requires --generate flag")\
        }\
        if rawTranscriptFlag && !hasCheckpointTarget {\
            return errors.New("--raw-transcript requires a checkpoint ID or commit SHA (positional), --checkpoint/-c, or --commit flag")\
        }\
        if transcriptFlag && !hasCheckpointTarget {\
            return errors.New("--transcript requires a checkpoint ID or commit SHA (positional), --checkpoint/-c, or --commit flag")\
        }\
        if cmd.Flags().Changed("session-index") {\
            if !transcriptFlag && !rawTranscriptFlag {\
                return errors.New("--session-index only applies with --transcript or --raw-transcript")\
            }\
            if sessionIndex < 0 {\
                return errors.New("--session-index must be non-negative")\
            }\
        }\
        if cmd.Flags().Changed("limit") {\
            if !jsonFlag {\
                return errors.New("--limit only applies with --json")\
            }\
            if listLimit <= 0 {\
                return errors.New("--limit must be positive")\
            }\
        }\
        // --summary-timeout-seconds only makes sense with --generate.\
        if cmd.Flags().Changed("summary-timeout-seconds") {\
            if !generateFlag {\
                return errors.New("--summary-timeout-seconds only applies with --generate")\
            }\
            if summaryTimeoutSecondsFlag < 0 {\
                return errors.New("--summary-timeout-seconds must be non-negative")\
            }\
        }\
\
        // Export modes — emit machine-readable output and skip the prose pipeline.\
        // --raw-transcript also routes here when --session-index is explicit; the\
        // legacy raw-transcript path (with spinner + prefetch) handles the default\
        // case where the caller wants the latest session.\
        rawWithSessionIndex := rawTranscriptFlag && cmd.Flags().Changed("session-index")\
        if jsonFlag || transcriptFlag || rawWithSessionIndex {\
            return runExplainExport(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), explainExportOptions{\
                sessionFilter:  sessionFlag,\
                commitRef:      commitFlag,\
                checkpointFlag: checkpointFlag,\
                target:         positional,\
                json:           jsonFlag,\
                transcript:     transcriptFlag,\
                rawTranscript:  rawTranscriptFlag,\
                sessionIndex:   sessionIndex,\
                listLimit:      listLimit,\
            })\
        }\
\
        // Convert short flag to verbose (verbose = !short)\
        verbose := !shortFlag\
        return runExplain(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), sessionFlag, commitFlag, checkpointFlag, positional, noPagerFlag, verbose, fullFlag, rawTranscriptFlag, generateFlag, forceFlag, searchAllFlag, summaryTimeoutSecondsFlag)\
    },\
}\
\
cmd.Flags().StringVar(&sessionFlag, "session", "", "Filter checkpoints by session ID (or prefix)")\
cmd.Flags().StringVar(&commitFlag, "commit", "", "Explain a specific commit (SHA or ref, \"commit-ish\")")\
cmd.Flags().StringVarP(&checkpointFlag, "checkpoint", "c", "", "Explain a specific checkpoint (ID or prefix)")\
cmd.Flags().BoolVar(&noPagerFlag, "no-pager", false, "Disable pager output")\
cmd.Flags().BoolVarP(&shortFlag, "short", "s", false, "Show summary only (omit prompts and files)")\
cmd.Flags().BoolVar(&fullFlag, "full", false, "Show full parsed transcript (all prompts/responses)")\
cmd.Flags().BoolVar(&rawTranscriptFlag, "raw-transcript", false, "Show raw transcript file (JSONL format)")\
cmd.Flags().BoolVar(&generateFlag, "generate", false, "Generate an AI summary for the checkpoint")\
cmd.Flags().BoolVar(&forceFlag, "force", false, "Regenerate summary even if one already exists (requires --generate)")\
cmd.Flags().BoolVar(&searchAllFlag, "search-all", false, "Search all commits (no branch/depth limit, may be slow)")\
cmd.Flags().BoolVar(&jsonFlag, "json", false, "Output metadata as JSON (no transcript bytes)")\
cmd.Flags().BoolVar(&transcriptFlag, "transcript", false, "Stream compact normalized transcript bytes to stdout (pair with --raw-transcript for the per-agent raw transcript)")\
cmd.Flags().IntVar(&sessionIndex, "session-index", -1, "Session index within a multi-session checkpoint (0-based, defaults to latest)")\
cmd.Flags().IntVar(&listLimit, "limit", 0, "Cap the list view at N checkpoints (default: 100). Only meaningful with --json.")\
cmd.Flags().IntVar(&summaryTimeoutSecondsFlag, "summary-timeout-seconds", 0, "Hard deadline in seconds for --generate summary generation; overrides summary_timeout_seconds setting. 0 = use setting or 5m default.")\
\
// Verbosity / transcript output modes are mutually exclusive\
cmd.MarkFlagsMutuallyExclusive("short", "full", "raw-transcript", "transcript", "json")\
// --generate and --raw-transcript are incompatible (summary would be generated but not shown)\
cmd.MarkFlagsMutuallyExclusive("generate", "raw-transcript")\
// --generate is a write op; export modes are reader-only\
cmd.MarkFlagsMutuallyExclusive("generate", "json")\
cmd.MarkFlagsMutuallyExclusive("generate", "transcript")\
\
return cmd\
```\
\
}\
\
// runExplain routes to the appropriate explain function based on flags and the\
// optional positional target.\
func runExplain(ctx context.Context, w, errW io.Writer, sessionID, commitRef, checkpointID, target string, noPager, verbose, full, rawTranscript, generate, force, searchAll bool, summaryTimeoutSeconds int) error {\
// Count mutually exclusive flags (--commit and --checkpoint are mutually exclusive)\
// --session is now a filter for the list view, not a separate mode\
flagCount := 0\
if commitRef != "" {\
flagCount++\
}\
if checkpointID != "" {\
flagCount++\
}\
// If --session is combined with --commit or --checkpoint, that's still an error\
if sessionID != "" && flagCount > 0 {\
return errors.New("cannot specify multiple of --session, --commit, --checkpoint")\
}\
if flagCount > 1 {\
return errors.New("cannot specify multiple of --session, --commit, --checkpoint")\
}\
\
```\
// Route to appropriate handler\
if target != "" {\
    return runExplainAuto(ctx, w, errW, target, noPager, verbose, full, rawTranscript, generate, force, searchAll, summaryTimeoutSeconds)\
}\
if commitRef != "" {\
    return runExplainCommit(ctx, w, errW, commitRef, noPager, verbose, full, rawTranscript, generate, force, searchAll, summaryTimeoutSeconds)\
}\
if checkpointID != "" {\
    return runExplainCheckpoint(ctx, w, errW, checkpointID, noPager, verbose, full, rawTranscript, generate, force, searchAll, summaryTimeoutSeconds)\
}\
\
// Default or with session filter: show list view (optionally filtered by session)\
return runExplainBranchWithFilter(ctx, w, noPager, sessionID)\
```\
\
}\
\
// runExplainAuto resolves a positional target as either a checkpoint ID\
// (or prefix) or a git commit ref. Ordering: checkpoint path first (which\
// also handles shadow-branch temp checkpoints), falling back to commit\
// resolution only on checkpoint.ErrCheckpointNotFound. --generate runs\
// an ambiguity pre-check to avoid writing a summary to the wrong\
// checkpoint on short-prefix collisions.\
func runExplainAuto(ctx context.Context, w, errW io.Writer, target string, noPager, verbose, full, rawTranscript, generate, force, searchAll bool, summaryTimeoutSeconds int) error {\
stop := startSpinner(errW, "Loading checkpoints")\
lookup, lookupErr := newExplainCheckpointLookup(ctx)\
stop(false)\
if generate {\
if err := runExplainAutoAmbiguityGuard(ctx, target, lookup, lookupErr); err != nil {\
return err\
}\
}\
checkpointErr := runExplainCheckpointWithLookup(ctx, w, errW, target, noPager, verbose, full, rawTranscript, generate, force, searchAll, lookup, lookupErr, summaryTimeoutSeconds)\
if checkpointErr == nil {\
return nil\
}\
// Fall back to commit resolution ONLY when nothing (committed or temp)\
// matched the target. errCannotGenerateTemporaryCheckpoint signals that\
// we DID match a temp checkpoint but --generate is unsupported for it;\
// falling back to commit in that case would produce a misleading\
// "no trailer" error for the shadow-branch commit.\
if !errors.Is(checkpointErr, checkpoint.ErrCheckpointNotFound) {\
return checkpointErr\
}\
logging.Debug(ctx, "explain auto: checkpoint lookup failed, trying commit fallback",\
slog.String("target", target),\
slog.String("checkpoint\_error", checkpointErr.Error()))\
\
```\
if lookupErr != nil {\
    // Composed message beats errors.Join here — the latter renders\
    // two lines (one per error) and users act on the first/stale one.\
    return fmt.Errorf("no checkpoint matched %q, and commit fallback failed: %w", target, lookupErr)\
}\
hash, ambiguousMatches, resolveErr := resolveCommitUnambiguous(lookup.repo, target)\
if resolveErr != nil {\
    if errors.Is(resolveErr, errAmbiguousCommitPrefix) {\
        renderAmbiguousPrefixFailure(errW, target, "commits", buildAmbiguousCommitMatches(lookup.repo, ambiguousMatches))\
        return NewSilentError(resolveErr)\
    }\
    logging.Debug(ctx, "explain auto: git ref resolution failed",\
        slog.String("target", target),\
        slog.String("error", resolveErr.Error()))\
    return fmt.Errorf("no checkpoint or commit found matching %q", target)\
}\
commit, commitErr := lookup.repo.CommitObject(hash)\
if commitErr != nil {\
    return fmt.Errorf("failed to get commit %s: %w", abbreviateCommitHash(lookup.repo, hash), commitErr)\
}\
cpID, hasCheckpoint := trailers.ParseCheckpoint(commit.Message)\
if !hasCheckpoint {\
    // Side-effect modes must error — silently succeeding would leave\
    // scripts unable to distinguish "done" from "didn't happen".\
    if generate || rawTranscript {\
        return fmt.Errorf("cannot %s: commit %s has no Entire-Checkpoint trailer", generateOrRawLabel(generate), abbreviateCommitHash(lookup.repo, hash))\
    }\
    printNoTrailerMessage(w, lookup.repo, hash)\
    return nil\
}\
logging.Debug(ctx, "explain auto: resolved commit to checkpoint via trailer",\
    slog.String("target", target),\
    slog.String("commit", abbreviateCommitHash(lookup.repo, hash)),\
    slog.String("checkpoint_id", cpID.String()))\
return runExplainCheckpointWithLookup(ctx, w, errW, cpID.String(), noPager, verbose, full, rawTranscript, generate, force, searchAll, lookup, nil, summaryTimeoutSeconds)\
```\
\
}\
\
// runExplainAutoAmbiguityGuard refuses --generate when the positional\
// target resolves as both a git revision and a committed-checkpoint prefix.\
// Writing a summary to the wrong checkpoint is destructive; read-only flows\
// tolerate the same ambiguity by preferring the checkpoint path.\
//\
// Best-effort: on repo/list failures we return nil so the main flow\
// surfaces the real error instead of double-reporting.\
func runExplainAutoAmbiguityGuard(ctx context.Context, target string, lookup \*explainCheckpointLookup, lookupErr error) error {\
// Targets longer than a checkpoint ID can't prefix-match one.\
// This is coupled to checkpoint IDs being fixed-width; longer t\
...\[truncated\]\
\
1mo ago·10.4s·13 calls\
\
\
\
{"findings":\[{"title":"Interrupt handling swallows follow-up termination signals","category":"bug","severity":"medium","confidence":"high","evidence":\[{"path":"cmd/entire/main.go","startLine":27,"endLine":31,"symbol":"main","quote":"signal.Notify(sigChan, signals...)"}\],"reasoning":"signal.Notify disables the default SIGINT/SIGTERM behavior while registered. The handler reads exactly one signal, cancels the context, and then exits without calling signal.Stop or otherwise restoring default behavior. If any built-in command, prompt, network call, or subprocess path does not promptly observe cmd.Context(), later Ctrl-C/SIGTERM signals are still captured by the registered channel instead of terminating the process, leaving users with SIGKILL as the practical escape hatch.","reproduction":"Run an `entire` command path that blocks without observing cmd.Context(), send SIGINT once, then send SIGINT or SIGTERM again. The first signal only cancels the context; subsequent handled signals do not take the OS default termination action because Notify remains active.","recommendation":"After the first signal, restore default handling or implement explicit escalation. For example, call signal.Stop(sigChan) after canceling so a second SIGINT/SIGTERM terminates normally, or keep listening and os.Exit with the conventional signal exit code on a second signal.","whyTestsDoNotAlreadyCoverThis":"The feature lists no linked tests. Existing signal coverage around external plugins validates first-signal forwarding, but does not cover second-signal escalation or a built-in command that ignores context cancellation.","suggestedRegressionTest":"Add an integration test with a test-only blocking command that ignores context, send SIGINT twice, and assert the process exits within a short timeout. Add a Unix SIGTERM variant if SIGTERM remains registered.","minimumFixScope":"`cmd/entire/main.go` signal setup plus a focused integration test."}\],"inspected":{"files":\["cmd/entire/main.go","cmd/entire/cli/activity\_cmd.go","cmd/entire/cli/activity\_render.go","cmd/entire/cli/activity\_tui.go","cmd/entire/cli/activity\_types.go","cmd/entire/cli/agent\_group.go","cmd/entire/cli/aliascmd.go","cmd/entire/cli/api\_client.go","cmd/entire/cli/attach.go","cmd/entire/cli/attach\_transcript.go","cmd/entire/cli/auth.go","cmd/entire/cli/checkpoint\_group.go","cmd/entire/cli/checkpoint\_reader.go","cmd/entire/cli/clean.go","cmd/entire/cli/commit\_message.go","cmd/entire/cli/config.go","cmd/entire/cli/constants.go","cmd/entire/cli/dispatch.go","cmd/entire/cli/dispatch\_tui.go","cmd/entire/cli/dispatch\_wizard.go","cmd/entire/cli/doctor.go","cmd/entire/cli/doctor\_bundle.go","cmd/entire/cli/doctor\_logs.go","cmd/entire/cli/errors.go","cmd/entire/cli/explain.go","cmd/entire/cli/root.go","cmd/entire/cli/plugin.go","cmd/entire/cli/integration\_test/external\_command\_signal\_unix\_test.go"\],"symbols":\["main","showSuggestion","cli.NewRootCmd","cli.MaybeRunPlugin","cli.PrependPluginBinDirToPATH","runPlugin","NewRootCmd"\],"notes":\["Used code-reviewer workflow; final output kept strict JSON per request.","No linked tests were declared in the feature metadata.","Evidence for the emitted finding points at the included owned file. Additional root/plugin files were inspected only to understand the entrypoint context."\]}}\
\
[a61b309ignore .clawpatch and entire-external-cp-test\\
\\
Entire-Checkpoint: 1cf96d1dbe7e+2](/content/gh/entireio/cli/commit/a61b3093554837013e507182c34858f3cbb45fdf/index.html)\
\
[c6d32c3ignore .clawpatch and entire-external-cp-test\\
\\
Entire-Checkpoint: 1cf96d1dbe7e+2](/content/gh/entireio/cli/commit/c6d32c3bcb3369b2b5cf9941e4893aabb3051432/index.html)