diff --git a/cmd/entire/cli/checkpoint/committed.go b/cmd/entire/cli/checkpoint/committed.go index 69a2f767ee..766ad45f4d 100644 --- a/cmd/entire/cli/checkpoint/committed.go +++ b/cmd/entire/cli/checkpoint/committed.go @@ -1826,6 +1826,15 @@ func GetGitAuthorFromRepo(repo *git.Repository) (name, email string) { return name, email } +// CreateMigrationCommit creates a commit on behalf of Entire's migration or +// repair tooling. Author and committer are the real git user (so commit +// signing keeps working); the standard migration trailer block is appended to +// subject to carry the tool-produced provenance signal. +func CreateMigrationCommit(ctx context.Context, repo *git.Repository, treeHash, parentHash plumbing.Hash, subject string) (plumbing.Hash, error) { + authorName, authorEmail := GetGitAuthorFromRepo(repo) + return CreateCommit(ctx, repo, treeHash, parentHash, trailers.FormatMigration(subject), authorName, authorEmail) +} + // 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) { diff --git a/cmd/entire/cli/checkpoint/v2_committed.go b/cmd/entire/cli/checkpoint/v2_committed.go index 35edf2273a..e699ff9d0c 100644 --- a/cmd/entire/cli/checkpoint/v2_committed.go +++ b/cmd/entire/cli/checkpoint/v2_committed.go @@ -17,6 +17,7 @@ import ( "github.com/entireio/cli/cmd/entire/cli/jsonutil" "github.com/entireio/cli/cmd/entire/cli/logging" "github.com/entireio/cli/cmd/entire/cli/paths" + "github.com/entireio/cli/cmd/entire/cli/trailers" "github.com/entireio/cli/cmd/entire/cli/validation" "github.com/entireio/cli/cmd/entire/cli/versioninfo" "github.com/entireio/cli/redact" @@ -128,8 +129,10 @@ func (s *V2GitStore) WriteCommittedMainBatch(ctx context.Context, batch []WriteC } } - // One commit, one ref update for the entire batch. - commitMsg := fmt.Sprintf("Migrate batch: %d checkpoint(s), %d session(s)\n", len(groupOrder), len(batch)) + // One commit, one ref update for the entire batch. WriteCommittedMainBatch + // is migration-only (see callers), so the message carries the standard + // migration trailer block alongside every other tool-produced commit. + commitMsg := trailers.FormatMigration(fmt.Sprintf("Migrate batch: %d checkpoint(s), %d session(s)", len(groupOrder), len(batch))) last := batch[len(batch)-1] authorName, authorEmail := last.AuthorName, last.AuthorEmail if authorName == "" || authorEmail == "" { diff --git a/cmd/entire/cli/migrate.go b/cmd/entire/cli/migrate.go index 38ce8b19fc..37163789df 100644 --- a/cmd/entire/cli/migrate.go +++ b/cmd/entire/cli/migrate.go @@ -383,9 +383,7 @@ var ( ) const ( - migrateRemoteName = "origin" - migrateAuthorName = "Entire Migration" - migrateAuthorEmail = "migration@entire.dev" + migrateRemoteName = "origin" ) var migrateMaxCheckpointsPerGeneration = checkpoint.DefaultMaxCheckpointsPerGeneration @@ -881,9 +879,8 @@ func writeMigratedFinalFullCurrent(ctx context.Context, repo *git.Repository, v2 return fmt.Errorf("build migrated full/current tree: %w", err) } - commitHash, err := checkpoint.CreateCommit(ctx, repo, treeHash, parentHash, - "Write migrated partial generation\n", - migrateAuthorName, migrateAuthorEmail) + commitHash, err := checkpoint.CreateMigrationCommit(ctx, repo, treeHash, parentHash, + "Write migrated partial generation") if err != nil { return fmt.Errorf("create migrated full/current commit: %w", err) } @@ -975,9 +972,8 @@ func writeMigratedFullGeneration(ctx context.Context, repo *git.Repository, v2St return plumbing.ZeroHash, fmt.Errorf("add generation metadata: %w", err) } - commitHash, err := checkpoint.CreateCommit(ctx, repo, treeHash, plumbing.ZeroHash, - fmt.Sprintf("Archive migrated generation: %s\n", refName), - migrateAuthorName, migrateAuthorEmail) + commitHash, err := checkpoint.CreateMigrationCommit(ctx, repo, treeHash, plumbing.ZeroHash, + fmt.Sprintf("Archive migrated generation: %s", refName)) if err != nil { return plumbing.ZeroHash, fmt.Errorf("create migrated generation commit: %w", err) } @@ -1144,9 +1140,8 @@ func ensureEmptyV2FullCurrent(ctx context.Context, repo *git.Repository) error { return fmt.Errorf("build empty v2 full/current tree: %w", err) } - commitHash, err := checkpoint.CreateCommit(ctx, repo, emptyTreeHash, plumbing.ZeroHash, - "Start generation\n", - migrateAuthorName, migrateAuthorEmail) + commitHash, err := checkpoint.CreateMigrationCommit(ctx, repo, emptyTreeHash, plumbing.ZeroHash, + "Start generation") if err != nil { return fmt.Errorf("create empty v2 full/current commit: %w", err) } @@ -1228,9 +1223,8 @@ func pruneV2CheckpointRef(ctx context.Context, repo *git.Repository, v2Store *ch return nil } - commitHash, err := checkpoint.CreateCommit(ctx, repo, newRoot, parentHash, - fmt.Sprintf("Reset checkpoint before force migration: %s\n", cpID), - migrateAuthorName, migrateAuthorEmail) + commitHash, err := checkpoint.CreateMigrationCommit(ctx, repo, newRoot, parentHash, + fmt.Sprintf("Reset checkpoint before force migration: %s", cpID)) if err != nil { return fmt.Errorf("failed to create v2 prune commit for %s: %w", refName, err) } @@ -1284,9 +1278,8 @@ func pruneV2ArchivedCheckpointRef(ctx context.Context, repo *git.Repository, v2S return fmt.Errorf("failed to recompute generation metadata for %s: %w", refName, err) } - commitHash, err := checkpoint.CreateCommit(ctx, repo, newRoot, parentHash, - fmt.Sprintf("Reset checkpoint before force migration: %s\n", cpID), - migrateAuthorName, migrateAuthorEmail) + commitHash, err := checkpoint.CreateMigrationCommit(ctx, repo, newRoot, parentHash, + fmt.Sprintf("Reset checkpoint before force migration: %s", cpID)) if err != nil { return fmt.Errorf("failed to create v2 prune commit for %s: %w", refName, err) } @@ -1391,8 +1384,8 @@ func buildMigrateWriteOpts(content *checkpoint.SessionContent, info checkpoint.C TranscriptIdentifierAtStart: m.TranscriptIdentifierAtStart, IsTask: m.IsTask, ToolUseID: m.ToolUseID, - AuthorName: migrateAuthorName, - AuthorEmail: migrateAuthorEmail, + // Author left zero so WriteCommittedMainBatch falls back to the real + // git user; provenance lives in the migration trailer block. } } diff --git a/cmd/entire/cli/strategy/generation_repair.go b/cmd/entire/cli/strategy/generation_repair.go index 3117084223..349875f7ff 100644 --- a/cmd/entire/cli/strategy/generation_repair.go +++ b/cmd/entire/cli/strategy/generation_repair.go @@ -12,11 +12,6 @@ import ( "github.com/go-git/go-git/v6/plumbing" ) -const ( - repairAuthorName = "Entire Migration" - repairAuthorEmail = "migration@entire.dev" -) - // RepairV2GenerationMetadataResult describes archived v2 generation metadata // repair work performed by RepairV2GenerationMetadata. type RepairV2GenerationMetadataResult struct { @@ -132,9 +127,8 @@ func repairOneV2GenerationMetadata( return false, nil } - newCommitHash, commitErr := checkpoint.CreateCommit(ctx, repo, newTreeHash, oldCommitHash, - fmt.Sprintf("Repair generation metadata: %s\n", candidate.Name), - repairAuthorName, repairAuthorEmail) + newCommitHash, commitErr := checkpoint.CreateMigrationCommit(ctx, repo, newTreeHash, oldCommitHash, + "Repair generation metadata: "+candidate.Name) if commitErr != nil { return false, fmt.Errorf("failed to create repair commit: %w", commitErr) } diff --git a/cmd/entire/cli/trailers/trailers.go b/cmd/entire/cli/trailers/trailers.go index a3c1b44036..ea6b5f590e 100644 --- a/cmd/entire/cli/trailers/trailers.go +++ b/cmd/entire/cli/trailers/trailers.go @@ -48,6 +48,17 @@ const ( // AgentTrailerKey identifies the agent that created a checkpoint. // Format: human-readable agent name e.g. "Claude Code", "Cursor" AgentTrailerKey = "Entire-Agent" + + // MigrationTrailerKey marks a commit as produced by Entire's migration or + // repair tooling. The author/committer stay the real git user so commit + // signing keeps working; this trailer carries the provenance signal as a + // greppable boolean. + MigrationTrailerKey = "Entire-Migration" + + // MigrationCoAuthor is the identity used in the Co-Authored-By trailer on + // migration/repair commits. GitHub renders it as a co-author chip, making + // the tool-produced nature visible without affecting signing attribution. + MigrationCoAuthor = "Entire Migration " ) // Pre-compiled regexes for trailer parsing. @@ -246,6 +257,13 @@ func FormatShadowTaskCommit(message, taskMetadataDir, sessionID string) string { return sb.String() } +// FormatMigration creates a commit message with the standard migration +// trailer block: a Co-Authored-By line crediting the migration identity and +// the Entire-Migration provenance flag. +func FormatMigration(message string) string { + return fmt.Sprintf("%s\n\nCo-Authored-By: %s\n%s: true\n", message, MigrationCoAuthor, MigrationTrailerKey) +} + // FormatCheckpoint creates a commit message with a checkpoint trailer. // This links user commits to their checkpoint metadata on entire/checkpoints/v1 branch. func FormatCheckpoint(message string, cpID checkpointID.CheckpointID) string { diff --git a/cmd/entire/cli/trailers/trailers_test.go b/cmd/entire/cli/trailers/trailers_test.go index 2fd0e89613..be8752dd46 100644 --- a/cmd/entire/cli/trailers/trailers_test.go +++ b/cmd/entire/cli/trailers/trailers_test.go @@ -180,6 +180,19 @@ func TestParseTaskMetadata(t *testing.T) { } } +func TestFormatMigration(t *testing.T) { + message := "Repair generation metadata: 0000000000007" + + expected := "Repair generation metadata: 0000000000007\n\n" + + "Co-Authored-By: Entire Migration \n" + + "Entire-Migration: true\n" + got := FormatMigration(message) + + if got != expected { + t.Errorf("FormatMigration() = %q, want %q", got, expected) + } +} + func TestParseBaseCommit(t *testing.T) { tests := []struct { name string