From 8e6b65b68f34b35b2d918872dc6734e403417298 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Mon, 26 Jan 2026 17:34:41 +0100 Subject: [PATCH 1/4] fix a bug that caused session conflicts Entire-Checkpoint: fd00dbbb51f1 --- cmd/entire/cli/hooks_claudecode_handlers.go | 73 +-------------------- 1 file changed, 3 insertions(+), 70 deletions(-) diff --git a/cmd/entire/cli/hooks_claudecode_handlers.go b/cmd/entire/cli/hooks_claudecode_handlers.go index 35f67287b..a955eeeed 100644 --- a/cmd/entire/cli/hooks_claudecode_handlers.go +++ b/cmd/entire/cli/hooks_claudecode_handlers.go @@ -774,80 +774,13 @@ func handlePreTask() error { // Log context to stdout logPreTaskHookContext(os.Stdout, input) - // Capture pre-task state + // Capture pre-task state locally (for computing new files when task completes) + // We don't create a shadow branch commit here - commits are only created + // when the task actually makes file changes (in handlePostTask/handlePostTodo) if err := CapturePreTaskState(input.ToolUseID); err != nil { return fmt.Errorf("failed to capture pre-task state: %w", err) } - // Create "Starting agent" checkpoint - // This allows rewinding to the state just before the subagent began - if err := createStartingAgentCheckpoint(input); err != nil { - // Log warning but don't fail the hook - state was already captured - fmt.Fprintf(os.Stderr, "Warning: failed to create starting checkpoint: %v\n", err) - } - - return nil -} - -// createStartingAgentCheckpoint creates a checkpoint commit marking the start of a subagent. -// This is called during PreToolUse[Task] hook. -func createStartingAgentCheckpoint(input *TaskHookInput) error { - // Get git author - author, err := GetGitAuthor() - if err != nil { - return fmt.Errorf("failed to get git author: %w", err) - } - - // Get the active strategy - strat := GetStrategy() - - // Ensure strategy setup is complete - if err := strat.EnsureSetup(); err != nil { - return fmt.Errorf("failed to ensure strategy setup: %w", err) - } - - entireSessionID := currentSessionIDWithFallback(input.SessionID) - - // Extract subagent type and description from tool_input for descriptive commit messages - subagentType, taskDescription := ParseSubagentTypeAndDescription(input.ToolInput) - - // Get agent type from session state - var agentType agent.AgentType - if sessionState, loadErr := strategy.LoadSessionState(entireSessionID); loadErr == nil && sessionState != nil { - agentType = sessionState.AgentType - } - - // Build task checkpoint context for the "starting" checkpoint - ctx := strategy.TaskCheckpointContext{ - SessionID: entireSessionID, - ToolUseID: input.ToolUseID, - TranscriptPath: input.TranscriptPath, - AuthorName: author.Name, - AuthorEmail: author.Email, - SubagentType: subagentType, - TaskDescription: taskDescription, - AgentType: agentType, - // No file changes yet - this is the starting state - ModifiedFiles: nil, - NewFiles: nil, - DeletedFiles: nil, - // Mark as starting checkpoint (sequence 0) - IsIncremental: true, - IncrementalSequence: 0, - IncrementalType: strategy.IncrementalTypeTaskStart, - } - - // Save the checkpoint - if err := strat.SaveTaskCheckpoint(ctx); err != nil { - return fmt.Errorf("failed to save starting checkpoint: %w", err) - } - - shortID := input.ToolUseID - if len(shortID) > 12 { - shortID = shortID[:12] - } - fmt.Fprintf(os.Stderr, "[entire] Created starting checkpoint for task %s\n", shortID) - return nil } From d102b447c4b8c53593e0cc3d4ad43746262f8f52 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Mon, 26 Jan 2026 18:23:10 +0100 Subject: [PATCH 2/4] cleanup / refactoring / deadcode Entire-Checkpoint: c3851e15e4c2 --- cmd/entire/cli/hooks_claudecode_handlers.go | 7 +- cmd/entire/cli/strategy/auto_commit.go | 26 ++---- cmd/entire/cli/strategy/auto_commit_test.go | 91 ++------------------- cmd/entire/cli/strategy/messages.go | 23 ++---- cmd/entire/cli/strategy/messages_test.go | 76 +---------------- cmd/entire/cli/strategy/strategy.go | 4 - 6 files changed, 24 insertions(+), 203 deletions(-) diff --git a/cmd/entire/cli/hooks_claudecode_handlers.go b/cmd/entire/cli/hooks_claudecode_handlers.go index a955eeeed..d998c969a 100644 --- a/cmd/entire/cli/hooks_claudecode_handlers.go +++ b/cmd/entire/cli/hooks_claudecode_handlers.go @@ -774,9 +774,10 @@ func handlePreTask() error { // Log context to stdout logPreTaskHookContext(os.Stdout, input) - // Capture pre-task state locally (for computing new files when task completes) - // We don't create a shadow branch commit here - commits are only created - // when the task actually makes file changes (in handlePostTask/handlePostTodo) + // Capture pre-task state locally (for computing new files when task completes). + // We don't create a shadow branch commit here. Commits are created during + // task completion (handlePostTask/handlePostTodo) only if the task resulted + // in file changes. if err := CapturePreTaskState(input.ToolUseID); err != nil { return fmt.Errorf("failed to capture pre-task state: %w", err) } diff --git a/cmd/entire/cli/strategy/auto_commit.go b/cmd/entire/cli/strategy/auto_commit.go index 7412d50cc..6f3c152be 100644 --- a/cmd/entire/cli/strategy/auto_commit.go +++ b/cmd/entire/cli/strategy/auto_commit.go @@ -578,14 +578,12 @@ func (s *AutoCommitStrategy) SaveTaskCheckpoint(ctx TaskCheckpointContext) error // commitTaskCodeToActive commits task code changes to the active branch. // Adds an Entire-Checkpoint trailer for metadata lookup that survives amend/rebase. -// For TaskStart checkpoints, creates an empty marker commit even without file changes. +// Skips commit creation if there are no file changes. func (s *AutoCommitStrategy) commitTaskCodeToActive(repo *git.Repository, ctx TaskCheckpointContext, checkpointID id.CheckpointID) (plumbing.Hash, error) { - // For TaskStart, we want to create a marker commit even without file changes - isTaskStart := ctx.IsIncremental && ctx.IncrementalType == IncrementalTypeTaskStart hasFileChanges := len(ctx.ModifiedFiles) > 0 || len(ctx.NewFiles) > 0 || len(ctx.DeletedFiles) > 0 - // If no file changes and not a TaskStart, skip code commit - if !hasFileChanges && !isTaskStart { + // If no file changes, skip code commit + if !hasFileChanges { fmt.Fprintf(os.Stderr, "No code changes to commit for task checkpoint\n") // Return current HEAD hash so metadata can still be stored head, err := repo.Head() @@ -632,21 +630,9 @@ func (s *AutoCommitStrategy) commitTaskCodeToActive(repo *git.Repository, ctx Ta When: time.Now(), } - var commitHash plumbing.Hash - if isTaskStart { - // For TaskStart, allow empty commits (marker commits) - commitHash, err = worktree.Commit(commitMsg, &git.CommitOptions{ - Author: author, - AllowEmptyCommits: true, - }) - if err != nil { - return plumbing.ZeroHash, fmt.Errorf("failed to create TaskStart marker commit: %w", err) - } - } else { - commitHash, err = commitOrHead(repo, worktree, commitMsg, author) - if err != nil { - return plumbing.ZeroHash, err - } + commitHash, err := commitOrHead(repo, worktree, commitMsg, author) + if err != nil { + return plumbing.ZeroHash, err } if ctx.IsIncremental { diff --git a/cmd/entire/cli/strategy/auto_commit_test.go b/cmd/entire/cli/strategy/auto_commit_test.go index 79450b8ee..4cad72865 100644 --- a/cmd/entire/cli/strategy/auto_commit_test.go +++ b/cmd/entire/cli/strategy/auto_commit_test.go @@ -357,86 +357,7 @@ func TestAutoCommitStrategy_SaveTaskCheckpoint_CommitHasMetadataRef(t *testing.T } } -func TestAutoCommitStrategy_SaveTaskCheckpoint_TaskStartCreatesEmptyCommit(t *testing.T) { - // Setup temp git repo - dir := t.TempDir() - repo, err := git.PlainInit(dir, false) - if err != nil { - t.Fatalf("failed to init git repo: %v", err) - } - - // Create initial commit (needed for a valid repo state) - worktree, err := repo.Worktree() - if err != nil { - t.Fatalf("failed to get worktree: %v", err) - } - readmeFile := filepath.Join(dir, "README.md") - if err := os.WriteFile(readmeFile, []byte("# Test"), 0o644); err != nil { - t.Fatalf("failed to write README: %v", err) - } - if _, err := worktree.Add("README.md"); err != nil { - t.Fatalf("failed to add README: %v", err) - } - initialCommit, err := worktree.Commit("Initial commit", &git.CommitOptions{ - Author: &object.Signature{Name: "Test", Email: "test@test.com"}, - }) - if err != nil { - t.Fatalf("failed to commit: %v", err) - } - - t.Chdir(dir) - - // Setup strategy - s := NewAutoCommitStrategy() - if err := s.EnsureSetup(); err != nil { - t.Fatalf("EnsureSetup() error = %v", err) - } - - // Create a TaskStart checkpoint with NO file changes - ctx := TaskCheckpointContext{ - SessionID: "test-session-taskstart", - ToolUseID: "toolu_taskstart123", - IsIncremental: true, - IncrementalType: IncrementalTypeTaskStart, - IncrementalSequence: 1, - SubagentType: "dev", - TaskDescription: "Implement feature", - // No file changes - ModifiedFiles: []string{}, - NewFiles: []string{}, - DeletedFiles: []string{}, - AuthorName: "Test", - AuthorEmail: "test@test.com", - } - - if err := s.SaveTaskCheckpoint(ctx); err != nil { - t.Fatalf("SaveTaskCheckpoint() error = %v", err) - } - - // Verify a NEW commit was created (not just returning HEAD) - head, err := repo.Head() - if err != nil { - t.Fatalf("failed to get HEAD: %v", err) - } - - if head.Hash() == initialCommit { - t.Error("TaskStart should create a new commit even without file changes, but HEAD is still the initial commit") - } - - // Verify the commit message contains the expected content - commit, err := repo.CommitObject(head.Hash()) - if err != nil { - t.Fatalf("failed to get HEAD commit: %v", err) - } - - // TaskStart commits should have "Starting" in the message - expectedSubstring := "Starting 'dev' agent: Implement feature" - if !strings.Contains(commit.Message, expectedSubstring) { - t.Errorf("commit message should contain %q, got:\n%s", expectedSubstring, commit.Message) - } -} - -func TestAutoCommitStrategy_SaveTaskCheckpoint_NonTaskStartNoChangesAmendedForMetadata(t *testing.T) { +func TestAutoCommitStrategy_SaveTaskCheckpoint_NoChangesSkipsCommit(t *testing.T) { // Setup temp git repo dir := t.TempDir() repo, err := git.PlainInit(dir, false) @@ -471,12 +392,12 @@ func TestAutoCommitStrategy_SaveTaskCheckpoint_NonTaskStartNoChangesAmendedForMe t.Fatalf("EnsureSetup() error = %v", err) } - // Create a regular incremental checkpoint (not TaskStart) with NO file changes + // Create an incremental checkpoint with NO file changes ctx := TaskCheckpointContext{ - SessionID: "test-session-nontaskstart", - ToolUseID: "toolu_nontaskstart456", + SessionID: "test-session-nochanges", + ToolUseID: "toolu_nochanges456", IsIncremental: true, - IncrementalType: "TodoWrite", // NOT TaskStart + IncrementalType: "TodoWrite", IncrementalSequence: 2, TodoContent: "Write some code", // No file changes @@ -512,7 +433,7 @@ func TestAutoCommitStrategy_SaveTaskCheckpoint_NonTaskStartNoChangesAmendedForMe // The tree hash should be the same (no file changes) if newCommit.TreeHash != oldCommit.TreeHash { - t.Error("non-TaskStart checkpoint without file changes should have the same tree hash") + t.Error("checkpoint without file changes should have the same tree hash") } // Metadata should still be stored on entire/sessions branch diff --git a/cmd/entire/cli/strategy/messages.go b/cmd/entire/cli/strategy/messages.go index 6aa28ea97..85ce62d8e 100644 --- a/cmd/entire/cli/strategy/messages.go +++ b/cmd/entire/cli/strategy/messages.go @@ -21,17 +21,6 @@ func TruncateDescription(s string, maxLen int) string { return s[:maxLen-3] + "..." } -// FormatSubagentStartMessage formats a commit message for when a subagent starts. -// Format: "Starting '' agent: ()" -// -// Edge cases: -// - Empty description: "Starting '' agent ()" -// - Empty agentType: "Starting agent: ()" -// - Both empty: "Task: " -func FormatSubagentStartMessage(agentType, description, toolUseID string) string { - return formatSubagentMessage("Starting", agentType, description, toolUseID) -} - // FormatSubagentEndMessage formats a commit message for when a subagent completes. // Format: "Completed '' agent: ()" // @@ -67,10 +56,11 @@ func formatSubagentMessage(verb, agentType, description, toolUseID string) strin } // FormatIncrementalSubject formats the commit message subject for incremental checkpoints. -// Handles both TaskStart (starting agent) and regular incremental (TodoWrite) checkpoints. +// Delegates to FormatIncrementalMessage. // -// For TaskStart: delegates to FormatSubagentStartMessage -// For other types: delegates to FormatIncrementalMessage +// Note: The incrementalType, subagentType, and taskDescription parameters are kept for +// API compatibility but are not currently used. They may be used in the future for +// different checkpoint types. func FormatIncrementalSubject( incrementalType string, subagentType string, @@ -79,9 +69,8 @@ func FormatIncrementalSubject( incrementalSequence int, shortToolUseID string, ) string { - if incrementalType == IncrementalTypeTaskStart { - return FormatSubagentStartMessage(subagentType, taskDescription, shortToolUseID) - } + // Currently all incremental checkpoints use the same format + _, _, _ = incrementalType, subagentType, taskDescription // Silence unused warnings return FormatIncrementalMessage(todoContent, incrementalSequence, shortToolUseID) } diff --git a/cmd/entire/cli/strategy/messages_test.go b/cmd/entire/cli/strategy/messages_test.go index 3229e6e5e..06c0293aa 100644 --- a/cmd/entire/cli/strategy/messages_test.go +++ b/cmd/entire/cli/strategy/messages_test.go @@ -51,62 +51,6 @@ func TestTruncateDescription(t *testing.T) { } } -func TestFormatSubagentStartMessage(t *testing.T) { - tests := []struct { - name string - agentType string - description string - toolUseID string - want string - }{ - { - name: "full message with all fields", - agentType: "dev", - description: "Implement user authentication", - toolUseID: "toolu_019t1c", - want: "Starting 'dev' agent: Implement user authentication (toolu_019t1c)", - }, - { - name: "empty description", - agentType: "dev", - description: "", - toolUseID: "toolu_019t1c", - want: "Starting 'dev' agent (toolu_019t1c)", - }, - { - name: "empty agent type", - agentType: "", - description: "Implement user authentication", - toolUseID: "toolu_019t1c", - want: "Starting agent: Implement user authentication (toolu_019t1c)", - }, - { - name: "both empty", - agentType: "", - description: "", - toolUseID: "toolu_019t1c", - want: "Task: toolu_019t1c", - }, - { - name: "long description truncated", - agentType: "dev", - description: "This is a very long description that should be truncated to fit within the limit", - toolUseID: "toolu_019t1c", - want: "Starting 'dev' agent: This is a very long description that should be truncated ... (toolu_019t1c)", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := FormatSubagentStartMessage(tt.agentType, tt.description, tt.toolUseID) - if got != tt.want { - t.Errorf("FormatSubagentStartMessage(%q, %q, %q) = %q, want %q", - tt.agentType, tt.description, tt.toolUseID, got, tt.want) - } - }) - } -} - func TestFormatSubagentEndMessage(t *testing.T) { tests := []struct { name string @@ -316,23 +260,7 @@ func TestFormatIncrementalSubject(t *testing.T) { want string }{ { - name: "TaskStart with full details", - incrementalType: IncrementalTypeTaskStart, - subagentType: "dev", - taskDescription: "Implement user authentication", - shortToolUseID: "toolu_019t1c", - want: "Starting 'dev' agent: Implement user authentication (toolu_019t1c)", - }, - { - name: "TaskStart with empty description", - incrementalType: IncrementalTypeTaskStart, - subagentType: "dev", - taskDescription: "", - shortToolUseID: "toolu_019t1c", - want: "Starting 'dev' agent (toolu_019t1c)", - }, - { - name: "Regular incremental with todo content", + name: "incremental with todo content", incrementalType: "TodoWrite", todoContent: "Set up Node.js project", incrementalSequence: 1, @@ -340,7 +268,7 @@ func TestFormatIncrementalSubject(t *testing.T) { want: "Set up Node.js project (toolu_01CJhrr)", }, { - name: "Regular incremental without todo content", + name: "incremental without todo content", incrementalType: "TodoWrite", todoContent: "", incrementalSequence: 3, diff --git a/cmd/entire/cli/strategy/strategy.go b/cmd/entire/cli/strategy/strategy.go index 6e599b5bf..9ce90be53 100644 --- a/cmd/entire/cli/strategy/strategy.go +++ b/cmd/entire/cli/strategy/strategy.go @@ -25,10 +25,6 @@ var ErrNotTaskCheckpoint = errors.New("not a task checkpoint") // ErrNotImplemented is returned when a feature is not yet implemented. var ErrNotImplemented = errors.New("not implemented") -// IncrementalTypeTaskStart is the incremental type for the starting checkpoint -// created during PreToolUse[Task] hook before a subagent begins execution. -const IncrementalTypeTaskStart = "TaskStart" - // ShadowBranchConflictError is returned when a shadow branch already exists // with activity from a different session or worktree. type ShadowBranchConflictError struct { From c26c21bd5f41de52741dd1374fd4ec10de9a00c1 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Mon, 26 Jan 2026 18:28:41 +0100 Subject: [PATCH 3/4] fixed tests Entire-Checkpoint: a7e6172c3842 --- cmd/entire/cli/integration_test/default_branch_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/entire/cli/integration_test/default_branch_test.go b/cmd/entire/cli/integration_test/default_branch_test.go index 076661adb..642d5e758 100644 --- a/cmd/entire/cli/integration_test/default_branch_test.go +++ b/cmd/entire/cli/integration_test/default_branch_test.go @@ -114,8 +114,8 @@ func TestDefaultBranch_PostTaskWorksOnMain(t *testing.T) { } points := env.GetRewindPoints() - if len(points) != 2 { - t.Errorf("expected 2 rewind points (starting + completed checkpoints) on main for %s, got %d", strategyName, len(points)) + if len(points) != 1 { + t.Errorf("expected 1 rewind point (completed checkpoint) on main for %s, got %d", strategyName, len(points)) } }) } From 49bda7279bb7c82d15f88ed58f1f81654daa27a5 Mon Sep 17 00:00:00 2001 From: Alex Ong Date: Tue, 27 Jan 2026 11:33:38 +1100 Subject: [PATCH 4/4] update docs to reflect removal of TaskStart checkpoints Remove documentation for the "Create Starting Checkpoint" step in PreToolUse[Task] since TaskStart checkpoints no longer exist. Co-Authored-By: Claude Opus 4.5 Entire-Checkpoint: 523d2b8bfaf3 --- docs/architecture/claude-hooks-integration.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/architecture/claude-hooks-integration.md b/docs/architecture/claude-hooks-integration.md index 3f3f4bfc5..cf82ad80e 100644 --- a/docs/architecture/claude-hooks-integration.md +++ b/docs/architecture/claude-hooks-integration.md @@ -11,7 +11,7 @@ Entire integrates with Claude Code through six hooks that fire at different poin | `SessionStart` | New chat session begins | Generate and persist Entire session ID | | `UserPromptSubmit` | User submits a prompt | Capture pre-prompt state, check for conflicts | | `Stop` | Claude finishes responding | Create checkpoint with code + metadata | -| `PreToolUse[Task]` | Subagent is about to start | Capture pre-task state, create starting marker | +| `PreToolUse[Task]` | Subagent is about to start | Capture pre-task state for diff computation | | `PostToolUse[Task]` | Subagent finishes | Create final checkpoint for subagent work | | `PostToolUse[TodoWrite]` | Subagent updates its todo list | Create incremental checkpoint if files changed | @@ -129,7 +129,7 @@ Fires when Claude finishes responding. Does **not** fire on user interrupt (Ctrl - **Command**: `entire hooks claude-code pre-task` - **Handler**: `handlePreTask()` in `hooks_claudecode_handlers.go:668` -Fires just before a subagent (Task tool) begins execution. Creates a clear starting point for the subagent's work. +Fires just before a subagent (Task tool) begins execution. Captures the current state so that file changes can be computed when the task completes. **What it does:** @@ -145,12 +145,7 @@ Fires just before a subagent (Task tool) begins execution. Creates a clear start - Runs `git status` to get current untracked files. - Saves to `.entire/tmp/pre-task-.json`. - This baseline is used by `PostToolUse[Task]` to determine which files the subagent created. - -4. **Create Starting Checkpoint**: - - Calls `strategy.SaveTaskCheckpoint()` with `IncrementalSequence: 0` and `IncrementalType: "task_start"`. - - Creates a commit with no file changes, just metadata marking "Starting: \". - - This allows rewinding to the exact state before the subagent made any changes. - - Includes subagent type and description in commit metadata for better rewind UX. + - **Note**: No checkpoint/commit is created at this stage. Commits are only created during task completion (`PostToolUse[Task]` or `PostToolUse[TodoWrite]`) and only if there are actual file changes. ### `PostToolUse[Task]` @@ -225,8 +220,8 @@ Fires whenever a subagent updates its todo list. Enables fine-grained, increment **Example sequence during a subagent Task:** ``` -PreToolUse[Task] → Checkpoint #0: "Starting: Implement auth feature" -PostToolUse[TodoWrite] → Checkpoint #1: "Planning: 5 todos" +PreToolUse[Task] → (no checkpoint - only captures pre-task state) +PostToolUse[TodoWrite] → Checkpoint #1: "Planning: 5 todos" (if files changed) PostToolUse[TodoWrite] → Checkpoint #2: "Completed: Create user model" PostToolUse[TodoWrite] → Checkpoint #3: "Completed: Add login endpoint" PostToolUse[Task] → Checkpoint #4: Final checkpoint with all changes