Skip to content
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fbc8394
Add a migration script
computermode May 22, 2026
1ea7f14
Add v2 checkpoint migration apply mode
computermode May 22, 2026
5ef0e37
Address v2 migration script review comments
computermode May 26, 2026
09dd22e
Write migrated checkpoints as dated v1 commits
computermode May 26, 2026
1cb2ec4
Qualify checkpoint push refspec
computermode May 26, 2026
acbd558
Repair split checkpoint migration rewrites
computermode May 26, 2026
49dede5
Record associated commits in v2 migration
computermode May 26, 2026
f6fa289
Merge branch 'main' of github.com:entireio/cli into tmp-migrate-v2-sc…
pfleidi May 26, 2026
d54434b
Preserve checkpoint commit timestamps
pfleidi May 26, 2026
f513458
Add v2 checkpoint migration command skeleton
pfleidi May 26, 2026
399c909
Migrate v2 checkpoint data through Go stores
pfleidi May 26, 2026
3a20ce9
Expand v2 checkpoint migration tests
pfleidi May 26, 2026
10e1a5b
Merge branch 'main' into tmp-migrate-v2-script
pfleidi May 26, 2026
9df4393
Merge branch 'tmp-migrate-v2-script' of github.com:entireio/cli into …
pfleidi May 26, 2026
81cddb9
Delegate temporary commit creation
pfleidi May 26, 2026
e68ee79
Limit v2 migration discovery to since histories
pfleidi May 26, 2026
f49344e
Scope migrated review status to restored sessions
pfleidi May 27, 2026
6641537
Address v2 migration feedback
pfleidi May 27, 2026
23dc98d
Merge origin/main into tmp-migrate-v2-script-go
pfleidi May 27, 2026
f5ef3ca
Simplify migrate-v2-checkpoints and checkpoint test
pfleidi May 27, 2026
9be5839
Improve v2 checkpoint migration reporting
pfleidi May 27, 2026
ae19515
Address review feedback
pfleidi May 27, 2026
6883a32
Discover v2 orphan checkpoints
pfleidi May 27, 2026
5a3760e
Update validation instructions
pfleidi May 27, 2026
2f140a3
Handle sparse v1 checkpoint sessions
pfleidi May 27, 2026
86f6dbb
Update validation instructions
pfleidi May 27, 2026
15b3e67
Fetch v2 refs before checkpoint migration
pfleidi May 27, 2026
773c304
Preserve v2 checkpoint authors during migration
pfleidi May 28, 2026
3d9c243
Update migration validation runbook
pfleidi May 28, 2026
224fc97
Merge branch 'main' of github.com:entireio/cli into tmp-migrate-v2-sc…
pfleidi May 28, 2026
f1e4ad8
Fetch v1 checkpoints before migration
pfleidi May 28, 2026
620378f
Update migration validation runbook
pfleidi May 28, 2026
e732e93
Merge branch 'main' of entire://aws-us-east-2.entire.io/gh/entireio/c…
pfleidi May 28, 2026
125b214
Disable signing for migrated checkpoint commits
pfleidi May 28, 2026
118296f
Document unsigned migration commits
pfleidi May 28, 2026
0b2a750
Ignore v2 merge commits for session authors
pfleidi May 28, 2026
6850571
Fix migration data preservation gaps
pfleidi May 29, 2026
092de10
Name eligible v2 session struct
pfleidi May 29, 2026
be2f7e8
Fix v2 session author lookup
pfleidi May 29, 2026
8616ee2
Disable signing for migration tool runs
pfleidi May 29, 2026
d6e4e40
Index v2 session authors during migration
pfleidi May 29, 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
4 changes: 4 additions & 0 deletions cmd/entire/cli/checkpoint/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ type WriteCommittedOptions struct {
// the original v1 checkpoint time in v2 metadata.
CreatedAt time.Time

// CommitTime is the optional git author/committer timestamp for the
// metadata-branch commit. When zero, writers use the current time.
CommitTime time.Time

// Strategy is the name of the strategy that created this checkpoint
Strategy string

Expand Down
13 changes: 10 additions & 3 deletions cmd/entire/cli/checkpoint/committed.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,11 @@ func (s *GitStore) WriteCommitted(ctx context.Context, opts WriteCommittedOption
}

commitMsg := s.buildCommitMessage(opts, taskMetadataPath)
newCommitHash, err := s.createCommit(ctx, newTreeHash, parentHash, commitMsg, opts.AuthorName, opts.AuthorEmail)
commitTime := opts.CommitTime
if commitTime.IsZero() {
commitTime = time.Now()
}
newCommitHash, err := s.createCommitAt(ctx, newTreeHash, parentHash, commitMsg, opts.AuthorName, opts.AuthorEmail, commitTime)
if err != nil {
return err
}
Expand Down Expand Up @@ -1932,11 +1936,14 @@ func GetGitAuthorFromRepo(repo *git.Repository) (name, email string) {
// CreateCommit creates a git commit object with the given tree, parent, message, and author.
// If parentHash is ZeroHash, the commit is created without a parent (orphan commit).
func CreateCommit(ctx context.Context, repo *git.Repository, treeHash, parentHash plumbing.Hash, message, authorName, authorEmail string) (plumbing.Hash, error) {
now := time.Now()
return createCommitObject(ctx, repo, treeHash, parentHash, message, authorName, authorEmail, time.Now())
}

func createCommitObject(ctx context.Context, repo *git.Repository, treeHash, parentHash plumbing.Hash, message, authorName, authorEmail string, commitTime time.Time) (plumbing.Hash, error) {
sig := object.Signature{
Name: authorName,
Email: authorEmail,
When: now,
When: commitTime,
}

commit := &object.Commit{
Expand Down
133 changes: 133 additions & 0 deletions cmd/entire/cli/checkpoint/committed_commit_time_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package checkpoint

import (
"context"
"testing"
"time"

"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/testutil"
"github.com/entireio/cli/redact"

"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/stretchr/testify/require"
)

const (
commitTimeStrategy = "manual-commit"
commitTimeTestAuthor = "Test"
commitTimeTestEmail = "test@example.com"
)

func TestWriteCommitted_CommitTime(t *testing.T) {
t.Parallel()

repo, store := setupCommittedCommitTimeRepo(t)
commitTime := time.Date(2024, 3, 2, 1, 2, 3, 0, time.UTC)

err := store.WriteCommitted(context.Background(), WriteCommittedOptions{
CheckpointID: id.MustCheckpointID("a1b2c3d4e5f6"),
SessionID: "session-commit-time",
CreatedAt: time.Date(2024, 3, 1, 1, 2, 3, 0, time.UTC),
CommitTime: commitTime,
Strategy: commitTimeStrategy,
Transcript: redact.AlreadyRedacted([]byte("transcript line\n")),
AuthorName: "Migration",
AuthorEmail: "migration@example.com",
})
require.NoError(t, err)

commit := metadataHeadCommit(t, repo)
require.True(t, commit.Author.When.Equal(commitTime), "author time = %s, want %s", commit.Author.When, commitTime)
require.True(t, commit.Committer.When.Equal(commitTime), "committer time = %s, want %s", commit.Committer.When, commitTime)
}

func TestWriteCommitted_ZeroCommitTimeUsesCurrentTime(t *testing.T) {
t.Parallel()

repo, store := setupCommittedCommitTimeRepo(t)
createdAt := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
before := time.Now().Add(-time.Second)

err := store.WriteCommitted(context.Background(), WriteCommittedOptions{
CheckpointID: id.MustCheckpointID("b2c3d4e5f6a1"),
SessionID: "session-current-time",
CreatedAt: createdAt,
Strategy: commitTimeStrategy,
Transcript: redact.AlreadyRedacted([]byte("transcript line\n")),
AuthorName: commitTimeTestAuthor,
AuthorEmail: commitTimeTestEmail,
})
require.NoError(t, err)
after := time.Now().Add(time.Second)

commit := metadataHeadCommit(t, repo)
require.False(t, commit.Author.When.Equal(createdAt), "zero CommitTime should not reuse CreatedAt as the commit timestamp")
require.False(t, commit.Author.When.Before(before), "author time = %s, want no earlier than %s", commit.Author.When, before)
require.False(t, commit.Author.When.After(after), "author time = %s, want no later than %s", commit.Author.When, after)
require.True(t, commit.Committer.When.Equal(commit.Author.When), "committer time = %s, want author time %s", commit.Committer.When, commit.Author.When)
}

func TestV2WriteCommitted_CommitTime(t *testing.T) {
t.Parallel()

repo, _ := setupCommittedCommitTimeRepo(t)
store := NewV2GitStore(repo)
commitTime := time.Date(2024, 4, 5, 6, 7, 8, 0, time.UTC)

err := store.WriteCommitted(context.Background(), WriteCommittedOptions{
CheckpointID: id.MustCheckpointID("c3d4e5f6a1b2"),
SessionID: "session-v2-commit-time",
CreatedAt: time.Date(2024, 4, 1, 2, 3, 4, 0, time.UTC),
CommitTime: commitTime,
Strategy: commitTimeStrategy,
Transcript: redact.AlreadyRedacted([]byte("transcript line\n")),
AuthorName: "Migration",
AuthorEmail: "migration@example.com",
})
require.NoError(t, err)

mainCommit := refHeadCommit(t, repo, plumbing.ReferenceName(paths.V2MainRefName))
fullCommit := refHeadCommit(t, repo, plumbing.ReferenceName(paths.V2FullCurrentRefName))
for _, commit := range []*object.Commit{mainCommit, fullCommit} {
require.True(t, commit.Author.When.Equal(commitTime), "author time = %s, want %s", commit.Author.When, commitTime)
require.True(t, commit.Committer.When.Equal(commitTime), "committer time = %s, want %s", commit.Committer.When, commitTime)
}
}

func setupCommittedCommitTimeRepo(t *testing.T) (*git.Repository, *GitStore) {
t.Helper()

dir := t.TempDir()
testutil.InitRepo(t, dir)

repo, err := git.PlainOpen(dir)
require.NoError(t, err)

testutil.WriteFile(t, dir, "README.md", "# Test\n")
testutil.GitAdd(t, dir, "README.md")
testutil.GitCommit(t, dir, "initial commit")

return repo, NewGitStore(repo)
}

func metadataHeadCommit(t *testing.T, repo *git.Repository) *object.Commit {
t.Helper()

return refHeadCommit(t, repo, plumbing.NewBranchReferenceName(paths.MetadataBranchName))
}

func refHeadCommit(t *testing.T, repo *git.Repository, refName plumbing.ReferenceName) *object.Commit {
t.Helper()

ref, err := repo.Reference(refName, true)
require.NoError(t, err)

commit, err := repo.CommitObject(ref.Hash())
require.NoError(t, err)

return commit
}
4 changes: 4 additions & 0 deletions cmd/entire/cli/checkpoint/temporary.go
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,10 @@ func (s *GitStore) createCommit(ctx context.Context, treeHash, parentHash plumbi
return CreateCommit(ctx, s.repo, treeHash, parentHash, message, authorName, authorEmail)
}

func (s *GitStore) createCommitAt(ctx context.Context, treeHash, parentHash plumbing.Hash, message, authorName, authorEmail string, commitTime time.Time) (plumbing.Hash, error) {
return createCommitObject(ctx, s.repo, treeHash, parentHash, message, authorName, authorEmail, commitTime)
}

// Helper functions extracted from strategy/common.go
// These are exported for use by strategy package (push_common.go, session_test.go)

Expand Down
50 changes: 31 additions & 19 deletions cmd/entire/cli/checkpoint/v2_committed.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"strconv"
"strings"
"time"

"github.com/entireio/cli/cmd/entire/cli/agent"
"github.com/entireio/cli/cmd/entire/cli/agent/types"
Expand Down Expand Up @@ -141,7 +142,14 @@ func (s *V2GitStore) WriteCommittedMainBatch(ctx context.Context, batch []WriteC
authorEmail = fallbackEmail
}
}
return s.updateRef(ctx, refName, rootTreeHash, parentHash, commitMsg, authorName, authorEmail)
return s.updateRefAt(ctx, refName, rootTreeHash, parentHash, commitMsg, authorName, authorEmail, commitTimeForWrite(last))
}

func commitTimeForWrite(opts WriteCommittedOptions) time.Time {
if opts.CommitTime.IsZero() {
return time.Now()
}
return opts.CommitTime
}

func (s *V2GitStore) existingMainCheckpointIDs(ctx context.Context, rootTreeHash plumbing.Hash) (map[id.CheckpointID]struct{}, error) {
Expand Down Expand Up @@ -353,7 +361,7 @@ func (s *V2GitStore) UpdateCommitted(ctx context.Context, opts UpdateCommittedOp
}

// fullSessionArtifacts describes where a checkpoint session's raw transcript
// artifacts live on the v2 /full/current ref.
// artifacts live on a v2 /full ref.
type fullSessionArtifacts struct {
RefName plumbing.ReferenceName
Found bool
Expand All @@ -362,7 +370,7 @@ type fullSessionArtifacts struct {
}

// HasFullSessionArtifacts reports whether the raw transcript and content hash
// for a checkpoint session exist in the local v2 /full/current ref.
// for a checkpoint session exist in the local v2 /full refs.
func (s *V2GitStore) HasFullSessionArtifacts(checkpointID id.CheckpointID, sessionIndex int) (bool, error) {
artifacts, err := s.findFullSessionArtifacts(checkpointID, sessionIndex)
if err != nil {
Expand Down Expand Up @@ -401,13 +409,13 @@ func (s *V2GitStore) findFullSessionArtifacts(checkpointID id.CheckpointID, sess
return fullSessionArtifacts{}, nil
}

// FullSessionArtifactsIndex answers "does this session have complete /full/current
// FullSessionArtifactsIndex answers "does this session have complete /full
// artifacts?" with an O(1) map lookup. Build it once via
// BuildFullSessionArtifactsIndex.
type FullSessionArtifactsIndex map[string]struct{}

// Has reports whether the given session has a complete pair of
// raw_transcript and raw_transcript_hash.txt entries in /full/current.
// raw_transcript and raw_transcript_hash.txt entries in a /full ref.
func (idx FullSessionArtifactsIndex) Has(checkpointID id.CheckpointID, sessionIndex int) bool {
if idx == nil {
return false
Expand All @@ -420,7 +428,7 @@ func fullArtifactsIndexKey(checkpointID id.CheckpointID, sessionIndex int) strin
return string(checkpointID) + "/" + strconv.Itoa(sessionIndex)
}

// BuildFullSessionArtifactsIndex walks the /full/current ref's tree once and
// BuildFullSessionArtifactsIndex walks the local /full refs once and
// records sessions whose subtree contains both raw_transcript[/.NNN] and
// raw_transcript_hash.txt. Amortizes per-session HasFullSessionArtifacts
// calls across the rest of the run.
Expand Down Expand Up @@ -511,7 +519,17 @@ func sessionHasCompleteFullArtifacts(entries []object.TreeEntry) bool {
}

func (s *V2GitStore) fullRefSearchOrder() ([]plumbing.ReferenceName, error) {
return []plumbing.ReferenceName{plumbing.ReferenceName(paths.V2FullCurrentRefName)}, nil
archived, err := s.listArchivedFullRefs()
if err != nil {
return nil, err
}

refNames := make([]plumbing.ReferenceName, 0, len(archived)+1)
refNames = append(refNames, plumbing.ReferenceName(paths.V2FullCurrentRefName))
for i := len(archived) - 1; i >= 0; i-- {
refNames = append(refNames, archived[i])
}
return refNames, nil
}

func (s *V2GitStore) inspectFullSessionArtifacts(refName plumbing.ReferenceName, checkpointID id.CheckpointID, sessionIndex int) (fullSessionArtifacts, error) {
Expand Down Expand Up @@ -668,18 +686,12 @@ func (s *V2GitStore) updateCommittedMain(ctx context.Context, opts UpdateCommitt
func (s *V2GitStore) updateCommittedFullTranscript(ctx context.Context, opts UpdateCommittedOptions, sessionIndex int) error {
refName := plumbing.ReferenceName(paths.V2FullCurrentRefName)

existing, findErr := s.findFullSessionArtifacts(opts.CheckpointID, sessionIndex)
if findErr != nil {
return findErr
}
if existing.Found {
refName = existing.RefName
if _, err := s.inspectFullSessionArtifacts(refName, opts.CheckpointID, sessionIndex); err != nil {
return err
}

if refName == plumbing.ReferenceName(paths.V2FullCurrentRefName) {
if err := s.ensureRef(ctx, refName); err != nil {
return fmt.Errorf("failed to ensure /full/current ref: %w", err)
}
if err := s.ensureRef(ctx, refName); err != nil {
return fmt.Errorf("failed to ensure /full/current ref: %w", err)
}

parentHash, rootTreeHash, err := s.GetRefState(refName)
Expand Down Expand Up @@ -802,7 +814,7 @@ func (s *V2GitStore) writeCommittedMain(ctx context.Context, opts WriteCommitted
}

commitMsg := fmt.Sprintf("Checkpoint: %s\n", opts.CheckpointID)
if err := s.updateRef(ctx, refName, newTreeHash, parentHash, commitMsg, opts.AuthorName, opts.AuthorEmail); err != nil {
if err := s.updateRefAt(ctx, refName, newTreeHash, parentHash, commitMsg, opts.AuthorName, opts.AuthorEmail, commitTimeForWrite(opts)); err != nil {
return 0, err
}
return sessionIndex, nil
Expand Down Expand Up @@ -1053,7 +1065,7 @@ func (s *V2GitStore) writeCommittedFullTranscript(ctx context.Context, opts Writ
}

commitMsg := fmt.Sprintf("Checkpoint: %s\n", opts.CheckpointID)
if err := s.updateRef(ctx, refName, newTreeHash, parentHash, commitMsg, opts.AuthorName, opts.AuthorEmail); err != nil {
if err := s.updateRefAt(ctx, refName, newTreeHash, parentHash, commitMsg, opts.AuthorName, opts.AuthorEmail, commitTimeForWrite(opts)); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/entire/cli/checkpoint/v2_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func (s *V2GitStore) ReadSessionPrompts(ctx context.Context, checkpointID id.Che
}

// ReadSessionContent reads a session's metadata and prompts from the v2 /main ref,
// and the raw transcript (raw_transcript) from /full/current.
// and the raw transcript (raw_transcript) from local or remote /full refs.
// This is the v2 equivalent of GitStore.ReadSessionContent — it reads the raw agent
// transcript, not the compact transcript.jsonl. Used by resume and RestoreLogsOnly.
// Returns ErrNoTranscript if the session exists but no raw transcript is available.
Expand Down
7 changes: 6 additions & 1 deletion cmd/entire/cli/checkpoint/v2_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log/slog"
"os/exec"
"strings"
"time"

"github.com/entireio/cli/cmd/entire/cli/logging"

Expand Down Expand Up @@ -130,7 +131,11 @@ func commitTreeHashViaCLI(ctx context.Context, commitHash plumbing.Hash) (plumbi

// updateRef creates a new commit on a ref with the given tree, updating the ref to point to it.
func (s *V2GitStore) updateRef(ctx context.Context, refName plumbing.ReferenceName, treeHash, parentHash plumbing.Hash, message, authorName, authorEmail string) error {
commitHash, err := CreateCommit(ctx, s.repo, treeHash, parentHash, message, authorName, authorEmail)
return s.updateRefAt(ctx, refName, treeHash, parentHash, message, authorName, authorEmail, time.Now())
}

func (s *V2GitStore) updateRefAt(ctx context.Context, refName plumbing.ReferenceName, treeHash, parentHash plumbing.Hash, message, authorName, authorEmail string, commitTime time.Time) error {
commitHash, err := createCommitObject(ctx, s.repo, treeHash, parentHash, message, authorName, authorEmail, commitTime)
if err != nil {
return fmt.Errorf("failed to create commit: %w", err)
}
Expand Down
Loading
Loading