feat: handle git reset in session hooks#948
Conversation
There was a problem hiding this comment.
Pull request overview
Adds narrow, status-only handling for git reset-driven session divergence by comparing active session linkage against the current HEAD’s Entire-Checkpoint trailer, auto-reconciling only when the checkpoint match is unambiguous and otherwise emitting a warning.
Changes:
- Resolve
HEADand parseEntire-Checkpointtrailer(s) duringentire statusrendering. - Reconcile active session
BaseCommit/AttributionBaseCommitonly whenHEADcheckpoint matchesLastCheckpointID; otherwise warn without mutating session state. - Add unit tests covering safe reset reconciliation and divergence warning behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| cmd/entire/cli/status.go | Adds HEAD linkage detection, worktree-path normalization, and session divergence reconciliation + warning rendering in status. |
| cmd/entire/cli/status_test.go | Adds tests exercising reset reconciliation vs divergence warning behavior. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit f692674. Configure here.
Transient flag gating the prepare-commit-msg divergence warning. Set when the warning fires, cleared when attribution base realigns with tracking base after condensation.
When HEAD carries the session's LastCheckpointID as an Entire-Checkpoint trailer (e.g., after git reset --hard to a condensed commit), update both BaseCommit and AttributionBaseCommit to HEAD without renaming or deleting the old shadow branch. Falls through to the existing migrate path for all other HEAD-moved cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ution migrateShadowBranchIfNeeded now runs before state.LastCheckpointID is cleared and before calculatePromptAttributionAtStart. This ensures: 1. The reconcile guard can read LastCheckpointID at turn start. 2. Attribution diffs against the reconciled BaseCommit, not the stale one.
Add HEAD-vs-session divergence detection to writeActiveSessions: - Full divergence warning when BaseCommit != HEAD (hooks haven't fired yet) - Soft attribution warning when BaseCommit == HEAD but AttributionBaseCommit is stale (migrate path fired, attribution not yet realigned) Status is purely read-only — no session state mutations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reports when a session's AttributionBaseCommit != BaseCommit, which indicates the migrate path fired after history movement and attribution figures may be inaccurate until the next condensation.
When a session has AttributionBaseCommit != BaseCommit (set by the migrate path after history movement), print a single stderr warning the next time the user commits. Gated by DivergenceNoticeShown flag on session state, cleared when condensation realigns the bases.
Two tests: 1. Reconcile flow: condense → pull → reset to condensed commit → reconciled 2. Migrate flow: condense → pull → reset to unrelated commit → migrate only
Move the divergence warning loop out of PrepareCommitMsg into a separate helper method to reduce cyclomatic complexity below the maintidx threshold. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the entire doctor attribution-divergence check and the reset_linkage_test.go integration tests to keep the PR focused on the core reconcile guard + warning surfaces. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix misplaced doc comment: warnIfAttributionDiverged and tryAgentCommitFastPath now have correctly separated doc comments - Use stderrWriter instead of os.Stderr in warnIfAttributionDiverged for testability (matches existing warnStaleEndedSessions pattern) - Add logging.Warn when CommitObject fails in reconcile guard (should never happen, but makes the fallthrough to migrate visible in logs for diagnostics) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes a regression from the prior ordering (migrate → clear → attribution) where the normal migrate path (pull/rebase) would cause attribution to use the post-migration BaseCommit as the base tree, undercounting agent lines. The new ordering preserves the original attribution behavior while still letting the reconcile guard read LastCheckpointID before it's cleared. Sequence: attribution (old BaseCommit) → migrate/reconcile → clear Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…support # Conflicts: # cmd/entire/cli/status.go
Addresses three issues surfaced by adversarial review of the reset-linkage feature: 1. `InitializeSession` captured PendingPromptAttribution against the stale pre-reset base before calling migrateShadowBranchIfNeeded. On the reconcile path (reset-to-known-checkpoint), reconcile advances BaseCommit + AttributionBaseCommit to HEAD — but the already-captured attribution came from the discarded-history tree, so edits from the abandoned segment would be misattributed on the next checkpoint. migrateShadowBranchIfNeeded now returns a reconciled bool; callers recompute attribution when it fires. 2. DivergenceNoticeShown was only cleared on condensation, not on reconcile or the post-commit base-advance paths (updateBaseCommitIfChanged, postCommitUpdateBaseCommitOnly). Once a session warned about divergence and then reconciled, a second divergence stayed silent until the next full condensation — defeating show-once-per-divergence semantics. Introduced State.RealignAttributionBase which couples AttributionBaseCommit with the flag clear, and routed all six realignment sites through it. Added a self-heal in NormalizeAfterLoad so pre-existing state files with a stale flag recover on load. 3. The empty-BaseCommit carve-out in computeSessionDivergenceWarnings silently hid partially-initialized sessions from `entire status`, giving operators a false-clean view at exactly the moment they needed the strongest signal. Replaced the skip with an explicit "session linkage incomplete; awaiting reinitialization" warning. Also tightened the warnIfAttributionDiverged test to reload from disk and assert a second call stays silent (previously in-memory-only), and rewrote the function docstring to drop ambiguous "show-once" framing in favor of "at most one warning per call." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The reconcile path in migrateShadowBranchIfNeeded fires when HEAD carries the session's LastCheckpointID trailer. But trailers survive cherry-pick and rebase — git copies commit messages verbatim — so a cherry-picked checkpoint commit with the same trailer but a different SHA would falsely trigger reconcile, drop the pinned AttributionBaseCommit, and corrupt attribution math for the session's uncondensed shadow-branch work. Added SessionState.LastCheckpointCommitHash, set at condensation time alongside LastCheckpointID. The reconcile path now requires currentHead to match this SHA exactly — cherry-pick/rebase creates a new SHA, so the guard routes those to the migrate path (which correctly preserves the attribution pin). Legacy state files without the new field fall back to trailer-only matching for backward compatibility. Caught by a codex /codex:review round on the full branch diff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Tested and is working. |

Summary
Adds proactive session linkage reconciliation after
git resetand passive divergence warnings when reconciliation isn't possible.When an active session's
BaseCommitno longer matches HEAD and HEAD carries the session's ownLastCheckpointIDas anEntire-Checkpointtrailer,migrateShadowBranchIfNeededsilently reconciles bothBaseCommitandAttributionBaseCommitto HEAD on every hook invocation.When HEAD moves to a commit without a matching trailer, the system surfaces a divergence warning through
entire statusand a show-onceprepare-commit-msgstderr line.What reconciliation does
migrateShadowBranchIfNeededgains a reconcile guard before the existing migrate logic. It fires when:state.BaseCommit != current HEADstate.LastCheckpointIDis non-emptyEntire-Checkpointtrailer matchesstate.LastCheckpointIDWhen matched: both
BaseCommitandAttributionBaseCommitupdate to HEAD. The old shadow branch is left untouched (preserves rewind data as a safety net).When not matched: the existing migrate path runs unchanged (updates
BaseCommitonly, renames shadow branch, leavesAttributionBaseCommitpinned).Warning surfaces
When the migrate path fires without reconciliation,
AttributionBaseCommit != BaseCommit. Two surfaces notify the user:entire status— renders a yellow warning per session card (read-only, no state mutations)prepare-commit-msg— prints a single stderr line on next commit, gated byDivergenceNoticeShownflag (cleared when condensation realigns the bases)Verified scenarios
Scenario A: Reset to known checkpoint (reconcile, no warning)
Scenario B: Reset to unrelated commit (migrate, warning fires)
Known tradeoffs
Attribution ordering: optimized for common case
There is no single ordering of
attributionvsmigrateinUserPromptSubmitthat is correct for both the normal migrate path (pull/rebase) and the reconcile path (reset to checkpoint):migrate → attributionattribution → migrate(chosen)We chose
attribution → migrate → clearbecause normal migrate is the common case. The reconcile-at-turn-start scenario is rare (requires condensation → HEAD moves → reset → new prompt, all within theLastCheckpointIDwindow). Additionally, reconcile fires more often mid-turn viaSaveStep/SaveTaskStepwhere this ordering does not apply (attribution is only calculated atUserPromptSubmit). The next condensation realigns everything regardless.Old shadow branch unreachable via
GetRewindPointsAfter reconcile updates
state.BaseCommitto HEAD,GetRewindPoints(keyed byBaseCommit) no longer finds checkpoints on the old shadow branch. This is intentional: the user reset to discard that work, so those checkpoints should not appear in normal rewind. The branch is preserved as a git-level safety net (recoverable viagit branch -a | grep entire/), not as active rewind data. A dedicatedentire recovercommand would be the right home for surfacing discarded-segment checkpoints.Other known limitations (follow-up PRs)
git commit --amendimmediately after reset (before any hook fires): The amend hook checksstate.BaseCommit == HEADwhich is stale until reconcile runs. Window is narrow — any prompt or tool call triggers reconcile first.FilesTouchedafter reconcile: Includes files from the discarded segment. Low severity — does not cause data corruption, worst case is a minimal/empty checkpoint on next condensation.Changes
strategy/manual_commit_migration.gomigrateShadowBranchIfNeededstrategy/manual_commit_hooks.gowarnIfAttributionDivergedhelperstrategy/manual_commit_condensation.goDivergenceNoticeShownon condensationsession/state.goDivergenceNoticeShownfieldstatus.gocomputeSessionDivergenceWarnings(read-only)testutil/testutil.goCreateBranch,GitResethelpersTest plan
manual_commit_migration_test.go)status_test.go)mise run fmt && mise run lint— 0 issuesmise run test:ci— all pass (unit + integration + E2E canary 56/56)🤖 Generated with Claude Code