This repo contains the CLI for Entire.
- CLI build with github.com/spf13/cobra and github.com/charmbracelet/huh
entire/: Main CLI entry pointentire/cli: CLI utilities and helpersentire/cli/commands: actual command implementationsentire/cli/strategy: strategy implementations - see section belowentire/cli/checkpoint: checkpoint storage abstractions (temporary and committed)entire/cli/session: session state managemententire/cli/integration_test: integration tests
- Language: Go 1.25.x
- Build tool: mise, go modules
- Linting: golangci-lint
mise run testmise run test:integrationmise run test:ciIntegration tests use the //go:build integration build tag and are located in cmd/entire/cli/integration_test/.
mise run lintThe CLI uses a specific pattern for error output to avoid duplication between Cobra and main.go.
How it works:
root.gosetsSilenceErrors: trueglobally - Cobra never prints errorsmain.goprints errors to stderr, unless the error is aSilentError- 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- DefinesSilentErrortype andNewSilentError()constructorroot.go- SetsSilenceErrors: trueon root commandmain.go- Checks forSilentErrorbefore printing
We use github.com/go-git/go-git for most git operations, but with important exceptions:
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.
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.
The CLI uses a strategy pattern for managing session data and checkpoints. Each strategy implements the Strategy interface defined in strategy.go.
All strategies implement:
SaveChanges()- Save session checkpoint (code + metadata)SaveTaskCheckpoint()- Save subagent task checkpointGetRewindPoints()/Rewind()- List and restore to checkpointsGetSessionLog()/GetSessionInfo()- Retrieve session dataListSessions()/GetSession()- Session discovery
| 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.
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/v1branch 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/v1branch 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-Checkpointtrailer only) - Metadata stored on orphan
entire/checkpoints/v1branch 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/v1branch alongside user pushes AllowsMainBranch() = false- creates commits, so not recommended on main branch
strategy.go- Interface definition and context structs (SaveContext,RewindPoint, etc.)registry.go- Strategy registration/discovery (factory pattern withGet(),List(),Default())common.go- Shared helpers for metadata extraction, tree building, rewind validation,ListCheckpoints()session.go- Session/checkpoint data structurespush_common.go- Shared PrePush logic for pushingentire/checkpoints/v1branchmanual_commit.go- Manual-commit strategy main implementationmanual_commit_types.go- Type definitions:SessionState,CheckpointInfo,CondenseResultmanual_commit_session.go- Session state management (load/save/list session states)manual_commit_condensation.go- Condense logic for copying logs toentire/checkpoints/v1manual_commit_rewind.go- Rewind implementation: file restoration from checkpoint treesmanual_commit_git.go- Git operations: checkpoint commits, tree buildingmanual_commit_logs.go- Session log retrieval and session listingmanual_commit_hooks.go- Git hook handlers (prepare-commit-msg, pre-push)manual_commit_reset.go- Shadow branch reset/cleanup functionalityauto_commit.go- Auto-commit strategy implementationhooks.go- Git hook installation
checkpoint.go- Data types (Checkpoint,TemporaryCheckpoint,CommittedCheckpoint)store.go-GitStorestruct wrapping git repositorytemporary.go- Shadow branch operations (WriteTemporary,ReadTemporary,ListTemporary)committed.go- Metadata branch operations (WriteCommitted,ReadCommitted,ListCommitted)
session.go- Session data types and interfacesstate.go-StateStorefor managing.git/entire-sessions/files
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.)
On active branch commits (auto-commit strategy only):
Entire-Checkpoint: <checkpoint-id>- 12-hex-char ID linking to metadata onentire/checkpoints/v1
On shadow branch commits (entire/<commit-hash>):
Entire-Session: <session-id>- Session identifierEntire-Metadata: <path>- Path to metadata directory within the treeEntire-Task-Metadata: <path>- Path to task metadata directoryEntire-Strategy: manual-commit- Strategy that created the commit
On metadata branch commits (entire/checkpoints/v1):
Entire-Session: <session-id>- Session identifierEntire-Strategy: <strategy>- Strategy that created the checkpointCommit: <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.
- All strategies must implement the full
Strategyinterface - Register new strategies in
init()usingRegister() - Test with
mise run test- strategy tests are in*_test.gofiles - Update this GEMINI.md when adding or modifying strategies to keep documentation current
- Tests: always run
mise run testbefore committing changes - Integration tests: run
mise run test:integrationwhen changing integration test code - Linting: always run
mise run lintbefore committing changes - Code formatting: always run
mise run fmtbefore committing changes - When adding new features, ensure they are well-tested and documented.
- Always check for code duplication and refactor as needed.
- 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.ymlfor enabled linters before writing Go code.
The CLI supports an accessibility mode for users who rely on screen readers. This mode uses simpler text prompts instead of interactive TUI elements.
ACCESSIBLE=1(or any non-empty value) enables accessibility mode- Users can set this in their shell profile (
.bashrc,.zshrc) for persistent use
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 { ... }- Always use the accessibility helpers for any
huhforms/prompts - Test new interactive features with
ACCESSIBLE=1to ensure they work - The accessible mode is documented in
--helpoutput