Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9de403c
paths: add v1.1 custom-ref constants
computermode May 20, 2026
ac8bab6
settings: parse checkpoints_version 1.1 (major version 1)
computermode May 20, 2026
4cd7c59
settings: add UsesCustomMetadataRef helper
computermode May 20, 2026
96879b9
checkpoint: add MetadataRef and MetadataTrackingRef resolvers
computermode May 20, 2026
f7f550a
checkpoint: preserve v1 history at custom ref on opt-in
computermode May 20, 2026
7b3e2b9
strategy: EnsureMetadataBranch preserves v1 history via resolver
computermode May 20, 2026
dfdc086
strategy: GetMetadata*BranchTree use MetadataRef resolver
computermode May 20, 2026
b6fdd7f
checkpoint: route committed.go through resolver + preserve hook
computermode May 20, 2026
12b5c8c
checkpoint: route parse_tree.go through resolver
computermode May 20, 2026
e8a5bbf
cli: route remaining metadata-ref sites through resolver
computermode May 20, 2026
3b6de6f
strategy: refactor push hook to take full ref names
computermode May 20, 2026
7f51a3d
setup: runEnable installs 1.1 fetch refspec when configured
computermode May 20, 2026
a9dff4f
integration: cover v1.1 fresh-repo, preservation, regression, push
computermode May 20, 2026
c20bdf9
docs: document checkpoints_version 1.1
computermode May 20, 2026
aa09f57
fix: lint nits across v1.1 changes
computermode May 20, 2026
20037d1
fix: address PR1242 bugbot findings
computermode May 20, 2026
ea2362b
fix: address PR1242 copilot findings
computermode May 20, 2026
fe7b0a1
refactor: simplify v1.1 changes after review
computermode May 20, 2026
341bb90
revert: drop PreserveV1HistoryAndLog wrapper
computermode May 20, 2026
de53c9d
1.1: drop legacy-branch history preservation for now
computermode May 21, 2026
b47ffc5
integration: drop refHash dead helper
computermode May 21, 2026
d0ef3bf
test: use execx.NonInteractive for git in setup_test.go
computermode May 21, 2026
6b83ab0
'entire review' fixes
computermode May 21, 2026
f38fd04
Merge branch 'main' into initial-v1.1-setup
computermode May 21, 2026
2d65aed
Merge branch 'main' into initial-v1.1-setup
computermode May 26, 2026
b134ab9
Rename v1.1 custom ref to refs/entire/checkpoints/v1.1
computermode May 28, 2026
722e8ae
Merge remote-tracking branch 'origin/main' into initial-v1.1-setup
computermode May 28, 2026
20e9c8c
Merge remote-tracking branch 'origin/main' into initial-v1.1-setup
computermode May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,40 @@ The manual-commit strategy (`manual_commit*.go`) does not modify the active bran
- PrePush hook can push `entire/checkpoints/v1` branch alongside user pushes
- Safe to use on main/master since it never modifies commit history

##### v1.1: custom-ref storage (opt-in)

When `strategy_options.checkpoints_version` is set to `1.1` (number `1.1`
or string `"1.1"`) in `.entire/settings.json`, metadata is stored at the
custom ref `refs/entire/checkpoints/v1.1` instead of the branch
`refs/heads/entire/checkpoints/v1`. Same format, same trailers, same
sharded tree layout; only the ref location differs. The custom ref is
invisible to `git branch -a` and to GitHub's branch UI, and is not pulled
by default `git clone`. The `v1.1` suffix (vs. plain `v1`) keeps the
custom ref distinct from the legacy branch so a single repo can hold
both refs side-by-side while history is being migrated.

Activation is **manual opt-in**: edit `.entire/settings.json` to set
`strategy_options.checkpoints_version` to `1.1`. `entire enable` does not
auto-enroll. On existing v1 repos with prior history, the custom ref
starts empty — past checkpoints on the legacy branch are NOT
automatically reachable under 1.1. A future migration command can handle
that; today, a user who wants their old history reachable can run, once:

```
git update-ref refs/entire/checkpoints/v1.1 refs/heads/entire/checkpoints/v1
```

