Skip to content

Latest commit

 

History

History
778 lines (587 loc) · 41.1 KB

File metadata and controls

778 lines (587 loc) · 41.1 KB

Entire - CLI

This repo contains the CLI for Entire.

Architecture

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

Key Directories

Commands (cmd/)

  • 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 implementations
  • entire/cli/agent: agent implementations (Claude Code, Gemini CLI, OpenCode, Cursor, Factory AI Droid, Copilot CLI, Pi) - see Agent Integration Checklist and Agent Implementation Guide
  • entire/cli/strategy: strategy implementation (manual-commit) - see section below
  • entire/cli/checkpoint: checkpoint storage abstractions (ephemeral and persistent)
  • entire/cli/session: session state management
  • entire/cli/integration_test: integration tests (simulated hooks)
  • e2e/: E2E tests with real agent calls (see e2e/README.md)

Command Layout

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. resume with 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 (--force overwrites it from the checkpoint). adopt moves 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 deprecated rewind (functional, prints a cobra deprecation message, will be removed in a future release)
  • agent: bare opens the interactive agent selector, plus list, add, remove
  • configure: bare prints help and a hint pointing at entire agent; flags manage non-agent settings (telemetry, git-hook installation mode, strategy options, summary provider). Agent CRUD lives under entire agent.
  • auth: login, logout, status, contexts, use, plus token (prints the active control-plane bearer to stdout for scripting/curl; honors ENTIRE_TOKEN, else the refreshed active-context login JWT). token also 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 exchanges ENTIRE_TOKEN when set (deriving the environment from the env token's aud), else the active login. auth status shows the caller's home jurisdiction so the slug is discoverable. logout takes --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, plus trace, logs, bundle
  • org: control-plane organization management — create, list, get, delete
  • project: control-plane project management — create, list, get, delete
  • repo: control-plane repository lifecycle — create, list, get, delete, clone, plus the mirror and visibility subtrees. Git content operations (log, diff, …) are intentionally out of scope.
  • grant: manage access grants and org membership — org, project, and repo each support add / 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): resumesession resume, attachsession attach, explaincheckpoint explain, tracedoctor trace. Cobra-native aliases (no hint): sessionssession, cp/checkpointscheckpoint. 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): resetclean, 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.

Tech Stack

  • Language: Go 1.26.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

This 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/.

Running E2E Canary Tests (Vogon Agent)

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) and cmd/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 run mise run test:e2e:canary after changing prompt text and fix Vogon's parsing if tests fail.

Running E2E Tests (Only When Explicitly Requested)

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 agents

E2E tests:

  • Use the //go:build e2e build tag
  • Located in e2e/tests/
  • See e2e/README.md for 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_AGENT env 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: haiku for cost efficiency)
  • E2E_TIMEOUT - Timeout per prompt (default: 2m)

Test Parallelization

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().

Git in Tests

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 resolution

testutil.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.

Config/Cache/Keyring Isolation 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/userdirs is the only place that resolves the per-user config dir (userdirs.Config(): $ENTIRE_CONFIG_DIR else ~/.config/entire) and cache dir (userdirs.Cache(): $XDG_CACHE_HOME/entire else ~/.cache/entire). Never derive these paths anywhere else.
  • In-process safety net: userdirs and the tokenstore default backend detect go test (via internal/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 set t.Setenv("ENTIRE_CONFIG_DIR", t.TempDir()) and tokenstore.UseFileBackendForTesting(...).
  • Spawned binaries are NOT covered: testing.Testing() is false in a subprocess. The integration and e2e TestMains set ENTIRE_CONFIG_DIR, XDG_CACHE_HOME, ENTIRE_TOKEN_STORE=file, ENTIRE_TOKEN_STORE_PATH, and ENTIRE_TEST_AUTH_STORE_FILE process-wide so every spawned entire (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 need keyring.MockInit() in TestMain (see cmd/entire/cli/global_test.go) — the testdirs fallback does not cover it in-process.

Spawning subprocesses in tests (TTY detection)

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:

  1. ENTIRE_TEST_TTY=1 → force interactive ON (any other non-empty value → force OFF).
  2. testing.Testing() → false. In-process go test runs are non-interactive by default; no per-test t.Setenv("ENTIRE_TEST_TTY", "0") is needed.
  3. Agent sentinels (GEMINI_CLI, COPILOT_CLI, PI_CODING_AGENT, GIT_TERMINAL_PROMPT=0) → false.
  4. CI=<non-empty-non-false> → false.
  5. /dev/tty probe.

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).

Linting and Formatting

mise run fmt && mise run lint

mise 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.

Before Every Commit (REQUIRED)

CI will fail if you skip these steps:

mise run check

Equivalent 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 Any Push Or Remote Code Update (REQUIRED)

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:

  • gofmt formatting differences → run mise run fmt
  • Lint errors → run mise run lint and fix issues
  • Test failures → run mise run test and fix

Code Duplication Prevention

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 75

Tiered thresholds:

  • 75 tokens (lint/CI) - Blocks on serious duplication (~20+ lines)
  • 50 tokens (dup) - Advisory, catches smaller patterns (~10+ lines)

When duplication is found:

  1. Check if a helper already exists in common.go or nearby utility files
  2. If not, consider extracting the duplicated logic to a shared helper
  3. If duplication is intentional (e.g., test setup), add a //nolint:dupl comment with explanation

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.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 - Defines SilentError type and NewSilentError() constructor
  • root.go - Sets SilenceErrors: true on root command
  • main.go - Checks for SilentError before printing

Settings

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.json or .entire/settings.local.json directly with os.ReadFile
  • Duplicate settings parsing logic in other packages
  • Create new settings helpers without adding them to the settings package

Key files:

  • settings/settings.go - EntireSettings struct, Load(), and helper methods
  • config.go - Higher-level config functions that use settings (for cli package consumers)

Logging vs User Output

  • Internal/debug logging: Use logging.Debug/Info/Warn/Error(ctx, msg, attrs...) from cmd/entire/cli/logging/. Writes to .entire/logs/.
  • Enabling debug/perf logs locally: Prefer adding "log_level": "DEBUG" to .entire/settings.local.json when you need detailed hook/perf logs. This file is gitignored. ENTIRE_LOG_LEVEL=debug also works and takes precedence.
  • User-facing output: Use fmt.Fprint*(cmd.OutOrStdout(), ...) or cmd.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).

