Skip to content

Latest commit

 

History

History
336 lines (259 loc) · 14.1 KB

File metadata and controls

336 lines (259 loc) · 14.1 KB

Entire - CLI

This repo contains the CLI for Entire.

Architecture

  • CLI build with github.com/spf13/cobra and github.com/charmbracelet/huh

Key Directories

Commands (cmd/)

  • entire/: Main CLI entry point
  • entire/cli: CLI utilities and helpers
  • entire/cli/commands: actual command implementations
  • entire/cli/strategy: strategy implementations - see section below
  • entire/cli/checkpoint: checkpoint storage abstractions (temporary and committed)
  • entire/cli/session: session state management
  • entire/cli/integration_test: integration tests

Tech Stack

  • Language: Go 1.25.x
  • Build tool: mise, go modules
  • Linting: golangci-lint

Development

Running Tests

mise run test

Running Integration Tests

mise run test:integration

Running All Tests (CI)

mise run test:ci

Integration tests use the //go:build integration build tag and are located in cmd/entire/cli/integration_test/.

Linting

mise run lint

Code Patterns

Error Handling

The CLI uses a specific pattern for error output to avoid duplication between Cobra and main.go.

How it works:

  • root.go sets SilenceErrors: true globally - Cobra never prints errors
  • main.go prints errors to stderr, unless the error is a SilentError
  • Commands return NewSilentError(err) when they've already printed a custom message

When to use SilentError: Use NewSilentError() when you want to print a custom, user-friendly error message instead of the raw error:

// In a command's RunE function:
if _, err := paths.RepoRoot(); err != nil {
    cmd.SilenceUsage = true  // Don't show usage for prerequisite errors
    fmt.Fprintln(cmd.ErrOrStderr(), "Not a git repository. Please run 'entire enable' from within a git repository.")
    return NewSilentError(errors.New("not a git repository"))
}

When NOT to use SilentError: For normal errors where the default error message is sufficient, just return the error directly. main.go will print it:

// Normal error - main.go will print "unknown strategy: foo"
return fmt.Errorf("unknown strategy: %s", name)

Key files:

  • errors.go - Defines SilentError type and NewSilentError() constructor
  • root.go - Sets SilenceErrors: true on root command
  • main.go - Checks for SilentError before printing

Git Operations

We use github.com/go-git/go-git for most git operations, but with important exceptions:

go-git v5 Bugs - Use CLI Instead

Do NOT use go-git v5 for checkout or reset --hard operations.

go-git v5 has a bug where worktree.Reset() with git.HardReset and worktree.Checkout() incorrectly delete untracked directories even when they're listed in .gitignore. This would destroy .entire/ and .worktrees/ directories.

Use the git CLI instead:

// WRONG - go-git deletes ignored directories
worktree.Reset(&git.ResetOptions{
    Commit: hash,
    Mode:   git.HardReset,
})

// CORRECT - use git CLI
cmd := exec.CommandContext(ctx, "git", "reset", "--hard", hash.String())

See HardResetWithProtection() in common.go and CheckoutBranch() in git_operations.go for examples.

Regression tests in hard_reset_test.go verify this behavior - if go-git v6 fixes this issue, those tests can be used to validate switching back.

Repo Root vs Current Working Directory

Always use repo root (not os.Getwd()) when working with git-relative paths.

Git commands like git status and worktree.Status() return paths relative to the repository root, not the current working directory. When Gemini runs from a subdirectory (e.g., /repo/frontend), using os.Getwd() to construct absolute paths will produce incorrect results for files in sibling directories.

// WRONG - breaks when running from subdirectory
cwd, _ := os.Getwd()  // e.g., /repo/frontend
absPath := filepath.Join(cwd, file)  // file="api/src/types.ts" → /repo/frontend/api/src/types.ts (WRONG)

// CORRECT - use repo root
repoRoot, _ := paths.RepoRoot()  // or strategy.GetWorktreePath()
absPath := filepath.Join(repoRoot, file)  // → /repo/api/src/types.ts (CORRECT)

This also affects path filtering. The paths.ToRelativePath() function rejects paths starting with .., so computing relative paths from cwd instead of repo root will filter out files in sibling directories:

// WRONG - filters out sibling directory files
cwd, _ := os.Getwd()  // /repo/frontend
relPath := paths.ToRelativePath("/repo/api/file.ts", cwd)  // returns "" (filtered out as "../api/file.ts")

// CORRECT - keeps all repo files
repoRoot, _ := paths.RepoRoot()
relPath := paths.ToRelativePath("/repo/api/file.ts", repoRoot)  // returns "api/file.ts"

