fix(strategy): sync checkpoints to a single elected remote (ENT-1451) - #1893
Merged
Conversation
Pre-push gate: pushes to any other remote (including raw URLs) no longer carry checkpoint data, on both the git-branch and git-refs backends. The git-refs push queue is now only drained by pushes to the checkpoint sync remote, fixing the first-push-wins loss (#1635). doctor migrate-checkpoints defaults its --remote to the resolver instead of hardcoded origin. Fixes ENT-1451, #1634 (write side), #1635. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZ4NW6GCBC9N4QSGP0HN2S8B
The observability half of the single-remote gate: status names the elected checkpoint sync remote (or the dedicated checkpoint_remote store, or the fail-closed misconfiguration) and counts checkpoints not yet on it, in text and --json. Also promotes the addRemote / checkpoint_push_remote settings test helpers into testutil (third call site) and adds a read-only PushQueue.Peek for the git-refs counter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZ4VFR6CCPTGJ80C9GJSGSW6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZ4WXHGHHW8DEVZ82VEBZ24E
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes multi-remote checkpoint syncing by ensuring checkpoint metadata (including transcripts) is pushed to exactly one elected git remote across both checkpoint backends, preventing accidental leakage to non-intended remotes. It adds a shared remote-election resolver, gates pre-push checkpoint work to the elected remote (with a dedicated checkpoint_remote exemption), and improves observability via entire status and supporting counters.
Changes:
- Add single-remote election (
strategy_options.checkpoint_push_remote→origin→ sole remote → first in.git/configorder) and enforce it in the pre-push hook for both git-branch and git-refs backends. - Add local-only observability:
entire status(text +--json) now reports checkpoint sync destination/source, fail-closed errors, and an “unpushed checkpoints” approximation. - Update doctor + plumbing/tests:
doctor migrate-checkpointsdefaults to the elected remote; git-refs push-queue gains a read-onlyPeek; unit + integration coverage added.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/architecture/sessions-and-checkpoints.md | Documents the new single-remote checkpoint sync behavior and exemption rules. |
| CLAUDE.md | Updates strategy behavior documentation with single-remote sync semantics. |
| cmd/entire/cli/testutil/testutil.go | Adds test helpers for remotes, settings, and ref updates used by new tests. |
| cmd/entire/cli/strategy/unpushed_checkpoints.go | Implements local-only unpushed checkpoint counting for both backends. |
| cmd/entire/cli/strategy/unpushed_checkpoints_test.go | Unit tests for unpushed checkpoint counting behavior. |
| cmd/entire/cli/strategy/manual_commit_push.go | Adds the single-remote pre-push gate to prevent syncing to non-elected remotes. |
| cmd/entire/cli/strategy/manual_commit_opf_prompt_test.go | Adjusts OPF prompt test setup to satisfy the new gate with a hermetic remote. |
| cmd/entire/cli/strategy/checkpoint_sync_remote.go | Introduces resolver + gate predicate for electing/allowing checkpoint sync remotes. |
| cmd/entire/cli/strategy/checkpoint_sync_remote_test.go | Unit tests covering election precedence and gating semantics. |
| cmd/entire/cli/status.go | Adds shared checkpoint-sync info computation + text/JSON status surfaces. |
| cmd/entire/cli/status_test.go | Tests new status text/JSON output for elected/dedicated/fail-closed cases and counters. |
| cmd/entire/cli/settings/settings.go | Adds GetCheckpointPushRemote() accessor for the new setting. |
| cmd/entire/cli/settings/settings_test.go | Unit tests for GetCheckpointPushRemote(). |
| cmd/entire/cli/integration_test/checkpoint_sync_remote_test.go | End-to-end integration matrix asserting single-remote routing across both backends. |
| cmd/entire/cli/doctor_migrate.go | Defaults migrate push target to the elected sync remote (fail-closed on misconfig). |
| cmd/entire/cli/doctor_migrate_test.go | Unit tests for migrate remote resolution logic. |
| cmd/entire/cli/checkpoint/pushqueue.go | Adds Peek() for non-mutating queue inspection (status/counters). |
| cmd/entire/cli/checkpoint/pushqueue_test.go | Tests that Peek() is read-only and de-duplicates without compacting. |
…tion succeeds Status previously showed "dedicated checkpoint remote" whenever the checkpoint_remote setting parsed, but the pre-push gate only exempts the dedicated store when remote.PushURL derives an eligible URL (owner match, parseable transport). Probe PushURL for the elected sync remote — a local-only call — so status reflects what the next push actually does. Addresses Copilot review on #1893. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZ506H9FY3DTJ01X4PHBYMTM
… election A corrupt settings file may contain a checkpoint_push_remote we cannot read; falling through to default election could re-route checkpoint data away from the remote the user configured for isolation. Unreachable via the pre-push hook today (IsSetUpAndEnabled already bails on load errors), but the resolver is an exported API and the inconsistency with the misconfigured-name fail-closed path was real. Addresses trail finding 019fc9fe on #1893. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZ50JQXPBNG8WF0HH0CQ30SJ
Origin-first election strands checkpoints when origin is a repo the user cannot push (cloned base + added fork): origin wins, is never pushed, and every checkpoint stays local. Mirror git's own push resolution instead — branch.<name>.pushRemote, remote.pushDefault, branch.<name>.remote — then fall back to origin/sole/first. Dangling tracking entries are skipped, not fail-closed: they are git state, not user intent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…otes A developer's real global gitconfig (e.g. remote.pushDefault) leaked into the in-process election tests. Production reads effective config on purpose; tests now pin behavior under isolated config. Also: neutral gate warn message, restored two accepted-divergence notes in status, gate-level fail-closed subtest, HEAD-based election caveat in docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Electing the branch's push destination (branch.<name>.pushRemote -> remote.pushDefault -> branch.<name>.remote) turned every push to a different remote into a silent no-op, because the elected remote is compared against the remote of the push actually being made. Any repo where those differ lost checkpoint sync entirely: `git push <other> HEAD`, a `git clone -o base` whose checkpoints go to a separately added origin, anything with remote.pushDefault set. TestAlternates_RelativeObjectAlternate_CheckpointSync caught it — it clones with `-o base` and pushes checkpoints to `origin`, so the tracking tier elected `base`, the gate rejected the `origin` push, and the pre-push hook produced no output and synced nothing. That is the git-branch canary failure on this PR; it passes with the tier removed. The tier also elected a remote the read paths cannot see: resume and explain resolve checkpoints through origin's remote-tracking refs, so checkpoints routed to an implicitly elected non-origin remote could not be read back from the same clone. Precedence is now checkpoint_push_remote -> origin -> sole remote -> first in .git/config order. The fork setup the tier was meant to serve (origin is an unpushable base repo, you push to your own fork) is served by naming the fork in checkpoint_push_remote, which is also the only form of it where the checkpoints can be read back. Tests: the three tracking-precedence unit tests collapse into TestResolveCheckpointSyncRemote_TrackingConfigDoesNotDecide, a table asserting each of the three git config keys leaves origin elected. The dangling-tracking and detached-HEAD tests go with them — with no tracking tier they assert the same thing. At integration level ForkSetup_TracksNonOriginRemote inverts into BranchTrackingDoesNotReroute. unsetBranchTracking existed only to defeat the tier and is removed with its two call sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
computermode
approved these changes
Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://entire.io/gh/entireio/cli/trails/967
Problem
In a repo with multiple remotes (e.g.
originandpublish), pushing to any remote synced checkpoint data to it — leaking session transcripts onto remotes where they don't belong. The two storage backends failed in mirror-image ways:entire/checkpoints/v1, so every remote received full transcripts (Checkpoint reads always target origin while pushes follow the hook remote (non-origin remotes unsupported) #1634, write side)Change
Checkpoint data now syncs to exactly one elected remote, identical on both backends:
strategy.ResolveCheckpointSyncRemote):strategy_options.checkpoint_push_remote(a plain remote name; fail-closed — a name that isn't a configured remote disables sync with a warning rather than falling back) → the current branch's own push destination, mirroring git's push resolution (branch.<name>.pushRemote→remote.pushDefault→branch.<name>.remote; dangling tracking entries are skipped, not fail-closed) →origin→ the sole remote → the first remote in.git/configorder. The tracking tier fixes the fork setup whereoriginis a base repo the user cannot push (cloned base + added fork): the branch's tracking remote wins, so checkpoints sync to the fork instead of stranding locally.checkpoint_remoteURL store is exempt (it's addressed directly, not selected by the push).doctor migrate-checkpoints:--remotenow defaults to the elected remote instead of hardcodedorigin; explicit--remotestill wins.entire status, text +--json):Checkpoints sync to: origindestination line (with source annotation), an unpushed-checkpoint counter, and acheckpoint_sync_errorsurface for the fail-closed case — so the behavior change is visible, not silent.Behavior change
Multi-remote repos where a secondary remote previously received checkpoints stop getting them. That was the bug for most users (transcripts on a public remote are unrecoverable; a deferred sync is not), and
checkpoint_push_remoteis the escape hatch for choosing which remote. Should be called out in the next release's changelog.Testing
mise run checkgreen (unit + integration-race+ Vogon canary).Notes for reviewers
TestGitRefsQueue_TwoRemotesQueueClearedByFirstPushwith a flip-me comment; when that stack rebases onto this, the assertion flips (this PR's integration matrix already pins the new semantics).origin(coherent with the default election); routing them through the resolver is the Checkpoint reads always target origin while pushes follow the hook remote (non-origin remotes unsupported) #1634 read-side follow-up.Fixes ENT-1451. Closes #1635. Refs #1634 (write side).
Trail: https://entire.io/gh/entireio/cli/trails/967 — full session history (spec, plan, and per-commit checkpoints) for this change.
🤖 Generated with Claude Code