The fetch refspec
`+refs/entire/checkpoints/v1.1:refs/entire/remotes/origin/checkpoints/v1.1`
is installed on origin by `entire enable` whenever settings already
specify 1.1. Push and rebase-on-non-FF use the same machinery as the
legacy branch path via `strategy.pushRefIfNeeded`.

Key entry points: `checkpoint.MetadataRef(ctx)`,
`checkpoint.MetadataTrackingRef(ctx)`,
`paths.MetadataRefName`, `paths.MetadataTrackingRefName`,
`settings.EntireSettings.UsesCustomMetadataRef`.

#### Key Files

- `strategy.go` - Interface definition and context structs (`StepContext`, `TaskStepContext`, `RewindPoint`, etc.)
Expand Down
28 changes: 21 additions & 7 deletions cmd/entire/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/entireio/cli/cmd/entire/cli/logging"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/session"
"github.com/entireio/cli/cmd/entire/cli/settings"
"github.com/entireio/cli/cmd/entire/cli/strategy"
"github.com/entireio/cli/cmd/entire/cli/trailers"
"github.com/entireio/cli/cmd/entire/cli/validation"
Expand All @@ -31,7 +32,6 @@ import (

"charm.land/huh/v2"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"
"github.com/go-git/go-git/v6/plumbing/object"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -384,7 +384,7 @@ func ensureCheckpointAvailable(ctx, logCtx context.Context, repo *git.Repository
}
}