When to use os.Getwd(): Only when you actually need the current directory (e.g., finding agent session directories that are cwd-relative).

When to use repo root: Any time you're working with paths from git status, git diff, or any git-relative file list.

Test case in state_test.go: TestFilterAndNormalizePaths_SiblingDirectories documents this bug pattern.

Session Strategies (cmd/entire/cli/strategy/)

The CLI uses a strategy pattern for managing session data and checkpoints. Each strategy implements the Strategy interface defined in strategy.go.

Core Interface

All strategies implement:

  • SaveChanges() - Save session checkpoint (code + metadata)
  • SaveTaskCheckpoint() - Save subagent task checkpoint
  • GetRewindPoints() / Rewind() - List and restore to checkpoints
  • GetSessionLog() / GetSessionInfo() - Retrieve session data
  • ListSessions() / GetSession() - Session discovery

Available Strategies

Strategy Main Branch Metadata Storage Use Case
manual-commit (default) Unchanged (no commits) entire/<HEAD-hash> branches + entire/checkpoints/v1 Recommended for most workflows
auto-commit Creates clean commits Orphan entire/checkpoints/v1 branch Teams that want code commits from sessions

Legacy names shadow and dual are only recognized when reading settings or checkpoint metadata.

Strategy Details

Manual-Commit Strategy (manual_commit*.go) - Default

  • Does not modify the active branch - no commits created on the working branch
  • Creates shadow branch entire/<HEAD-commit-hash> per base commit for checkpoints
  • Session logs are condensed to permanent entire/checkpoints/v1 branch on user commits
  • Builds git trees in-memory using go-git plumbing APIs
  • Rewind restores files from shadow branch commit tree (does not use git reset)
  • Tracks session state in .git/entire-sessions/ (shared across worktrees)
  • PrePush hook can push entire/checkpoints/v1 branch alongside user pushes
  • AllowsMainBranch() = true - safe to use on main/master since it never modifies commit history

Auto-Commit Strategy (auto_commit.go)

  • Code commits to active branch with clean history (commits have Entire-Checkpoint trailer only)
  • Metadata stored on orphan entire/checkpoints/v1 branch at sharded paths: <id[:2]>/<id[2:]>/
  • Uses checkpoint.WriteCommitted() for metadata storage
  • Checkpoint ID (12-hex-char) links code commits to metadata on entire/checkpoints/v1
  • Full rewind allowed if commit is only on current branch (not in main); otherwise logs-only
  • Rewind via git reset --hard
  • PrePush hook can push entire/checkpoints/v1 branch alongside user pushes
  • AllowsMainBranch() = false - creates commits, so not recommended on main branch

Key Files

  • strategy.go - Interface definition and context structs (SaveContext, RewindPoint, etc.)
  • registry.go - Strategy registration/discovery (factory pattern with Get(), List(), Default())
  • common.go - Shared helpers for metadata extraction, tree building, rewind validation, ListCheckpoints()
  • session.go - Session/checkpoint data structures
  • push_common.go - Shared PrePush logic for pushing entire/checkpoints/v1 branch
  • manual_commit.go - Manual-commit strategy main implementation
  • manual_commit_types.go - Type definitions: SessionState, CheckpointInfo, CondenseResult
  • manual_commit_session.go - Session state management (load/save/list session states)
  • manual_commit_condensation.go - Condense logic for copying logs to entire/checkpoints/v1
  • manual_commit_rewind.go - Rewind implementation: file restoration from checkpoint trees
  • manual_commit_git.go - Git operations: checkpoint commits, tree building
  • manual_commit_logs.go - Session log retrieval and session listing
  • manual_commit_hooks.go - Git hook handlers (prepare-commit-msg, pre-push)
  • manual_commit_reset.go - Shadow branch reset/cleanup functionality
  • auto_commit.go - Auto-commit strategy implementation
  • hooks.go - Git hook installation

Checkpoint Package (cmd/entire/cli/checkpoint/)

  • checkpoint.go - Data types (Checkpoint, TemporaryCheckpoint, CommittedCheckpoint)
  • store.go - GitStore struct wrapping git repository
  • temporary.go - Shadow branch operations (WriteTemporary, ReadTemporary, ListTemporary)
  • committed.go - Metadata branch operations (WriteCommitted, ReadCommitted, ListCommitted)

Session Package (cmd/entire/cli/session/)

  • session.go - Session data types and interfaces
  • state.go - StateStore for managing .git/entire-sessions/ files

Metadata Structure

