This repo contains the CLI for Entire.
- CLI built with github.com/spf13/cobra and github.com/charmbracelet/huh
entire/: Main CLI entry point. Also home to kubectl-style external-command resolution (entire <name>→entire-<name>on PATH) — see External Commands.entire/cli: CLI utilities and helpers (Cobra commands, helpers, group roots)entire/cli/commands: actual command implementationsentire/cli/agent: agent implementations (Claude Code, Gemini CLI, OpenCode, Cursor, Factory AI Droid, Copilot CLI, Pi) - see Agent Integration Checklist and Agent Implementation Guideentire/cli/strategy: strategy implementation (manual-commit) - see section belowentire/cli/checkpoint: checkpoint storage abstractions (ephemeral and persistent)entire/cli/session: session state managemententire/cli/integration_test: integration tests (simulated hooks)e2e/: E2E tests with real agent calls (see e2e/README.md)
The visible CLI is organized around a set of noun groups plus a small set of
top-level verbs. The groups are the canonical home for each verb; legacy
top-level shortcuts remain functional but hidden, and emit a deprecation hint
pointing at the canonical group form. Newer experimental command families are
discoverable through entire labs and their canonical paths are always
runnable.
Experimental commands are gated by a build-time visibility flag (the
cmd/entire/cli/experimental package): they are shown — grouped under an
"Experimental commands:" help section — in developer and nightly builds, and
hidden in stable release builds. Visibility is toggled by experimental.Visible
(default "true"), which GoReleaser stamps "false" only on stable tags
(.Prerelease empty); nightly (vX.Y.Z-nightly.*) and local builds leave it at
the default. Register a command as experimental with experimental.Register(parent, child) instead of parent.AddCommand(child). Gating only controls visibility —
the commands are always runnable in every build.
session(alias:sessions):list,info,tokens,stop,attach,adopt,resume,current.resumewith a branch arg switches to it and resumes its session; with no arg it opens an interactive picker of stopped sessions (across all worktrees), resolving each to its branch and pointing at the owning worktree when the branch is checked out elsewhere. Resume keeps an existing local session log as-is by default (--forceoverwrites it from the checkpoint).adoptmoves an active session from another repo or worktree into the current worktree and resets target-local checkpoint bookkeeping so future commits link to the adopted session from the new location.checkpoint(aliases:cp,checkpoints):list,explain,tokens,search, plus the deprecatedrewind(functional, prints a cobra deprecation message, will be removed in a future release)agent: bare opens the interactive agent selector, pluslist,add,removeconfigure: bare prints help and a hint pointing atentire agent; flags manage non-agent settings (telemetry, git-hook installation mode, strategy options, summary provider). Agent CRUD lives underentire agent.auth:login,logout,status,contexts,use, plustoken(prints the active control-plane bearer to stdout for scripting/curl; honorsENTIRE_TOKEN, else the refreshed active-context login JWT).tokenalso takes--jurisdiction <slug>(e.g.us,eu), which instead mints a jurisdictional identity token (RFC 8693 exchange,scope=openid,aud=<jurisdiction host>) for that jurisdiction's entire-api cells (e.g.https://aws-us-east-2.api.entire.io/api/v1), which reject the control-plane bearer; it exchangesENTIRE_TOKENwhen set (deriving the environment from the env token'saud), else the active login.auth statusshows the caller's home jurisdiction so the slug is discoverable.logouttakes--everywhere(revoke every session on the active core, not just the current one) and--all-contexts(log out of every saved login)doctor: bare runs the scan-and-fix flow, plustrace,logs,bundleorg: control-plane organization management —create,list,get,deleteproject: control-plane project management —create,list,get,deleterepo: control-plane repository lifecycle —create,list,get,delete,clone, plus themirrorandvisibilitysubtrees. Git content operations (log, diff, …) are intentionally out of scope.grant: manage access grants and org membership —org,project, andrepoeach supportadd/list/remove
Experimental commands (gated by the build-time visibility flag above — visible
and grouped under "Experimental commands:" in developer/nightly builds, hidden
in stable releases, always runnable): tokens, import, review,
investigate, blame, why, the top-level search shortcut, experts,
runner, and checkpoint policy. tokens is also advertised through entire labs. The canonical checkpoint search is not gated and stays visible.
Top-level lifecycle and standalone commands: enable, disable, status,
login, logout, clean, version, dispatch, activity, help,
configure, agent-help, api.
api is an authenticated passthrough to Entire's HTTP APIs (gh-style): it
attaches the right bearer and dials the right host so callers don't plumb auth
themselves. --to core (default) hits the control plane; --to cell hits an
entire-api cell. --jurisdiction <slug> (e.g. us, eu) targets a specific
jurisdiction's cell instead of the caller's home cell and implies --to cell
(cell routing + identity-token exchange live in auth.NewEntireAPICellClient
via auth.CellTarget). {owner}/{repo}/{repo_id} in the path are filled
from the current repo's origin remote. It is visible in entire help and
entire agent-help, so agents discover it as the supported way to call the API.
agent-help renders machine-readable, agent-facing usage live from the Cobra
command tree (so it always matches the installed binary): bare prints a
"when to use entire / which subcommand" map; agent-help <command> drills into
one command's current flags; --json emits structured output. It is the single
source of truth the first-turn context injection and the --agent-help-skill
skill point agents at, instead of enumerating a surface that goes stale.
Hidden commands opt into being advertised here by setting
Annotations[agentHelpAnnotation] = "true" (e.g. trail). Because agent-help
renders live and lists non-hidden commands, the experimental commands appear in
agent-help in developer/nightly builds and are absent in stable releases — the
advertised surface is build-dependent, matching what entire help shows.
No-channel agents (Cursor, Copilot CLI, Factory Droid, MCP hosts — no
context-injection channel and no agent-help skill template) reach it without an
active push. All of them can discover it passively: it is visible in entire help, the entire status footer points at it, and entire status --json
exposes it as the agent_help field. On top of that, Factory AI Droid (which is
banner-only) gets the pointer appended to its SessionStart hook banner, and
MCP-host agents can launch the hidden entire mcp stdio server, which exposes
agent_help and entire_status as MCP tools using the same live rendering.
Enabling a no-channel agent with --agent-help-skill reports the skill
unsupported and points the agent at this passive path instead.
Hidden top-level shortcuts (functional, emit a one-line deprecation hint):
resume → session resume, attach → session attach, explain →
checkpoint explain, trace → doctor trace.
Cobra-native aliases (no hint): sessions → session, cp/checkpoints →
checkpoint. The search top-level is experimental (see the visibility gate
above), so it follows the build-dependent visibility rather than being
unconditionally hidden.
Deprecated top-level commands (functional, print a cobra deprecation message):
reset → clean, and rewind (no replacement, announces removal — same
deprecation as checkpoint rewind).
Hidden infrastructure commands: hooks, trail,
curl-bash-post-install, __send_analytics, mcp (MCP stdio server for
MCP-host agents).
The hideAsAlias(cmd, canonical) helper in cmd/entire/cli/aliascmd.go
marks a command Hidden and sets cobra's Deprecated field so the hint
renders to stderr on every invocation while the command stays functional.
Diagnostic subcommands live alongside doctor.go as doctor_logs.go and
doctor_bundle.go. Group roots and noun-group children live in files
named <noun>_group.go and <noun>_<verb>.go respectively.
- Language: Go 1.26.x
- Build tool: mise, go modules
- Linting: golangci-lint
mise run testmise run test:integrationmise run test:ciThis runs unit tests, integration tests, and the E2E canary (Vogon agent) in sequence. Integration tests use the //go:build integration build tag and are located in cmd/entire/cli/integration_test/.
The Vogon agent is a deterministic fake agent that exercises the full E2E test suite without making any API calls.
mise run test:e2e:canary # Run all E2E tests with the Vogon agent
mise run test:e2e:canary TestFoo # Run a specific test- Runs as part of
test:ci— canary failures block merges - No API calls, no cost — safe to run freely, unlike real agent E2E tests
- If a canary test fails, the bug is in the CLI or test infrastructure, not in an agent
- Located in
e2e/vogon/(binary) andcmd/entire/cli/agent/vogon/(Agent interface) - The binary parses prompts via regex, creates/modifies/deletes files, and fires lifecycle hooks
- IMPORTANT: When changing E2E test prompt wording, the Vogon binary (
e2e/vogon/main.go) parses prompts with hardcoded regexes. New phrasing may not match existing patterns — always runmise run test:e2e:canaryafter changing prompt text and fix Vogon's parsing if tests fail.
IMPORTANT: Do NOT run E2E tests proactively. E2E tests make real API calls to agents, which consume tokens and cost money. Only run them when the user explicitly asks for E2E testing.
mise run test:e2e [filter] # All agents, filtered
mise run test:e2e --agent claude-code [filter] # Claude Code only as an example here, replace `claude-code` with other agents to run tests for those agentsE2E tests:
- Use the
//go:build e2ebuild tag - Located in
e2e/tests/ - See
e2e/README.mdfor full documentation (structure, debugging, adding agents) - Test real agent interactions (Claude Code, Gemini CLI, OpenCode, Cursor, Factory AI Droid, Copilot CLI, Pi, or Vogon creating files, committing, etc.)
- Validate checkpoint scenarios documented in
docs/architecture/checkpoint-scenarios.md - Support multiple agents via
E2E_AGENTenv var (claude-code,gemini,opencode,cursor,factoryai-droid,copilot-cli,pi,vogon)
Environment variables:
E2E_AGENT- Agent to test with (default:claude-code)E2E_CLAUDE_MODEL- Claude model to use (default:haikufor cost efficiency)E2E_TIMEOUT- Timeout per prompt (default:2m)
Always use t.Parallel() in tests. Every top-level test function and subtest should call t.Parallel() unless it modifies process-global state (e.g., os.Chdir()).
func TestFeature_Foo(t *testing.T) {
t.Parallel()
// ...
}
// Integration tests with TestEnv
func TestFeature_Bar(t *testing.T) {
t.Parallel()
env := NewFeatureBranchEnv(t)
// ...
}Exception: Tests that modify process-global state cannot be parallelized. This includes os.Chdir()/t.Chdir() and os.Setenv()/t.Setenv() — Go's test framework will panic if these are used after t.Parallel().
Tests that touch git state must use an isolated temp repo — never the real repo CWD.
Many handlers (lifecycle, strategy, hooks) resolve the git repo from CWD via OpenRepository, GetGitCommonDir, DetectFileChanges, etc. Without isolation, tests can create session state files, shadow branches, or other artifacts in the real .git/ directory.
Use the testutil helpers:
tmpDir := t.TempDir()
testutil.InitRepo(t, tmpDir) // git init + user config + disable GPG
testutil.WriteFile(t, tmpDir, "f.txt", "init") // create a file
testutil.GitAdd(t, tmpDir, "f.txt") // stage it
testutil.GitCommit(t, tmpDir, "init") // commit (needs at least one commit for HEAD)
t.Chdir(tmpDir) // redirect CWD-based git resolutiontestutil.InitRepo configures user.name, user.email, and disables GPG signing — safe for CI environments without global git config.
Prefer testutil.InitRepo() over direct git.PlainInit() in tests. When a test in this repo needs an initialized repository, use testutil.InitRepo(t, dir) unless the test specifically needs lower-level initialization behavior that the helper cannot provide. Do not call git.PlainInit() directly and then create commits or run CLI git operations without also reproducing the helper's repo-local config.
Do NOT shell out to git init/git commit directly without setting user config and --no-gpg-sign, and do NOT run lifecycle/strategy handlers from the real repo CWD in tests.
Tests must never read or write the developer's real ~/.config/entire
(contexts.json, version_check.json), ~/.cache/entire (nodes.json,
cluster_cores.json, api_discovery.json), or OS keychain. The developer may be
using entire for real while tests run.
- Single resolver:
internal/entireclient/userdirsis the only place that resolves the per-user config dir (userdirs.Config():$ENTIRE_CONFIG_DIRelse~/.config/entire) and cache dir (userdirs.Cache():$XDG_CACHE_HOME/entireelse~/.cache/entire). Never derive these paths anywhere else. - In-process safety net:
userdirsand thetokenstoredefault backend detectgo test(viainternal/testdirs) and fall back to a throwaway per-process temp directory when their env override is unset. The fallback is shared across tests in one process — for per-test isolation still sett.Setenv("ENTIRE_CONFIG_DIR", t.TempDir())andtokenstore.UseFileBackendForTesting(...). - Spawned binaries are NOT covered:
testing.Testing()is false in a subprocess. The integration and e2e TestMains setENTIRE_CONFIG_DIR,XDG_CACHE_HOME,ENTIRE_TOKEN_STORE=file,ENTIRE_TOKEN_STORE_PATH, andENTIRE_TEST_AUTH_STORE_FILEprocess-wide so every spawnedentire(and every agent-invoked hook) inherits isolation. Any new harness that spawns the real binary must do the same. - Legacy auth store:
auth.NewStore()talks straight to the zalando keyring; packages whose tests can reach it needkeyring.MockInit()inTestMain(seecmd/entire/cli/global_test.go) — thetestdirsfallback does not cover it in-process.
Tests that spawn the real entire or git binary need the child to be non-interactive so prompts don't hang on a developer terminal.
interactive.CanPromptInteractively() resolves in this order:
ENTIRE_TEST_TTY=1→ force interactive ON (any other non-empty value → force OFF).testing.Testing()→ false. In-processgo testruns are non-interactive by default; no per-testt.Setenv("ENTIRE_TEST_TTY", "0")is needed.- Agent sentinels (
GEMINI_CLI,COPILOT_CLI,PI_CODING_AGENT,GIT_TERMINAL_PROMPT=0) → false. CI=<non-empty-non-false>→ false./dev/ttyprobe.
For subprocesses spawning the real entire binary (e2e, integration tests, entire calling itself from a hook), prefer execx.NonInteractive over env-var plumbing:
import "github.com/entireio/cli/cmd/entire/cli/execx"
cmd := execx.NonInteractive(ctx, getTestBinary(), "status")
cmd.Dir = repoDir
out, err := cmd.CombinedOutput()execx.NonInteractive puts the child in a new session with no controlling terminal (Setsid on Unix, DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP on Windows), so the child's /dev/tty probe fails naturally. No env var required.
interactive.UnderTest() returns true when testing.Testing() or ENTIRE_TEST_TTY is set — use it where code needs to skip a real-terminal operation even if CanPromptInteractively() returns true (e.g., reading from /dev/tty directly inside askConfirmTTY).
mise run fmt && mise run lintmise run fmt can rewrite files. Treat mise run fmt && mise run lint as a single verification sequence: if formatting changes anything, run lint again on the formatted tree rather than assuming a previous lint result still applies.
CI will fail if you skip these steps:
mise run checkEquivalent expanded form:
mise run fmt # Format code (CI enforces gofmt)
mise run lint # Lint check (CI enforces golangci-lint)
mise run test:ci # Run all tests (unit + integration)mise run check runs the three commands above.
Safety note: do not treat a clean mise run lint result as final unless it was run after the most recent mise run fmt pass.
Before pushing commits or otherwise sending code changes to any remote, run mise run lint on the current tree and ensure it passes. If mise run fmt changed files, rerun mise run lint on the formatted tree before pushing.
Common CI failures from skipping this:
gofmtformatting differences → runmise run fmt- Lint errors → run
mise run lintand fix issues - Test failures → run
mise run testand fix
Before implementing Go code, use /go:discover-related to find existing utilities and patterns that might be reusable.
Check for duplication:
mise run dup # Comprehensive check (threshold 50) with summary
mise run dup:staged # Check only staged files
mise run lint # Normal lint includes dupl at threshold 75 (new issues only)
mise run lint:full # All issues at threshold 75Tiered thresholds:
- 75 tokens (lint/CI) - Blocks on serious duplication (~20+ lines)
- 50 tokens (dup) - Advisory, catches smaller patterns (~10+ lines)
When duplication is found:
- Check if a helper already exists in
common.goor nearby utility files - If not, consider extracting the duplicated logic to a shared helper
- If duplication is intentional (e.g., test setup), add a
//nolint:duplcomment with explanation
The 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.WorktreeRoot(); 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, 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
All settings access should go through the settings package (cmd/entire/cli/settings/).
Why a separate package:
The settings package exists to avoid import cycles. The cli package imports strategy, so strategy cannot import cli. The settings package provides shared settings loading that both can use.
Usage:
import "github.com/entireio/cli/cmd/entire/cli/settings"
// Load full settings object
s, err := settings.Load()
if err != nil {
// handle error
}
if s.Enabled {
// ...
}
// Or use convenience functions
if settings.IsSummarizeEnabled() {
// ...
}Do NOT:
- Read
.entire/settings.jsonor.entire/settings.local.jsondirectly withos.ReadFile - Duplicate settings parsing logic in other packages
- Create new settings helpers without adding them to the
settingspackage
Key files:
settings/settings.go-EntireSettingsstruct,Load(), and helper methodsconfig.go- Higher-level config functions that use settings (forclipackage consumers)
- Internal/debug logging: Use
logging.Debug/Info/Warn/Error(ctx, msg, attrs...)fromcmd/entire/cli/logging/. Writes to.entire/logs/. - Enabling debug/perf logs locally: Prefer adding
"log_level": "DEBUG"to.entire/settings.local.jsonwhen you need detailed hook/perf logs. This file is gitignored.ENTIRE_LOG_LEVEL=debugalso works and takes precedence. - User-facing output: Use
fmt.Fprint*(cmd.OutOrStdout(), ...)orcmd.ErrOrStderr().
Don't use fmt.Print* for operational messages (checkpoint saves, hook invocations, strategy decisions) - those should use the logging package.
Privacy: Don't log user content (prompts, file contents, commit messages). Log only operational metadata (IDs, counts, paths, durations).
We use github.com/go-git/go-git for most git operations, but with important exceptions:
Never call git.Open, git.PlainOpen, or git.PlainOpenWithOptions directly.
cmd/entire/cli/gitrepo is the single source of truth for opening a
repository. Use gitrepo.OpenCurrent(ctx) for the current worktree or
gitrepo.OpenPath(root) for a specific worktree root. Both funnel through
openPathWithAlternates, which is the only place that opens a *git.Repository.
Routing every open through gitrepo guarantees two behaviours no ad-hoc
git.PlainOpen call gets right:
- Object alternates are rewritten to absolute paths so shared clones resolve
their objects (
PlainOpencannot follow relative/absolute alternates). - Reftable repositories are detected and opened through the git-CLI-backed
reference storer (
reftableStorer). A directgit.PlainOpenon a reftable repo fails outright withunknown extension: refstorage, because go-git's filesystem storer cannot read the reftable backend. The reftable storer also re-approves theobjectformat(sha1/sha256) andworktreeconfigextensions that go-git verifies at open time.
If a code path opens a repo with a bare go-git call, it silently breaks on
reftable and sha256 repositories. Reviewers should flag any new
git.PlainOpen*/git.Open outside gitrepo. Key files: gitrepo/repository.go
(open entry points) and gitrepo/reftable.go (reftableStorer).
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 CheckoutBranch() in git_operations.go for an example.
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 an agent 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.WorktreeRoot()
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.WorktreeRoot()
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.
Control-plane commands dial one of three cores: the active context's
(coreapi.New), a specific cluster's (coreapi.NewForCluster), or — when
ENTIRE_TOKEN is set — the env token's aud (the bypass inside New/
NewForCluster). This precedence lives only inside coreapi; nothing else
re-derives it.
To display which core a request uses, ask the client: client.CoreOrigin().
It returns whatever was actually wired in, so the shown core can never diverge
from where the request goes. Do NOT re-resolve with
auth.ResolveControlPlaneTarget() for display — it only knows the active
context and silently ignores both ENTIRE_TOKEN and the cluster case, so it can
name a core the request never touches (this was a real bug in the mirror list
banner; see repo_mirror.go and coreapi.Client.CoreOrigin).
When a command resolves auth outside a coreapi.Client (e.g. entire auth status, which builds its own /me client), it must apply the same
env-token-first precedence itself — see resolveAuthStatusTarget /
resolveEnvTokenStatusTarget in auth.go, which branch on
auth.EnvTokenVar before falling back to the active context. logout is the
deliberate exception: it manages a stored login session, which an ephemeral
env token has none of, so it stays on the active context.
The data plane (entire-api) is deployed per jurisdiction; a repo placement
lives in exactly one cell, user /me/* activity is consolidated in the
caller's home cell, and no server-side cross-cell aggregator exists. The CLI
therefore has exactly three routing shapes, mirroring the entire.io BFF:
- Repo-scoped → one cell:
resolveRepoCellTarget(cell_target.go) maps a repo (ULID or owner/repo) to the cell hosting it via mirrors + the cluster catalog. Best-effort: any failure returns nil and the auth layer falls back to home-jurisdiction routing. Used by experts (NewAuthenticatedEntireAPICellClientinapi_client.go). - User-scoped
/me→ home cell, never fan out:auth.NewEntireAPICellClient(ctx, insecure, nil)routes by thehome_jurisdictionJWT claim; activity/recap use it with a data-API fallback (runAuthenticatedActivityAPIinentireapi_client.go). - Repo-set queries → fan out and merge client-side:
cell_fanout.go—groupReposByCell(repo index → per-cell groups; the catalog join key isClusterSlug↔Cluster.Slug, NOT the cell name, which the catalog does not expose),resolveCellBaseURLs, andfanOutCells(parallel per-cell calls, per-cell timeout, partial failures isolated per slot). Merge semantics stay with the command.
Token rule: identity tokens are per-jurisdiction, not per-cell. Multi-cell
callers must build one auth.CellClientFactory
(NewEntireAPICellClientFactory) per operation — it resolves the login
subject once and mints at most one token per jurisdiction. fanOutCells does
this automatically; do not call NewEntireAPICellClient in a loop.
The CLI uses a manual-commit strategy for managing session data and checkpoints. The strategy implements the Strategy interface defined in strategy.go.
The Strategy interface provides:
SaveStep()- Save session step checkpoint (code + metadata)SaveTaskStep()- Save subagent task step checkpointGetRewindPoints()/Rewind()- List and restore to checkpointsGetSessionLog()/GetSessionInfo()- Retrieve session data
The manual-commit strategy (manual_commit*.go) does not modify the active branch - no commits are created on the working branch. Instead it:
- Creates shadow branch
entire/<HEAD-commit-hash[:7]>-<worktreeHash[:6]>per base commit + worktree - Worktree-specific branches - each git worktree gets its own shadow branch namespace, preventing conflicts
- Supports multiple concurrent sessions - checkpoints from different sessions in the same directory interleave on the same shadow branch
- Condenses session logs to permanent
entire/checkpoints/v1branch on user commits - Each committed session stores the raw transcript (
full.jsonl, read by CLI rewind/resume/explain) plus a best-effort compact transcript (transcript.jsonl, generated viatranscript/compact). Likefull.jsonl,transcript.jsonlstores the full compacted session on every checkpoint (viacompact.FullWithBoundary), so each checkpoint is self-contained and the session survives a mid-history checkpoint being lost/reverted/rebased. This checkpoint's slice begins at the session metadata'scompact_transcript_start(a line offset in compact-output coordinates, distinct fromcheckpoint_transcript_startwhich indexes rawfull.jsonllines); a nil/absent marker means a legacy delta-onlytranscript.jsonl(read from line 0). The marker rounds toward inclusion when a streaming message straddles the boundary, so the slice never drops this checkpoint's content but may repeat ≤1 merged line at its head. Compact generation is best-effort and is skipped when the compacted output exceeds the 50MB blob cap (unlikefull.jsonl,transcript.jsonlis not chunked —full.jsonlstays authoritative and the compact is regenerable); in the OPF finalize rewrite a failed/skipped regeneration drops the priortranscript.jsonland clears the marker rather than shipping a stale, less-redacted compact. Both files are pushed with the v1 branch. The rootmetadata.jsonsessions[].transcriptpointer keeps targetingfull.jsonl; when the compact transcript was generated the session entry also carries acompact_transcriptpath pointing attranscript.jsonl(omitted otherwise) so external readers can locate it next tofull.jsonl. - Uses the
post-rewriteGit hook to keep local session linkage aligned after amend/rebase rewrites - Builds git trees in-memory using go-git plumbing APIs
- Rewind restores files from shadow branch commit tree (does not use
git reset) - Location-independent transcript resolution - transcript paths are always computed dynamically from the current repo location (via
agent.GetSessionDir+agent.ResolveSessionFile), never stored in checkpoint metadata. This ensures restore/rewind works after repo relocation or across machines. - Token usage scoping -
SessionState.TokenUsageis the session-wide total used byentire status;SessionState.CheckpointTokenUsageis the pending checkpoint delta since the last condensation. Checkpoint metadata must stay scoped toCheckpointTranscriptStartor the pending checkpoint delta. Cursor tokens come only from stop-hook payloads, while Copilot CLI can also backfill full-session totals fromsession.shutdown. - Tracks session state in
.git/entire-sessions/(shared across worktrees) - Shadow branch migration - if user does stash/pull/rebase (HEAD changes without commit), shadow branch is automatically moved to new base commit
- Orphaned branch cleanup - if a shadow branch exists without a corresponding session state file, it is automatically reset when a new session starts
- PrePush hook can push
entire/checkpoints/v1branch alongside user pushes - OPF (OpenAI Privacy Filter) runs at pre-push, not post-commit: when
redaction.openai_privacy_filter.enabledis true, the PrePush hook re-redacts unpushedentire/checkpoints/v1commits with the OPF 9th layer, builds new commits carrying anEntire-OPF-Applied: truetrailer, and atomically updates the local v1 ref before pushing. Per-commit condensation stays on the fast 8-layer pipeline. Seestrategy/manual_commit_opf_rewrite.goanddocs/security-and-privacy.mdfor the full flow, including divergence detection, bootstrap caps, and CAS-on-conflict semantics. - Safe to use on main/master since it never modifies commit history
strategy.go- Interface definition and context structs (StepContext,TaskStepContext,RewindPoint, etc.)common.go- Helpers for metadata extraction, tree building, rewind validation,ListCheckpoints()manual_commit*.go- Manual-commit strategy: main impl, types, session state, condensation, rewind, git ops, logs, hook handlers (prepare-commit-msg, post-commit, post-rewrite, pre-push), resetmanual_commit_opf_rewrite.go- Pre-push OPF re-redaction: walks unpushed v1 commits, runs OPF over their blobs, rebuilds commits withEntire-OPF-Applied: truetrailer, CAS-updates the local ref. Sentinel error types (useerrors.As):V1DivergedError,BootstrapTooLargeError,V1RefMovedError,OPFRuntimeFailedError.cleanup.go- Cleanup discovery/deletion for shadow branches, session states, and checkpoint metadatasession_state.go- Package-level session state functionshooks.go- Git hook installation
Note: checkpoint/configloader.go overrides go-git's default config loader with a symlink-following billy.Basic (osSymlinkFS) — go-git's default reads config via os.Root, which rejects absolute symlinks in any path component (e.g. a ~/.config managed by a dotfile tool), silently dropping global config so author identity fell back to "Unknown" and signing was skipped.
The phase state machine, metadata directory layout, sharded checkpoint format, multi-session metadata, checkpoint ID linking, commit trailers, and concurrent-session / shadow-branch-migration behavior are documented in:
- Sessions and Checkpoints - domain model, storage layout, checkpoint ID linking, commit trailers, package structure
- Checkpoint Scenarios - phase state machine and worked condensation scenarios
- Ref-Based Checkpoint Backend - git-refs backend: primary/mirror taxonomy, ref layout + sharding, push-discovery queue, read routing, config + rollout
- The strategy must implement the full
Strategyinterface - Test with
mise run test- strategy tests are in*_test.gofiles - Keep this file and
docs/architecture/sessions-and-checkpoints.mdcurrent when changing strategy behavior (AGENTS.mdis a symlink to this file)
entire review runs a configured review profile. Keep documentation brief and user-facing.
See Review Command for usage, minimal profile config, and key files.
When building CLI features, do not make useful output available only through a TUI, picker, wizard, terminal selection menu, confirmation dialog, or stdin question. Agents must be able to complete the same read-only workflow from a non-interactive terminal.
Plain text output is acceptable when it contains the full information needed for
the workflow. JSON is preferred for structured data, following existing patterns
such as --json on status, agent-help, sessions, search, and trail
finding commands. Long human-readable output may use a pager in TTY mode, but
must provide a bypass like the existing --no-pager pattern on explain.
For interactive browsing flows, provide one of these non-interactive shapes:
- a list command that prints stable identifiers, plus a show/detail command that accepts an identifier
- a flag or positional argument that selects the item directly
- a complete text or JSON fallback when stdout is not a terminal, like existing static/text fallbacks for TUI-backed commands
When reviewing CLI changes, inspect terminal-gated paths such as
IsTerminalWriter, CanPromptInteractively, Bubble Tea, huh, direct stdin
reads, terminal selection menus, confirmation dialogs, and wizard flows. Flag
the change if a non-interactive agent can only see a menu, preview, truncated
summary, or cannot select the item whose details matter.
Tests for interactive CLI features should cover the non-interactive path. See
the "Spawning subprocesses in tests (TTY detection)" section above for the
execx.NonInteractive pattern when testing a real entire command.
Existing good patterns:
entire investigate --findingsprints a complete plain-text list and includesview: entire investigate show <run-id>hints.entire investigate show <run-id>prints the saved investigation summary and findings without needing a TUI.entire repo clone /gh/...prompts only when several clusters are possible; without a TTY it asks for--cluster.entire experts --tuiis safe because the TUI is opt-in and non-TTY output falls back to deterministic plain text.entire explain --no-pageris the local pattern for avoiding pager-only long text output.entire status --json,entire agent-help --json,entire sessions list --json, and trail finding commands show the local--jsonconvention.
Do not require JSON everywhere. Human-readable text is fine if it contains the complete information an agent needs. The failure mode is requiring an interactive terminal to select something or reveal details.
- Before committing: Follow the "Before Every Commit (REQUIRED)" checklist above - CI will fail without it
- Integration tests: run
mise run test:integrationwhen changing integration test code - 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