Git Operations

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

Opening Repositories - Always Use gitrepo

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 (PlainOpen cannot follow relative/absolute alternates).
  • Reftable repositories are detected and opened through the git-CLI-backed reference storer (reftableStorer). A direct git.PlainOpen on a reftable repo fails outright with unknown extension: refstorage, because go-git's filesystem storer cannot read the reftable backend. The reftable storer also re-approves the objectformat (sha1/sha256) and worktreeconfig extensions 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).

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 CheckoutBranch() in git_operations.go for an example.

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 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 Core Resolution (which core am I talking to?)

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.

Entire-API Cell Routing (which cell does a data-plane request go to?)

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 (NewAuthenticatedEntireAPICellClient in api_client.go).
  • User-scoped /me → home cell, never fan out: auth.NewEntireAPICellClient(ctx, insecure, nil) routes by the home_jurisdiction JWT claim; activity/recap use it with a data-API fallback (runAuthenticatedActivityAPI in entireapi_client.go).
  • Repo-set queries → fan out and merge client-side: cell_fanout.gogroupReposByCell (repo index → per-cell groups; the catalog join key is ClusterSlugCluster.Slug, NOT the cell name, which the catalog does not expose), resolveCellBaseURLs, and fanOutCells (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.

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

The CLI uses a manual-commit strategy for managing session data and checkpoints. The strategy implements the Strategy interface defined in strategy.go.

Strategy Interface

The Strategy interface provides:

  • SaveStep() - Save session step checkpoint (code + metadata)
  • SaveTaskStep() - Save subagent task step checkpoint
  • GetRewindPoints() / Rewind() - List and restore to checkpoints
  • GetSessionLog() / GetSessionInfo() - Retrieve session data

How It Works

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/v1 branch 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 via transcript/compact). Like full.jsonl, transcript.jsonl stores the full compacted session on every checkpoint (via compact.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's compact_transcript_start (a line offset in compact-output coordinates, distinct from checkpoint_transcript_start which indexes raw full.jsonl lines); a nil/absent marker means a legacy delta-only transcript.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 (unlike full.jsonl, transcript.jsonl is not chunked — full.jsonl stays authoritative and the compact is regenerable); in the OPF finalize rewrite a failed/skipped regeneration drops the prior transcript.jsonl and clears the marker rather than shipping a stale, less-redacted compact. Both files are pushed with the v1 branch. The root metadata.json sessions[].transcript pointer keeps targeting full.jsonl; when the compact transcript was generated the session entry also carries a compact_transcript path pointing at transcript.jsonl (omitted otherwise) so external readers can locate it next to full.jsonl.
  • Uses the post-rewrite Git 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.TokenUsage is the session-wide total used by entire status; SessionState.CheckpointTokenUsage is the pending checkpoint delta since the last condensation. Checkpoint metadata must stay scoped to CheckpointTranscriptStart or the pending checkpoint delta. Cursor tokens come only from stop-hook payloads, while Copilot CLI can also backfill full-session totals from session.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/v1 branch alongside user pushes
  • OPF (OpenAI Privacy Filter) runs at pre-push, not post-commit: when redaction.openai_privacy_filter.enabled is true, the PrePush hook re-redacts unpushed entire/checkpoints/v1 commits with the OPF 9th layer, builds new commits carrying an Entire-OPF-Applied: true trailer, and atomically updates the local v1 ref before pushing. Per-commit condensation stays on the fast 8-layer pipeline. See strategy/manual_commit_opf_rewrite.go and docs/security-and-privacy.md for 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

Key Files

  • 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), reset
  • manual_commit_opf_rewrite.go - Pre-push OPF re-redaction: walks unpushed v1 commits, runs OPF over their blobs, rebuilds commits with Entire-OPF-Applied: true trailer, CAS-updates the local ref. Sentinel error types (use errors.As): V1DivergedError, BootstrapTooLargeError, V1RefMovedError, OPFRuntimeFailedError.
  • cleanup.go - Cleanup discovery/deletion for shadow branches, session states, and checkpoint metadata
  • session_state.go - Package-level session state functions
  • hooks.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.

Deep-Dive Reference

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:

When Modifying the Strategy

  • The strategy must implement the full Strategy interface
  • Test with mise run test - strategy tests are in *_test.go files
  • Keep this file and docs/architecture/sessions-and-checkpoints.md current when changing strategy behavior (AGENTS.md is a symlink to this file)

entire review Command

entire review runs a configured review profile. Keep documentation brief and user-facing.

See Review Command for usage, minimal profile config, and key files.

Agent-Safe CLI Fallbacks

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 --findings prints a complete plain-text list and includes view: 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 --tui is safe because the TUI is opt-in and non-TTY output falls back to deterministic plain text.
  • entire explain --no-pager is 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 --json convention.

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.

Important Notes

  • Before committing: Follow the "Before Every Commit (REQUIRED)" checklist above - CI will fail without it
  • Integration tests: run mise run test:integration when changing integration test code
  • 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