branchDescription := "entire/checkpoints/v1 branch"
branchDescription := cpkg.RefDisplayName(cpkg.MetadataRef(ctx)) + " ref"
return repo, fmt.Errorf(
"checkpoint %s referenced by HEAD is missing from the local %s after a refresh attempt. Creating a fresh checkpoint here would overwrite the original session data on push. Run:\n\n %s\n\nthen re-run attach. If the colleague who made this commit hasn't pushed their checkpoint metadata yet, ask them to do so first",
checkpointID.String(), branchDescription, suggestCheckpointFetchCommand(logCtx),
Expand All @@ -400,10 +400,10 @@ func refreshCheckpointRefs(ctx context.Context) (*git.Repository, error) {
}

// checkpointPresentLocally reports whether the checkpoint already exists on
// the local v1 ref we would write to. Remote-tracking alone is not enough;
// see ensureCheckpointAvailable.
// the local metadata ref we would write to. Remote-tracking alone is not
// enough; see ensureCheckpointAvailable.
func checkpointPresentLocally(ctx context.Context, repo *git.Repository, checkpointID id.CheckpointID) (bool, error) {
localRef := plumbing.NewBranchReferenceName(paths.MetadataBranchName)
localRef := cpkg.MetadataRef(ctx)
if _, err := repo.Reference(localRef, true); err != nil {
// Local branch ref doesn't exist — treat as "not present locally".
// We deliberately do not fall back to remote-tracking: see
Expand All @@ -418,9 +418,12 @@ func checkpointPresentLocally(ctx context.Context, repo *git.Repository, checkpo
}

// suggestCheckpointFetchCommand returns a git fetch command the user can
// paste to pull the missing v1 metadata branch.
// paste to pull the missing metadata ref. Legacy v1 lives on a regular branch
// and its short name is enough, while 1.1 uses a fully-qualified src:dst pair
// so the fetched ref lands in the tracking namespace instead of clobbering the
// local ref.
func suggestCheckpointFetchCommand(ctx context.Context) string {
ref := "entire/checkpoints/v1:entire/checkpoints/v1"
ref := metadataFetchRefspec(ctx)
if remote.Configured(ctx) {
if url, err := remote.FetchURL(ctx); err == nil && url != "" {
return fmt.Sprintf("git fetch %s %s", url, ref)
Expand All @@ -429,6 +432,17 @@ func suggestCheckpointFetchCommand(ctx context.Context) string {
return "git fetch origin " + ref
}

// metadataFetchRefspec returns the src:dst pair a user should paste into
// `git fetch origin <refspec>` to pull v1 metadata. For legacy v1 it
// refers to the branch by short name; for 1.1 it uses the fully-qualified
// custom ref and the separate tracking namespace.
func metadataFetchRefspec(ctx context.Context) string {
if settings.UsesCustomMetadataRef(ctx) {
return "+" + paths.MetadataRefName + ":" + paths.MetadataTrackingRefName
}
return paths.MetadataBranchName + ":" + paths.MetadataBranchName
}

func resolveCheckpointID(headCommit *object.Commit) (id.CheckpointID, bool) {
existing := trailers.ParseAllCheckpoints(headCommit.Message)
if len(existing) > 0 {
Expand Down
6 changes: 3 additions & 3 deletions cmd/entire/cli/attach_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ func TestAttach_RefusesWhenCheckpointMissingFromLocalBranch(t *testing.T) {
if err == nil {
t.Fatal("expected error: checkpoint referenced by HEAD is missing locally and attach should refuse")
}
if !strings.Contains(err.Error(), "missing from the local entire/checkpoints/v1 branch") {
if !strings.Contains(err.Error(), "missing from the local entire/checkpoints/v1 ref") {
t.Errorf("error message should explain the missing-branch situation; got: %v", err)
}
if !strings.Contains(err.Error(), "git fetch origin entire/checkpoints/v1") {
Expand Down Expand Up @@ -463,7 +463,7 @@ func TestAttach_RefusesWhenCheckpointOnlyInRemoteTrackingRef(t *testing.T) {
if err == nil {
t.Fatal("expected attach to refuse when checkpoint is only in the remote-tracking ref")
}
if !strings.Contains(err.Error(), "missing from the local entire/checkpoints/v1 branch") {
if !strings.Contains(err.Error(), "missing from the local entire/checkpoints/v1 ref") {
t.Errorf("error should explain the local-branch gap; got: %v", err)
}

Expand Down Expand Up @@ -1173,7 +1173,7 @@ func TestAttach_ReviewRefusesWhenCheckpointMissingFromLocalBranch(t *testing.T)
if err == nil {
t.Fatal("expected error: checkpoint referenced by HEAD is missing locally and attach should refuse")
}
if !strings.Contains(err.Error(), "missing from the local entire/checkpoints/v1 branch") {
if !strings.Contains(err.Error(), "missing from the local entire/checkpoints/v1 ref") {
t.Errorf("error message should explain the missing-branch situation; got: %v", err)
}
if !strings.Contains(err.Error(), "git fetch origin entire/checkpoints/v1") {
Expand Down
10 changes: 5 additions & 5 deletions cmd/entire/cli/checkpoint/blob_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestBlobResolver_HasBlob_Present(t *testing.T) {
repo, store, cpID := setupRepoForUpdate(t)

// Get the metadata branch tree
tree, err := store.getSessionsBranchTree()
tree, err := store.getSessionsBranchTree(context.Background())
if err != nil {
t.Fatalf("getSessionsBranchTree() error = %v", err)
}
Expand Down Expand Up @@ -56,7 +56,7 @@ func TestBlobResolver_ReadBlob(t *testing.T) {

repo, store, cpID := setupRepoForUpdate(t)

tree, err := store.getSessionsBranchTree()
tree, err := store.getSessionsBranchTree(context.Background())
if err != nil {
t.Fatalf("getSessionsBranchTree() error = %v", err)
}
Expand Down Expand Up @@ -102,7 +102,7 @@ func TestCollectTranscriptBlobHashes_SingleSession(t *testing.T) {

_, store, cpID := setupRepoForUpdate(t)

tree, err := store.getSessionsBranchTree()
tree, err := store.getSessionsBranchTree(context.Background())
if err != nil {
t.Fatalf("getSessionsBranchTree() error = %v", err)
}
Expand Down Expand Up @@ -147,7 +147,7 @@ func TestCollectTranscriptBlobHashes_MultiSession(t *testing.T) {
t.Fatalf("WriteCommitted() for second session error = %v", err)
}

tree, err := store.getSessionsBranchTree()
tree, err := store.getSessionsBranchTree(context.Background())
if err != nil {
t.Fatalf("getSessionsBranchTree() error = %v", err)
}
Expand Down Expand Up @@ -188,7 +188,7 @@ func TestCollectTranscriptBlobHashes_NonexistentCheckpoint(t *testing.T) {

_, store, _ := setupRepoForUpdate(t)

tree, err := store.getSessionsBranchTree()
tree, err := store.getSessionsBranchTree(context.Background())
if err != nil {
t.Fatalf("getSessionsBranchTree() error = %v", err)
}
Expand Down
44 changes: 22 additions & 22 deletions cmd/entire/cli/checkpoint/committed.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (s *GitStore) WriteCommitted(ctx context.Context, opts WriteCommittedOption
}

// Get branch ref and root tree hash (O(1), no flatten)
parentHash, rootTreeHash, err := s.getSessionsBranchRef()
parentHash, rootTreeHash, err := s.getSessionsBranchRef(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -123,7 +123,7 @@ func (s *GitStore) WriteCommitted(ctx context.Context, opts WriteCommittedOption
return err
}

refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName)
refName := MetadataRef(ctx)
newRef := plumbing.NewHashReference(refName, newCommitHash)
if err := s.repo.Storer.SetReference(newRef); err != nil {
return fmt.Errorf("failed to set branch reference: %w", err)
Expand Down Expand Up @@ -339,8 +339,8 @@ func (s *GitStore) writeStandardCheckpointEntries(ctx context.Context, opts Writ
slog.String("write_session_id", opts.SessionID),
slog.Bool("existing_summary_nil", existingSummary == nil))
return fmt.Errorf(
"refusing to overwrite session 0 of checkpoint %s: existing session ID %q differs from write session ID %q. The checkpoint tree is inconsistent (session 0 belongs to a different session than this write claims). No automated repair exists for this shape — please report it along with the output of `git ls-tree entire/checkpoints/v1 %s/`",
opts.CheckpointID, existingMeta.SessionID, opts.SessionID, opts.CheckpointID.Path(),
"refusing to overwrite session 0 of checkpoint %s: existing session ID %q differs from write session ID %q. The checkpoint tree is inconsistent (session 0 belongs to a different session than this write claims). No automated repair exists for this shape — please report it along with the output of `git ls-tree %s %s/`",
opts.CheckpointID, existingMeta.SessionID, opts.SessionID, RefDisplayName(MetadataRef(ctx)), opts.CheckpointID.Path(),
)
}
}
Expand Down Expand Up @@ -549,7 +549,7 @@ func (s *GitStore) UpdateCheckpointSummary(ctx context.Context, checkpointID id.
return fmt.Errorf("failed to ensure sessions branch: %w", err)
}

parentHash, rootTreeHash, err := s.getSessionsBranchRef()
parentHash, rootTreeHash, err := s.getSessionsBranchRef(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -599,7 +599,7 @@ func (s *GitStore) UpdateCheckpointSummary(ctx context.Context, checkpointID id.
return err
}

refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName)
refName := MetadataRef(ctx)
newRef := plumbing.NewHashReference(refName, newCommitHash)
if err := s.repo.Storer.SetReference(newRef); err != nil {
return fmt.Errorf("failed to set branch reference: %w", err)
Expand Down Expand Up @@ -1213,7 +1213,7 @@ func (s *GitStore) ListCommitted(ctx context.Context) ([]CommittedInfo, error) {
return nil, err //nolint:wrapcheck // Propagating context cancellation
}

tree, err := s.getSessionsBranchTree()
tree, err := s.getSessionsBranchTree(ctx)
if err != nil {
return []CommittedInfo{}, nil //nolint:nilerr // No sessions branch means empty list
}
Expand Down Expand Up @@ -1354,7 +1354,7 @@ func (s *GitStore) UpdateSummary(ctx context.Context, checkpointID id.Checkpoint
}

// Get branch ref and root tree hash (O(1), no flatten)
parentHash, rootTreeHash, err := s.getSessionsBranchRef()
parentHash, rootTreeHash, err := s.getSessionsBranchRef(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -1424,7 +1424,7 @@ func (s *GitStore) UpdateSummary(ctx context.Context, checkpointID id.Checkpoint
return err
}

refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName)
refName := MetadataRef(ctx)
newRef := plumbing.NewHashReference(refName, newCommitHash)
if err := s.repo.Storer.SetReference(newRef); err != nil {
return fmt.Errorf("failed to set branch reference: %w", err)
Expand Down Expand Up @@ -1452,7 +1452,7 @@ func (s *GitStore) UpdateCommitted(ctx context.Context, opts UpdateCommittedOpti
}

// Get branch ref and root tree hash (O(1), no flatten)
parentHash, rootTreeHash, err := s.getSessionsBranchRef()
parentHash, rootTreeHash, err := s.getSessionsBranchRef(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -1543,7 +1543,7 @@ func (s *GitStore) UpdateCommitted(ctx context.Context, opts UpdateCommittedOpti
return err
}

refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName)
refName := MetadataRef(ctx)
newRef := plumbing.NewHashReference(refName, newCommitHash)
if err := s.repo.Storer.SetReference(newRef); err != nil {
return fmt.Errorf("failed to set branch reference: %w", err)
Expand Down Expand Up @@ -1680,9 +1680,9 @@ func PrecomputeTranscriptBlobs(ctx context.Context, repo *git.Repository, transc
}, nil
}

// ensureSessionsBranch ensures the entire/checkpoints/v1 branch exists.
// ensureSessionsBranch ensures the v1 metadata ref exists.
func (s *GitStore) ensureSessionsBranch(ctx context.Context) error {
refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName)
refName := MetadataRef(ctx)
_, err := s.repo.Reference(refName, true)
if err == nil {
return nil // Branch exists
Expand Down Expand Up @@ -1729,24 +1729,24 @@ func (s *GitStore) maybeMergeVercelConfig(ctx context.Context, rootTreeHash plum
// If a blob fetcher is configured on the store, File() calls on the returned
// tree will automatically fetch missing blobs from the remote.
func (s *GitStore) getFetchingTree(ctx context.Context) (*FetchingTree, error) {
tree, err := s.getSessionsBranchTree()
tree, err := s.getSessionsBranchTree(ctx)
if err != nil {
return nil, err
}
return NewFetchingTree(ctx, tree, s.repo.Storer, s.blobFetcher), nil
}

// getSessionsBranchTree returns the tree object for the entire/checkpoints/v1 branch.
// Falls back to origin/entire/checkpoints/v1 if the local branch doesn't exist.
func (s *GitStore) getSessionsBranchTree() (*object.Tree, error) {
refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName)
// getSessionsBranchTree returns the tree object for the v1 metadata ref.
// Falls back to the remote-tracking ref if the local ref doesn't exist.
func (s *GitStore) getSessionsBranchTree(ctx context.Context) (*object.Tree, error) {
refName := MetadataRef(ctx)
ref, err := s.repo.Reference(refName, true)
if err != nil {
// Local branch doesn't exist, try remote-tracking branch
remoteRefName := plumbing.NewRemoteReferenceName("origin", paths.MetadataBranchName)
// Local ref doesn't exist, try remote-tracking
remoteRefName := MetadataTrackingRef(ctx)
ref, err = s.repo.Reference(remoteRefName, true)
if err != nil {
return nil, fmt.Errorf("sessions branch not found: %w", err)
return nil, fmt.Errorf("metadata ref %q not found: %w", RefDisplayName(refName), err)
}
}

Expand Down Expand Up @@ -2152,7 +2152,7 @@ type Author struct {
// Finds the commit whose subject matches "Checkpoint: <id>" and returns its author.
// Returns empty Author if the checkpoint is not found or the sessions branch doesn't exist.
func (s *GitStore) GetCheckpointAuthor(ctx context.Context, checkpointID id.CheckpointID) (Author, error) {
return getCheckpointAuthorFromRef(ctx, s.repo, plumbing.NewBranchReferenceName(paths.MetadataBranchName), checkpointID)
return getCheckpointAuthorFromRef(ctx, s.repo, MetadataRef(ctx), checkpointID)
}

func getCheckpointAuthorFromRef(ctx context.Context, repo *git.Repository, refName plumbing.ReferenceName, checkpointID id.CheckpointID) (Author, error) {
Expand Down
Loading
Loading