From bb9f3b2b3eac8c95bc3d09e12e89481f98dddb3c Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Tue, 6 Jan 2026 19:04:48 +0100 Subject: [PATCH 1/7] include commit info into session list Entire-Checkpoint: 24672feafdde --- CLAUDE.md | 13 +++ cmd/entire/cli/strategy/auto_commit_test.go | 13 ++- cmd/entire/cli/strategy/session.go | 93 ++++++++++++++++++++- 3 files changed, 111 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7c2fed8185..2affcf502b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -196,6 +196,19 @@ Legacy names `shadow` and `dual` are only recognized when reading settings or ch **Note:** Both strategies keep active branch history **clean**. Manual-commit strategy never creates commits on the active branch. Auto-commit strategy creates commits with only the `Entire-Checkpoint` trailer. All detailed metadata is stored on the `entire/sessions` orphan branch or shadow branches. +#### Why Checkpoint ID is the Stable Link (Not Commit Hash) + +**Important architectural constraint:** Never store commit hashes as primary links to session data. Commit hashes are mutable - they change during `git commit --amend`, `git rebase`, and `git cherry-pick`. + +The `Entire-Checkpoint: ` trailer is the **stable link** because: +1. It's embedded in the commit message content +2. It travels with the commit through history-rewriting operations +3. The checkpoint ID (12-hex-char) is immutable once generated + +**To find a commit for a checkpoint:** Search git history for commits containing the `Entire-Checkpoint: ` trailer, don't store/cache the commit hash. + +**Example:** To display the commit message for a session in `entire session list`, search for commits with matching `Entire-Checkpoint` trailer rather than storing the commit hash in metadata. + #### When Modifying Strategies - All strategies must implement the full `Strategy` interface - Register new strategies in `init()` using `Register()` diff --git a/cmd/entire/cli/strategy/auto_commit_test.go b/cmd/entire/cli/strategy/auto_commit_test.go index 97e14a2f7c..3bcde2cd61 100644 --- a/cmd/entire/cli/strategy/auto_commit_test.go +++ b/cmd/entire/cli/strategy/auto_commit_test.go @@ -670,21 +670,26 @@ func TestAutoCommitStrategy_ListSessions_HasDescription(t *testing.T) { t.Fatalf("failed to write log file: %v", err) } - // Write prompt.txt with description - expectedDescription := "Fix the authentication bug in login.go" + // Write prompt.txt with description (this is the fallback if no commit is found) + promptDescription := "Fix the authentication bug in login.go" promptFile := filepath.Join(metadataDir, paths.PromptFileName) - if err := os.WriteFile(promptFile, []byte(expectedDescription+"\n\nMore details here..."), 0o644); err != nil { + if err := os.WriteFile(promptFile, []byte(promptDescription+"\n\nMore details here..."), 0o644); err != nil { t.Fatalf("failed to write prompt file: %v", err) } + // The expected description is the commit message, not prompt.txt + // Since we now prefer commit messages over prompt.txt + expectedDescription := "Implement user authentication feature" + metadataDirAbs, err := paths.AbsPath(metadataDir) if err != nil { metadataDirAbs = metadataDir } // Save changes - this creates a checkpoint on entire/sessions + // The commit message is what will be shown as the session description ctx := SaveContext{ - CommitMessage: "Test checkpoint", + CommitMessage: expectedDescription, MetadataDir: metadataDir, MetadataDirAbs: metadataDirAbs, NewFiles: []string{"test.go"}, diff --git a/cmd/entire/cli/strategy/session.go b/cmd/entire/cli/strategy/session.go index c44f7dbbd0..5fe3353eb2 100644 --- a/cmd/entire/cli/strategy/session.go +++ b/cmd/entire/cli/strategy/session.go @@ -1,6 +1,7 @@ package strategy import ( + "errors" "fmt" "sort" "strings" @@ -8,6 +9,7 @@ import ( "entire.io/cli/cmd/entire/cli/paths" "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" ) // NoDescription is the default description for sessions without one. @@ -106,8 +108,8 @@ func ListSessions() ([]Session, error) { ToolUseID: cp.ToolUseID, }) } else { - // Get description from the checkpoint tree - description := getDescriptionForCheckpoint(repo, cp.CheckpointID) + // Get description - first try commit message, then fall back to prompt.txt + description := getDescriptionForCheckpoint(repo, cp.CheckpointID, cp.CreatedAt) sessionMap[cp.SessionID] = &Session{ ID: cp.SessionID, @@ -197,8 +199,18 @@ func GetSession(sessionID string) (*Session, error) { return findSessionByID(sessions, sessionID) } -// getDescriptionForCheckpoint reads the description for a checkpoint from the entire/sessions branch. -func getDescriptionForCheckpoint(repo *git.Repository, checkpointID string) string { +// getDescriptionForCheckpoint gets the description for a checkpoint. +// First tries to find the commit message from a commit with the Entire-Checkpoint trailer, +// then falls back to reading prompt.txt from the entire/sessions branch. +// The metadataCreatedAt is used to optimize the commit search - we only look at commits +// from a few minutes before this timestamp up to now (commits can be rebased to be newer). +func getDescriptionForCheckpoint(repo *git.Repository, checkpointID string, metadataCreatedAt time.Time) string { + // First, try to find the commit message + if commitMsg := findCommitMessageByCheckpointID(repo, checkpointID, metadataCreatedAt); commitMsg != "" { + return commitMsg + } + + // Fall back to reading from entire/sessions branch (prompt.txt or context.md) tree, err := GetMetadataBranchTree(repo) if err != nil { return NoDescription @@ -208,6 +220,79 @@ func getDescriptionForCheckpoint(repo *git.Repository, checkpointID string) stri return getSessionDescriptionFromTree(tree, checkpointPath) } +// findCommitMessageByCheckpointID searches for a commit with the given Entire-Checkpoint trailer +// and returns the first line of its commit message. +// The search window is from (metadataCreatedAt - 5 minutes) to now, since: +// - The original commit was created around metadataCreatedAt +// - Rebase/amend can make the commit newer than the metadata timestamp +// - We don't need to look at commits older than a few minutes before the metadata +func findCommitMessageByCheckpointID(repo *git.Repository, checkpointID string, metadataCreatedAt time.Time) string { + // Search window: commits from 5 minutes before metadata timestamp to now + // The commit can't be much older than the metadata, but can be arbitrarily newer (after rebase) + searchLowerBound := metadataCreatedAt.Add(-5 * time.Minute) + + // Get HEAD to start iteration + head, err := repo.Head() + if err != nil { + return "" + } + + // Iterate through commits + iter, err := repo.Log(&git.LogOptions{ + From: head.Hash(), + Order: git.LogOrderCommitterTime, + }) + if err != nil { + return "" + } + defer iter.Close() + + var foundMsg string + maxCommitsToScan := 500 // Safety limit + + //nolint:errcheck,gosec // ForEach error handling via sentinel + iter.ForEach(func(c *object.Commit) error { + maxCommitsToScan-- + if maxCommitsToScan <= 0 { + return errors.New("limit reached") + } + + // Stop if commit is older than our search window + if c.Committer.When.Before(searchLowerBound) { + return errors.New("too old") + } + + // Check if this commit has the matching Entire-Checkpoint trailer + if cpID, found := paths.ParseCheckpointTrailer(c.Message); found && cpID == checkpointID { + // Found the commit - extract first line of message + foundMsg = extractCommitSubject(c.Message) + return errors.New("found") + } + + return nil + }) + + return foundMsg +} + +// extractCommitSubject returns the first line of a commit message, +// excluding any trailing Entire-Checkpoint trailer if it's on the first line. +func extractCommitSubject(message string) string { + lines := strings.SplitN(message, "\n", 2) + if len(lines) == 0 { + return "" + } + + subject := strings.TrimSpace(lines[0]) + + // If the subject line is just the trailer, return empty (let fallback handle it) + if strings.HasPrefix(subject, paths.CheckpointTrailerKey+":") { + return "" + } + + return subject +} + // findSessionByID finds a session by exact ID or prefix match. func findSessionByID(sessions []Session, sessionID string) (*Session, error) { for _, session := range sessions { From dd0fcccabc19cf5b8014eafb9a4811e10656371e Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Tue, 6 Jan 2026 19:44:48 +0100 Subject: [PATCH 2/7] Looking at this now I can see for example: 2026-01-06-cf564e14 1 Entire-Checkpoint: 63590d292e96 --- cmd/entire/cli/checkpoint/checkpoint.go | 4 ++ cmd/entire/cli/checkpoint/committed.go | 1 + cmd/entire/cli/strategy/auto_commit.go | 12 +++++- cmd/entire/cli/strategy/common.go | 15 +++++++ .../strategy/manual_commit_condensation.go | 4 ++ .../cli/strategy/manual_commit_types.go | 1 + cmd/entire/cli/strategy/session.go | 40 +++++++++++++++---- 7 files changed, 68 insertions(+), 9 deletions(-) diff --git a/cmd/entire/cli/checkpoint/checkpoint.go b/cmd/entire/cli/checkpoint/checkpoint.go index d4ffb363b6..0ad9d9190c 100644 --- a/cmd/entire/cli/checkpoint/checkpoint.go +++ b/cmd/entire/cli/checkpoint/checkpoint.go @@ -176,6 +176,9 @@ type WriteCommittedOptions struct { // CheckpointsCount is the number of checkpoints in this session CheckpointsCount int + // Branch is the branch where the code commit lives (used for finding commit message) + Branch string + // EphemeralBranch is the shadow branch name (for manual-commit strategy) EphemeralBranch string @@ -257,6 +260,7 @@ type CommittedMetadata struct { CreatedAt time.Time `json:"created_at"` CheckpointsCount int `json:"checkpoints_count"` FilesTouched []string `json:"files_touched"` + Branch string `json:"branch,omitempty"` // Branch where the code commit lives // Task checkpoint fields (only populated for task checkpoints) IsTask bool `json:"is_task,omitempty"` diff --git a/cmd/entire/cli/checkpoint/committed.go b/cmd/entire/cli/checkpoint/committed.go index 278c762fae..472eb57546 100644 --- a/cmd/entire/cli/checkpoint/committed.go +++ b/cmd/entire/cli/checkpoint/committed.go @@ -293,6 +293,7 @@ func (s *GitStore) writeMetadataJSON(opts WriteCommittedOptions, basePath string CreatedAt: time.Now(), CheckpointsCount: opts.CheckpointsCount, FilesTouched: opts.FilesTouched, + Branch: opts.Branch, IsTask: opts.IsTask, ToolUseID: opts.ToolUseID, } diff --git a/cmd/entire/cli/strategy/auto_commit.go b/cmd/entire/cli/strategy/auto_commit.go index d696071f20..98f1a6258f 100644 --- a/cmd/entire/cli/strategy/auto_commit.go +++ b/cmd/entire/cli/strategy/auto_commit.go @@ -213,7 +213,7 @@ func (s *AutoCommitStrategy) commitCodeToActive(repo *git.Repository, ctx SaveCo // Metadata is stored at sharded path: // // This allows direct lookup from the checkpoint ID trailer on the code commit. // Uses checkpoint.WriteCommitted for git operations. -func (s *AutoCommitStrategy) commitMetadataToMetadataBranch(_ *git.Repository, ctx SaveContext, checkpointID string) (plumbing.Hash, error) { +func (s *AutoCommitStrategy) commitMetadataToMetadataBranch(repo *git.Repository, ctx SaveContext, checkpointID string) (plumbing.Hash, error) { store, err := s.getCheckpointStore() if err != nil { return plumbing.ZeroHash, fmt.Errorf("failed to get checkpoint store: %w", err) @@ -222,12 +222,16 @@ func (s *AutoCommitStrategy) commitMetadataToMetadataBranch(_ *git.Repository, c // Extract session ID from metadata dir sessionID := filepath.Base(ctx.MetadataDir) + // Get current branch name for commit lookup hint + branchName := getCurrentBranchName(repo) + // Write committed checkpoint using the checkpoint store err = store.WriteCommitted(context.Background(), checkpoint.WriteCommittedOptions{ CheckpointID: checkpointID, SessionID: sessionID, Strategy: StrategyNameAutoCommit, // Use new strategy name MetadataDir: ctx.MetadataDirAbs, // Copy all files from metadata dir + Branch: branchName, AuthorName: ctx.AuthorName, AuthorEmail: ctx.AuthorEmail, }) @@ -689,7 +693,7 @@ func (s *AutoCommitStrategy) commitTaskCodeToActive(repo *git.Repository, ctx Ta // Returns the metadata commit hash. // When IsIncremental is true, only writes the incremental checkpoint file, skipping transcripts. // Uses checkpoint.WriteCommitted for git operations. -func (s *AutoCommitStrategy) commitTaskMetadataToMetadataBranch(_ *git.Repository, ctx TaskCheckpointContext, checkpointID string) (plumbing.Hash, error) { +func (s *AutoCommitStrategy) commitTaskMetadataToMetadataBranch(repo *git.Repository, ctx TaskCheckpointContext, checkpointID string) (plumbing.Hash, error) { store, err := s.getCheckpointStore() if err != nil { return plumbing.ZeroHash, fmt.Errorf("failed to get checkpoint store: %w", err) @@ -715,6 +719,9 @@ func (s *AutoCommitStrategy) commitTaskMetadataToMetadataBranch(_ *git.Repositor messageSubject = FormatSubagentEndMessage(ctx.SubagentType, ctx.TaskDescription, shortToolUseID) } + // Get current branch name for commit lookup hint + branchName := getCurrentBranchName(repo) + // Write committed checkpoint using the checkpoint store err = store.WriteCommitted(context.Background(), checkpoint.WriteCommittedOptions{ CheckpointID: checkpointID, @@ -731,6 +738,7 @@ func (s *AutoCommitStrategy) commitTaskMetadataToMetadataBranch(_ *git.Repositor IncrementalType: ctx.IncrementalType, IncrementalData: ctx.IncrementalData, CommitSubject: messageSubject, + Branch: branchName, AuthorName: ctx.AuthorName, AuthorEmail: ctx.AuthorEmail, }) diff --git a/cmd/entire/cli/strategy/common.go b/cmd/entire/cli/strategy/common.go index 5d0aef589c..fb5bee63db 100644 --- a/cmd/entire/cli/strategy/common.go +++ b/cmd/entire/cli/strategy/common.go @@ -1107,3 +1107,18 @@ func GetMainBranchHash(repo *git.Repository) plumbing.Hash { } return plumbing.ZeroHash } + +// getCurrentBranchName returns the current branch name, or empty string if not on a branch (detached HEAD). +// Returns short name like "main" or "feature/add-auth", not the full ref path. +func getCurrentBranchName(repo *git.Repository) string { + head, err := repo.Head() + if err != nil { + return "" + } + + if !head.Name().IsBranch() { + return "" // Detached HEAD + } + + return head.Name().Short() +} diff --git a/cmd/entire/cli/strategy/manual_commit_condensation.go b/cmd/entire/cli/strategy/manual_commit_condensation.go index 301007f250..778c5a45e5 100644 --- a/cmd/entire/cli/strategy/manual_commit_condensation.go +++ b/cmd/entire/cli/strategy/manual_commit_condensation.go @@ -116,6 +116,9 @@ func (s *ManualCommitStrategy) CondenseSession(repo *git.Repository, checkpointI // Get author info authorName, authorEmail := GetGitAuthorFromRepo(repo) + // Get current branch name for commit lookup hint + branchName := getCurrentBranchName(repo) + // Write checkpoint metadata using the checkpoint store if err := store.WriteCommitted(context.Background(), cpkg.WriteCommittedOptions{ CheckpointID: checkpointID, @@ -126,6 +129,7 @@ func (s *ManualCommitStrategy) CondenseSession(repo *git.Repository, checkpointI Context: sessionData.Context, FilesTouched: sessionData.FilesTouched, CheckpointsCount: state.CheckpointCount, + Branch: branchName, EphemeralBranch: shadowBranchName, AuthorName: authorName, AuthorEmail: authorEmail, diff --git a/cmd/entire/cli/strategy/manual_commit_types.go b/cmd/entire/cli/strategy/manual_commit_types.go index e65c9834f9..f999285bb2 100644 --- a/cmd/entire/cli/strategy/manual_commit_types.go +++ b/cmd/entire/cli/strategy/manual_commit_types.go @@ -35,6 +35,7 @@ type CheckpointInfo struct { CreatedAt time.Time `json:"created_at"` CheckpointsCount int `json:"checkpoints_count"` FilesTouched []string `json:"files_touched"` + Branch string `json:"branch,omitempty"` // Branch where the code commit lives (hint for finding commit message) IsTask bool `json:"is_task,omitempty"` ToolUseID string `json:"tool_use_id,omitempty"` } diff --git a/cmd/entire/cli/strategy/session.go b/cmd/entire/cli/strategy/session.go index 5fe3353eb2..50ededa0b6 100644 --- a/cmd/entire/cli/strategy/session.go +++ b/cmd/entire/cli/strategy/session.go @@ -9,6 +9,7 @@ import ( "entire.io/cli/cmd/entire/cli/paths" "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) @@ -109,7 +110,7 @@ func ListSessions() ([]Session, error) { }) } else { // Get description - first try commit message, then fall back to prompt.txt - description := getDescriptionForCheckpoint(repo, cp.CheckpointID, cp.CreatedAt) + description := getDescriptionForCheckpoint(repo, cp.CheckpointID, cp.CreatedAt, cp.Branch) sessionMap[cp.SessionID] = &Session{ ID: cp.SessionID, @@ -204,9 +205,11 @@ func GetSession(sessionID string) (*Session, error) { // then falls back to reading prompt.txt from the entire/sessions branch. // The metadataCreatedAt is used to optimize the commit search - we only look at commits // from a few minutes before this timestamp up to now (commits can be rebased to be newer). -func getDescriptionForCheckpoint(repo *git.Repository, checkpointID string, metadataCreatedAt time.Time) string { +// The branchHint is the branch where the commit was originally created, used to prioritize +// searching that branch first (useful for unmerged PRs). +func getDescriptionForCheckpoint(repo *git.Repository, checkpointID string, metadataCreatedAt time.Time, branchHint string) string { // First, try to find the commit message - if commitMsg := findCommitMessageByCheckpointID(repo, checkpointID, metadataCreatedAt); commitMsg != "" { + if commitMsg := findCommitMessageByCheckpointID(repo, checkpointID, metadataCreatedAt, branchHint); commitMsg != "" { return commitMsg } @@ -226,20 +229,43 @@ func getDescriptionForCheckpoint(repo *git.Repository, checkpointID string, meta // - The original commit was created around metadataCreatedAt // - Rebase/amend can make the commit newer than the metadata timestamp // - We don't need to look at commits older than a few minutes before the metadata -func findCommitMessageByCheckpointID(repo *git.Repository, checkpointID string, metadataCreatedAt time.Time) string { +// If branchHint is provided, searches that branch first before falling back to HEAD. +func findCommitMessageByCheckpointID(repo *git.Repository, checkpointID string, metadataCreatedAt time.Time, branchHint string) string { // Search window: commits from 5 minutes before metadata timestamp to now // The commit can't be much older than the metadata, but can be arbitrarily newer (after rebase) searchLowerBound := metadataCreatedAt.Add(-5 * time.Minute) - // Get HEAD to start iteration + // Try branch hint first if provided + if branchHint != "" { + if msg := searchBranchForCheckpoint(repo, branchHint, checkpointID, searchLowerBound); msg != "" { + return msg + } + } + + // Fall back to searching from HEAD head, err := repo.Head() if err != nil { return "" } - // Iterate through commits + return searchCommitsForCheckpoint(repo, head.Hash(), checkpointID, searchLowerBound) +} + +// searchBranchForCheckpoint searches a specific branch for a checkpoint. +func searchBranchForCheckpoint(repo *git.Repository, branchName, checkpointID string, searchLowerBound time.Time) string { + // Try to resolve the branch + ref, err := repo.Reference(plumbing.NewBranchReferenceName(branchName), true) + if err != nil { + return "" // Branch doesn't exist + } + + return searchCommitsForCheckpoint(repo, ref.Hash(), checkpointID, searchLowerBound) +} + +// searchCommitsForCheckpoint searches commits starting from a given hash. +func searchCommitsForCheckpoint(repo *git.Repository, startHash plumbing.Hash, checkpointID string, searchLowerBound time.Time) string { iter, err := repo.Log(&git.LogOptions{ - From: head.Hash(), + From: startHash, Order: git.LogOrderCommitterTime, }) if err != nil { From b9ad0cd1f4439e8bed7f848a1b70726e75d9abf5 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Tue, 6 Jan 2026 19:49:10 +0100 Subject: [PATCH 3/7] Can we also show the branch name in `entire session list` Entire-Checkpoint: e367e53d89ee --- cmd/entire/cli/session.go | 19 ++++++++++++++----- cmd/entire/cli/strategy/session.go | 4 ++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/cmd/entire/cli/session.go b/cmd/entire/cli/session.go index 5b2fd8c444..665b6229a6 100644 --- a/cmd/entire/cli/session.go +++ b/cmd/entire/cli/session.go @@ -330,8 +330,8 @@ func runSessionList() error { } // Print header (2-space indent to align with marker column) - fmt.Printf(" %-19s %-11s %s\n", "session-id", "Checkpoints", "Description") - fmt.Printf(" %-19s %-11s %s\n", "───────────────────", "───────────", "────────────────────────────────────────────────────────────────") + fmt.Printf(" %-19s %-11s %-20s %s\n", "session-id", "Checkpoints", "Branch", "Description") + fmt.Printf(" %-19s %-11s %-20s %s\n", "───────────────────", "───────────", "────────────────────", "────────────────────────────────────────") for _, sess := range sessions { // Show session ID - truncate to 19 chars for display @@ -350,13 +350,22 @@ func runSessionList() error { // Checkpoint count checkpoints := len(sess.Checkpoints) + // Branch name - truncate if needed + branch := sess.Branch + if branch == "" { + branch = "-" + } + if len(branch) > 20 { + branch = branch[:17] + "..." + } + // Truncate description for display description := sess.Description - if len(description) > 64 { - description = description[:61] + "..." + if len(description) > 44 { + description = description[:41] + "..." } - fmt.Printf("%s%-19s %-11d %s\n", marker, displayID, checkpoints, description) + fmt.Printf("%s%-19s %-11d %-20s %s\n", marker, displayID, checkpoints, branch, description) } // Print usage hint diff --git a/cmd/entire/cli/strategy/session.go b/cmd/entire/cli/strategy/session.go index 50ededa0b6..c36a6dcbcb 100644 --- a/cmd/entire/cli/strategy/session.go +++ b/cmd/entire/cli/strategy/session.go @@ -27,6 +27,9 @@ type Session struct { // (typically the first prompt or derived from commit messages) Description string + // Branch is the branch where the session's code commit lives + Branch string + // Strategy is the name of the strategy that created this session Strategy string @@ -115,6 +118,7 @@ func ListSessions() ([]Session, error) { sessionMap[cp.SessionID] = &Session{ ID: cp.SessionID, Description: description, + Branch: cp.Branch, Strategy: "", // Will be set from metadata if available StartTime: cp.CreatedAt, Checkpoints: []Checkpoint{{ From b6ea36bd1bcb7b553a732d75ddd2e0e87c9f1749 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Tue, 6 Jan 2026 19:53:05 +0100 Subject: [PATCH 4/7] Thinking about "Refactored findCommitMessageByCheckpointID() to search t Entire-Checkpoint: f839a29c35ef --- cmd/entire/cli/strategy/session.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cmd/entire/cli/strategy/session.go b/cmd/entire/cli/strategy/session.go index c36a6dcbcb..2b5993a095 100644 --- a/cmd/entire/cli/strategy/session.go +++ b/cmd/entire/cli/strategy/session.go @@ -233,26 +233,28 @@ func getDescriptionForCheckpoint(repo *git.Repository, checkpointID string, meta // - The original commit was created around metadataCreatedAt // - Rebase/amend can make the commit newer than the metadata timestamp // - We don't need to look at commits older than a few minutes before the metadata -// If branchHint is provided, searches that branch first before falling back to HEAD. +// Searches HEAD first (to find merged commits), then falls back to branchHint (for unmerged PRs). func findCommitMessageByCheckpointID(repo *git.Repository, checkpointID string, metadataCreatedAt time.Time, branchHint string) string { // Search window: commits from 5 minutes before metadata timestamp to now // The commit can't be much older than the metadata, but can be arbitrarily newer (after rebase) searchLowerBound := metadataCreatedAt.Add(-5 * time.Minute) - // Try branch hint first if provided - if branchHint != "" { - if msg := searchBranchForCheckpoint(repo, branchHint, checkpointID, searchLowerBound); msg != "" { + // First, search from HEAD (finds merged commits or commits on current branch) + head, err := repo.Head() + if err == nil { + if msg := searchCommitsForCheckpoint(repo, head.Hash(), checkpointID, searchLowerBound); msg != "" { return msg } } - // Fall back to searching from HEAD - head, err := repo.Head() - if err != nil { - return "" + // Fall back to searching the branch hint (for unmerged PRs or if we're on a different branch) + if branchHint != "" { + if msg := searchBranchForCheckpoint(repo, branchHint, checkpointID, searchLowerBound); msg != "" { + return msg + } } - return searchCommitsForCheckpoint(repo, head.Hash(), checkpointID, searchLowerBound) + return "" } // searchBranchForCheckpoint searches a specific branch for a checkpoint. From 7e9e975b2c77c6b7281430629c66545df12a5b94 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Tue, 6 Jan 2026 20:43:21 +0100 Subject: [PATCH 5/7] Option 3 Entire-Checkpoint: a3c1ef347792 --- cmd/entire/cli/strategy/session.go | 65 ++++++++++++++++-------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/cmd/entire/cli/strategy/session.go b/cmd/entire/cli/strategy/session.go index 2b5993a095..e8499df610 100644 --- a/cmd/entire/cli/strategy/session.go +++ b/cmd/entire/cli/strategy/session.go @@ -112,13 +112,32 @@ func ListSessions() ([]Session, error) { ToolUseID: cp.ToolUseID, }) } else { - // Get description - first try commit message, then fall back to prompt.txt - description := getDescriptionForCheckpoint(repo, cp.CheckpointID, cp.CreatedAt, cp.Branch) + // Get description and determine current location + description, foundInHead := findCommitMessageByCheckpointID(repo, cp.CheckpointID, cp.CreatedAt, cp.Branch) + if description == "" { + // Fall back to reading from entire/sessions branch + tree, err := GetMetadataBranchTree(repo) + if err == nil { + checkpointPath := paths.CheckpointPath(cp.CheckpointID) + description = getSessionDescriptionFromTree(tree, checkpointPath) + } else { + description = NoDescription + } + } + + // Determine display branch: show current location if different from original + displayBranch := cp.Branch + if foundInHead && cp.Branch != "" { + currentBranch := getCurrentBranchName(repo) + if currentBranch != "" && currentBranch != cp.Branch { + displayBranch = formatBranchLocation(currentBranch, cp.Branch) + } + } sessionMap[cp.SessionID] = &Session{ ID: cp.SessionID, Description: description, - Branch: cp.Branch, + Branch: displayBranch, Strategy: "", // Will be set from metadata if available StartTime: cp.CreatedAt, Checkpoints: []Checkpoint{{ @@ -204,37 +223,15 @@ func GetSession(sessionID string) (*Session, error) { return findSessionByID(sessions, sessionID) } -// getDescriptionForCheckpoint gets the description for a checkpoint. -// First tries to find the commit message from a commit with the Entire-Checkpoint trailer, -// then falls back to reading prompt.txt from the entire/sessions branch. -// The metadataCreatedAt is used to optimize the commit search - we only look at commits -// from a few minutes before this timestamp up to now (commits can be rebased to be newer). -// The branchHint is the branch where the commit was originally created, used to prioritize -// searching that branch first (useful for unmerged PRs). -func getDescriptionForCheckpoint(repo *git.Repository, checkpointID string, metadataCreatedAt time.Time, branchHint string) string { - // First, try to find the commit message - if commitMsg := findCommitMessageByCheckpointID(repo, checkpointID, metadataCreatedAt, branchHint); commitMsg != "" { - return commitMsg - } - - // Fall back to reading from entire/sessions branch (prompt.txt or context.md) - tree, err := GetMetadataBranchTree(repo) - if err != nil { - return NoDescription - } - - checkpointPath := paths.CheckpointPath(checkpointID) - return getSessionDescriptionFromTree(tree, checkpointPath) -} - // findCommitMessageByCheckpointID searches for a commit with the given Entire-Checkpoint trailer -// and returns the first line of its commit message. +// and returns the first line of its commit message, plus whether it was found in HEAD. // The search window is from (metadataCreatedAt - 5 minutes) to now, since: // - The original commit was created around metadataCreatedAt // - Rebase/amend can make the commit newer than the metadata timestamp // - We don't need to look at commits older than a few minutes before the metadata // Searches HEAD first (to find merged commits), then falls back to branchHint (for unmerged PRs). -func findCommitMessageByCheckpointID(repo *git.Repository, checkpointID string, metadataCreatedAt time.Time, branchHint string) string { +// Returns (message, foundInHead). +func findCommitMessageByCheckpointID(repo *git.Repository, checkpointID string, metadataCreatedAt time.Time, branchHint string) (string, bool) { // Search window: commits from 5 minutes before metadata timestamp to now // The commit can't be much older than the metadata, but can be arbitrarily newer (after rebase) searchLowerBound := metadataCreatedAt.Add(-5 * time.Minute) @@ -243,18 +240,18 @@ func findCommitMessageByCheckpointID(repo *git.Repository, checkpointID string, head, err := repo.Head() if err == nil { if msg := searchCommitsForCheckpoint(repo, head.Hash(), checkpointID, searchLowerBound); msg != "" { - return msg + return msg, true // Found in HEAD } } // Fall back to searching the branch hint (for unmerged PRs or if we're on a different branch) if branchHint != "" { if msg := searchBranchForCheckpoint(repo, branchHint, checkpointID, searchLowerBound); msg != "" { - return msg + return msg, false // Found in branch hint, not in HEAD } } - return "" + return "", false } // searchBranchForCheckpoint searches a specific branch for a checkpoint. @@ -325,6 +322,12 @@ func extractCommitSubject(message string) string { return subject } +// formatBranchLocation formats a branch location string showing current and original branches. +// Returns "current (was original)" format, e.g., "main (was feature/add-auth)". +func formatBranchLocation(currentBranch, originalBranch string) string { + return fmt.Sprintf("%s (was %s)", currentBranch, originalBranch) +} + // findSessionByID finds a session by exact ID or prefix match. func findSessionByID(sessions []Session, sessionID string) (*Session, error) { for _, session := range sessions { From c294986cfc52a18ebf4dd3ecd2fabbb45a089608 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Tue, 6 Jan 2026 20:45:42 +0100 Subject: [PATCH 6/7] Can we make it a bit wider Entire-Checkpoint: c55af530f311 --- cmd/entire/cli/session.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/entire/cli/session.go b/cmd/entire/cli/session.go index 665b6229a6..6d74219f33 100644 --- a/cmd/entire/cli/session.go +++ b/cmd/entire/cli/session.go @@ -330,8 +330,8 @@ func runSessionList() error { } // Print header (2-space indent to align with marker column) - fmt.Printf(" %-19s %-11s %-20s %s\n", "session-id", "Checkpoints", "Branch", "Description") - fmt.Printf(" %-19s %-11s %-20s %s\n", "───────────────────", "───────────", "────────────────────", "────────────────────────────────────────") + fmt.Printf(" %-19s %-11s %-30s %s\n", "session-id", "Checkpoints", "Branch", "Description") + fmt.Printf(" %-19s %-11s %-30s %s\n", "───────────────────", "───────────", "──────────────────────────────", "────────────────────────────────────────") for _, sess := range sessions { // Show session ID - truncate to 19 chars for display @@ -355,8 +355,8 @@ func runSessionList() error { if branch == "" { branch = "-" } - if len(branch) > 20 { - branch = branch[:17] + "..." + if len(branch) > 30 { + branch = branch[:27] + "..." } // Truncate description for display @@ -365,7 +365,7 @@ func runSessionList() error { description = description[:41] + "..." } - fmt.Printf("%s%-19s %-11d %-20s %s\n", marker, displayID, checkpoints, branch, description) + fmt.Printf("%s%-19s %-11d %-30s %s\n", marker, displayID, checkpoints, branch, description) } // Print usage hint From 278b6bbacbe9d402fde448cd6146e912145a8503 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Tue, 6 Jan 2026 21:01:47 +0100 Subject: [PATCH 7/7] search none checked out branches too Entire-Checkpoint: 7d66f047ac98 --- cmd/entire/cli/strategy/session.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cmd/entire/cli/strategy/session.go b/cmd/entire/cli/strategy/session.go index e8499df610..dcbf0caa42 100644 --- a/cmd/entire/cli/strategy/session.go +++ b/cmd/entire/cli/strategy/session.go @@ -255,11 +255,16 @@ func findCommitMessageByCheckpointID(repo *git.Repository, checkpointID string, } // searchBranchForCheckpoint searches a specific branch for a checkpoint. +// Tries local branch first, then falls back to remote tracking branch (origin/). func searchBranchForCheckpoint(repo *git.Repository, branchName, checkpointID string, searchLowerBound time.Time) string { - // Try to resolve the branch + // Try to resolve the local branch first ref, err := repo.Reference(plumbing.NewBranchReferenceName(branchName), true) if err != nil { - return "" // Branch doesn't exist + // Local branch doesn't exist, try remote tracking branch + ref, err = repo.Reference(plumbing.NewRemoteReferenceName("origin", branchName), true) + if err != nil { + return "" // Neither local nor remote branch exists + } } return searchCommitsForCheckpoint(repo, ref.Hash(), checkpointID, searchLowerBound)