Shadow Strategy - Shadow branches (entire/<commit-hash>):

.entire/metadata/<session-id>/
├── full.jsonl               # Session transcript
├── prompt.txt               # User prompts
├── context.md               # Generated context
└── tasks/<tool-use-id>/     # Task checkpoints
    ├── checkpoint.json      # UUID mapping for rewind
    └── agent-<id>.jsonl     # Subagent transcript

Both Strategies - Metadata branch (entire/checkpoints/v1) - sharded checkpoint format:

<checkpoint-id[:2]>/<checkpoint-id[2:]>/
├── metadata.json            # Checkpoint info (checkpoint_id, session_id, strategy, created_at)
├── full.jsonl               # Session transcript
├── prompt.txt               # User prompts
├── context.md               # Generated context
├── content_hash.txt         # SHA256 of transcript (shadow only)
└── tasks/<tool-use-id>/     # Task checkpoints (if applicable)
    ├── checkpoint.json      # UUID mapping
    └── agent-<id>.jsonl     # Subagent transcript

Session State (filesystem, .git/entire-sessions/):

<session-id>.json            # Active session state (base_commit, checkpoint_count, etc.)

Commit Trailers

On active branch commits (auto-commit strategy only):

  • Entire-Checkpoint: <checkpoint-id> - 12-hex-char ID linking to metadata on entire/checkpoints/v1

On shadow branch commits (entire/<commit-hash>):

  • Entire-Session: <session-id> - Session identifier
  • Entire-Metadata: <path> - Path to metadata directory within the tree
  • Entire-Task-Metadata: <path> - Path to task metadata directory
  • Entire-Strategy: manual-commit - Strategy that created the commit

On metadata branch commits (entire/checkpoints/v1):

  • Entire-Session: <session-id> - Session identifier
  • Entire-Strategy: <strategy> - Strategy that created the checkpoint
  • Commit: <short-sha> - Code commit this checkpoint relates to (manual-commit strategy)

Note: Both strategies keep active branch history clean. Manual-commit strategy never creates commits on the active branch. Auto-commit strategy creates commits with only the Entire-Checkpoint trailer. All detailed metadata is stored on the entire/checkpoints/v1 orphan branch or shadow branches.

When Modifying Strategies

  • All strategies must implement the full Strategy interface
  • Register new strategies in init() using Register()
  • Test with mise run test - strategy tests are in *_test.go files
  • Update this GEMINI.md when adding or modifying strategies to keep documentation current

Important Notes

  • Tests: always run mise run test before committing changes
  • Integration tests: run mise run test:integration when changing integration test code
  • Linting: always run mise run lint before committing changes
  • Code formatting: always run mise run fmt before committing changes
  • When adding new features, ensure they are well-tested and documented.
  • Always check for code duplication and refactor as needed.

Go Code Style

  • Write lint-compliant Go code on the first attempt. Before outputting Go code, mentally verify it passes golangci-lint (or your specific linter).
  • Follow standard Go idioms: proper error handling, no unused variables/imports, correct formatting (gofmt), meaningful names.
  • Handle all errors explicitly—don't leave them unchecked.
  • Reference .golangci.yml for enabled linters before writing Go code.

Accessibility

The CLI supports an accessibility mode for users who rely on screen readers. This mode uses simpler text prompts instead of interactive TUI elements.

Environment Variable

  • ACCESSIBLE=1 (or any non-empty value) enables accessibility mode
  • Users can set this in their shell profile (.bashrc, .zshrc) for persistent use

Implementation Guidelines

When adding new interactive forms or prompts using huh:

In the cli package: Use NewAccessibleForm() instead of huh.NewForm():

// Good - respects ACCESSIBLE env var
form := NewAccessibleForm(
    huh.NewGroup(
        huh.NewSelect[string]().
            Title("Choose an option").
            Options(...).
            Value(&choice),
    ),
)

// Bad - ignores accessibility setting
form := huh.NewForm(...)

In the strategy package: Use the isAccessibleMode() helper. Note that WithAccessible() is only available on forms, not individual fields, so wrap confirmations in a form:

form := huh.NewForm(
    huh.NewGroup(
        huh.NewConfirm().
            Title("Confirm action?").
            Value(&confirmed),
    ),
)
if isAccessibleMode() {
    form = form.WithAccessible(true)
}
if err := form.Run(); err != nil { ... }

Key Points

  • Always use the accessibility helpers for any huh forms/prompts
  • Test new interactive features with ACCESSIBLE=1 to ensure they work
  • The accessible mode is documented in --help output