Skip to content

feat: handle git reset in session hooks#948

Merged
gtrrz-victor merged 23 commits intomainfrom
peyton/reset-linkage-support
Apr 18, 2026
Merged

feat: handle git reset in session hooks#948
gtrrz-victor merged 23 commits intomainfrom
peyton/reset-linkage-support

Conversation

@peyton-alt
Copy link
Copy Markdown
Contributor

@peyton-alt peyton-alt commented Apr 14, 2026

Summary

Adds proactive session linkage reconciliation after git reset and passive divergence warnings when reconciliation isn't possible.

When an active session's BaseCommit no longer matches HEAD and HEAD carries the session's own LastCheckpointID as an Entire-Checkpoint trailer, migrateShadowBranchIfNeeded silently reconciles both BaseCommit and AttributionBaseCommit to HEAD on every hook invocation.

When HEAD moves to a commit without a matching trailer, the system surfaces a divergence warning through entire status and a show-once prepare-commit-msg stderr line.

What reconciliation does

migrateShadowBranchIfNeeded gains a reconcile guard before the existing migrate logic. It fires when:

  1. state.BaseCommit != current HEAD
  2. state.LastCheckpointID is non-empty
  3. HEAD's Entire-Checkpoint trailer matches state.LastCheckpointID

When matched: both BaseCommit and AttributionBaseCommit update 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 BaseCommit only, renames shadow branch, leaves AttributionBaseCommit pinned).

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 by DivergenceNoticeShown flag (cleared when condensation realigns the bases)

Verified scenarios

Scenario A: Reset to known checkpoint (reconcile, no warning)

After condensation:      BaseCommit=96a8d42  AttributionBase=96a8d42  ALIGNED
After migration (pull):  BaseCommit=df45e26  AttributionBase=96a8d42  DIVERGED
After reset + reconcile: BaseCommit=96a8d42  AttributionBase=96a8d42  ALIGNED ✅
Old shadow branch entire/df45e26-e3b0c4 preserved ✅

Scenario B: Reset to unrelated commit (migrate, warning fires)

After condensation:       BaseCommit=fca4f11  AttributionBase=fca4f11  DivNotice=false  ALIGNED
After migration (pull):   BaseCommit=35fe407  AttributionBase=fca4f11  DivNotice=false  DIVERGED
After unrelated reset:    BaseCommit=73a8938  AttributionBase=fca4f11  DivNotice=false  DIVERGED
After prepare-commit-msg: BaseCommit=73a8938  AttributionBase=fca4f11  DivNotice=true   DIVERGED
  → stderr: "entire: session attribution diverged after recent history movement;
             figures may be off until next checkpoint" ✅

Known tradeoffs

Attribution ordering: optimized for common case

There is no single ordering of attribution vs migrate in UserPromptSubmit that is correct for both the normal migrate path (pull/rebase) and the reconcile path (reset to checkpoint):

Ordering Normal migrate (common) Reconcile at turn start (rare)
migrate → attribution ❌ Undercounts agent lines (base tree includes pulled content) ✅ Uses reconciled base
attribution → migrate (chosen) ✅ Uses original base tree ❌ Uses stale shadow branch

We chose attribution → migrate → clear because normal migrate is the common case. The reconcile-at-turn-start scenario is rare (requires condensation → HEAD moves → reset → new prompt, all within the LastCheckpointID window). Additionally, reconcile fires more often mid-turn via SaveStep/SaveTaskStep where this ordering does not apply (attribution is only calculated at UserPromptSubmit). The next condensation realigns everything regardless.

Old shadow branch unreachable via GetRewindPoints

After reconcile updates state.BaseCommit to HEAD, GetRewindPoints (keyed by BaseCommit) 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 via git branch -a | grep entire/), not as active rewind data. A dedicated entire recover command would be the right home for surfacing discarded-segment checkpoints.

Other known limitations (follow-up PRs)

  • git commit --amend immediately after reset (before any hook fires): The amend hook checks state.BaseCommit == HEAD which is stale until reconcile runs. Window is narrow — any prompt or tool call triggers reconcile first.
  • Stale FilesTouched after 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

File What
strategy/manual_commit_migration.go Reconcile guard in migrateShadowBranchIfNeeded
strategy/manual_commit_hooks.go Reorder UserPromptSubmit (attribution → migrate → clear) + warnIfAttributionDiverged helper
strategy/manual_commit_condensation.go Clear DivergenceNoticeShown on condensation
session/state.go Add DivergenceNoticeShown field
status.go Passive computeSessionDivergenceWarnings (read-only)
testutil/testutil.go Add CreateBranch, GitReset helpers

Test plan

  • 6 unit tests for reconcile/migrate paths (manual_commit_migration_test.go)
  • 3 status tests (status_test.go)
  • Manual end-to-end verification of both scenarios with real CLI binary
  • mise run fmt && mise run lint — 0 issues
  • mise run test:ci — all pass (unit + integration + E2E canary 56/56)

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 14, 2026 04:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 HEAD and parse Entire-Checkpoint trailer(s) during entire status rendering.
  • Reconcile active session BaseCommit/AttributionBaseCommit only when HEAD checkpoint matches LastCheckpointID; 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.

Comment thread cmd/entire/cli/status.go
Comment thread cmd/entire/cli/status.go
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread cmd/entire/cli/status.go
@peyton-alt peyton-alt marked this pull request as ready for review April 15, 2026 17:36
@peyton-alt peyton-alt requested a review from a team as a code owner April 15, 2026 17:36
@peyton-alt peyton-alt changed the title fix: detect reset-driven session divergence in status fix: preserve session linkage after git amend and rebase Apr 15, 2026
@peyton-alt peyton-alt changed the title fix: preserve session linkage after git amend and rebase fix: detect reset-driven session divergence in status Apr 15, 2026
@peyton-alt peyton-alt changed the title fix: detect reset-driven session divergence in status fix: reconcile session linkage after git reset Apr 15, 2026
peyton-alt and others added 15 commits April 15, 2026 19:32
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>
@peyton-alt peyton-alt changed the title fix: reconcile session linkage after git reset feat: handle git reset in session hooks Apr 16, 2026
…support

# Conflicts:
#	cmd/entire/cli/status.go
peyton-alt and others added 3 commits April 17, 2026 11:37
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>
@gtrrz-victor
Copy link
Copy Markdown
Contributor

Tested and is working.

@gtrrz-victor gtrrz-victor merged commit 957f073 into main Apr 18, 2026
9 checks passed
@gtrrz-victor gtrrz-victor deleted the peyton/reset-linkage-support branch April 18, 2026 05:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants