Skip to content
Draft
Show file tree
Hide file tree
Changes from 20 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 @@ -118,7 +118,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 @@ -1933,11 +1937,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
100 changes: 100 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,100 @@
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 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()

ref, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), 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
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 @@ -266,7 +266,7 @@ func (s *V2GitStore) GetCheckpointAuthor(ctx context.Context, checkpointID id.Ch
}

// 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/strategy/push_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func tryPushSessionsCommon(ctx context.Context, remoteName, branchName string) (
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()

result, err := remote.Push(ctx, remoteName, branchName)
result, err := remote.Push(ctx, remoteName, branchPushRefSpec(branchName))
outputStr := result.Output
if err != nil {
return pushResult{}, classifyPushFailure(ctx, outputStr, err)
Expand All @@ -221,6 +221,11 @@ func tryPushSessionsCommon(ctx context.Context, remoteName, branchName string) (
return parsePushResult(outputStr), nil
}

func branchPushRefSpec(branchName string) string {
branchRef := plumbing.NewBranchReferenceName(branchName).String()
return branchRef + ":" + branchRef
}

// protectedRefError means the remote is blocking writes to the ref itself.
type protectedRefError struct {
output string
Expand Down
43 changes: 43 additions & 0 deletions cmd/entire/cli/strategy/push_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,49 @@ func TestDoPushBranch_NewContent_SaysDone(t *testing.T) {
assert.NotContains(t, output, "already up-to-date", "should not say 'already up-to-date' when content was pushed")
}

// TestDoPushBranch_AmbiguousLocalRefs verifies that checkpoint pushes qualify
// the branch refspec. A stale refs/entire/checkpoints/v1 ref can otherwise make
// git reject the unqualified source ref as ambiguous.
//
// Not parallel: uses t.Chdir() and os.Stderr redirection.
func TestDoPushBranch_AmbiguousLocalRefs(t *testing.T) {
workDir := setupRepoWithCheckpointBranch(t)

headCmd := exec.CommandContext(context.Background(), "git", "rev-parse", "HEAD")
headCmd.Dir = workDir
headCmd.Env = testutil.GitIsolatedEnv()
headOut, err := headCmd.Output()
require.NoError(t, err)

staleRefCmd := exec.CommandContext(
context.Background(),
"git",
"update-ref",
"refs/entire/checkpoints/v1",
strings.TrimSpace(string(headOut)),
)
staleRefCmd.Dir = workDir
staleRefCmd.Env = testutil.GitIsolatedEnv()
out, err := staleRefCmd.CombinedOutput()
require.NoError(t, err, "stale ref setup failed: %s", out)

bareDir := t.TempDir()
initCmd := exec.CommandContext(context.Background(), "git", "init", "--bare")
initCmd.Dir = bareDir
initCmd.Env = testutil.GitIsolatedEnv()
out, err = initCmd.CombinedOutput()
require.NoError(t, err, "git init --bare failed: %s", out)

t.Chdir(workDir)

restore := captureStderr(t)
err = doPushBranch(context.Background(), bareDir, paths.MetadataBranchName)
output := restore()

require.NoError(t, err)
assert.Contains(t, output, " done", "should push despite ambiguous local refs")
}

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

Expand Down
Loading
Loading