Skip to content

fix(strategy): sync checkpoints to a single elected remote (ENT-1451) - #1893

Merged
peyton-alt merged 10 commits into
mainfrom
fix/checkpoint-single-remote
Aug 7, 2026
Merged

fix(strategy): sync checkpoints to a single elected remote (ENT-1451)#1893
peyton-alt merged 10 commits into
mainfrom
fix/checkpoint-single-remote

Conversation

@peyton-alt

@peyton-alt peyton-alt commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/967

Problem

In a repo with multiple remotes (e.g. origin and publish), 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:

Change

Checkpoint data now syncs to exactly one elected remote, identical on both backends:

  • Election precedence (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>.pushRemoteremote.pushDefaultbranch.<name>.remote; dangling tracking entries are skipped, not fail-closed) → origin → the sole remote → the first remote in .git/config order. The tracking tier fixes the fork setup where origin is 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.
  • Pre-push gate: pushes to any other remote — including raw-URL pushes — carry no checkpoint data and skip the policy sync/OPF work entirely. The dedicated checkpoint_remote URL store is exempt (it's addressed directly, not selected by the push).
  • git-refs queue: only drained by pushes to the elected remote, fixing the first-push-wins loss (git-refs: push queue is drained by the first successful push — checkpoints never reach a second remote #1635).
  • doctor migrate-checkpoints: --remote now defaults to the elected remote instead of hardcoded origin; explicit --remote still wins.
  • Observability (entire status, text + --json): Checkpoints sync to: origin destination line (with source annotation), an unpushed-checkpoint counter, and a checkpoint_sync_error surface 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_remote is the escape hatch for choosing which remote. Should be called out in the next release's changelog.

Testing

  • Unit: resolver precedence table (incl. fail-closed, pushurl-only invisibility, config-order-beats-alphabetical), gate, migrate helper, counter (both backends), status text/JSON.
  • Integration: 6 tests / 11 subtests across both backends with remote-side assertions on bare repos — default election, config override, fail-closed (through the real installed pre-push hook), raw-URL push, status before/after, dedicated-store exemption (HTTPS fixture). Negative-control verified: reverting the gate makes the matrix fail.
  • Full mise run check green (unit + integration -race + Vogon canary).

Notes for reviewers

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


[!NOTE] Medium Risk
Changes where session transcripts are pushed in multi-remote repos (intentional privacy fix); misconfigured checkpoint_push_remote disables sync until fixed, though status surfaces the failure.

Overview**
**Checkpoint data now syncs to one elected git remote instead of following every git push target. Election uses strategy_options.checkpoint_push_remote (errors if the name is missing — no fallback), then origin, sole remote, or first remote in .git/config order.

The pre-push hook skips checkpoint/OPF work unless the push targets that remote (or uses dedicated checkpoint_remote URL mode). Other remotes and raw-URL pushes leave git-refs queue entries intact.

entire status (text and --json) reports sync destination, source, fail-closed errors, and a local unpushed checkpoint count (CountUnpushedCheckpoints: v1 ahead-of-tracking on git-branch, push-queue Peek on git-refs).

doctor migrate-checkpoints defaults --remote to the elected sync remote instead of origin. GetCheckpointPushRemote reads the new setting. Docs and integration tests cover both backends.

Reviewed by Cursor Bugbot for commit 03abd8d. Configure here.

peyton-alt and others added 4 commits August 3, 2026 16:23
Precedence: checkpoint_push_remote setting (fail-closed on a name that is
not a configured remote), origin, sole remote, first remote in .git/config
order. Groundwork for ENT-1451 / #1634 / #1635.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 01KZ4MQ95RE8RD5TA91B2HGV3A
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
@peyton-alt
peyton-alt requested a review from a team as a code owner August 3, 2026 23:12
Copilot AI review requested due to automatic review settings August 3, 2026 23:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_remoteorigin → sole remote → first in .git/config order) 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-checkpoints defaults to the elected remote; git-refs push-queue gains a read-only Peek; 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.

Comment thread cmd/entire/cli/status.go Outdated
peyton-alt and others added 5 commits August 3, 2026 19:32
…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>
@peyton-alt
peyton-alt enabled auto-merge August 7, 2026 22:10
@peyton-alt
peyton-alt merged commit caa0c9b into main Aug 7, 2026
12 checks passed
@peyton-alt
peyton-alt deleted the fix/checkpoint-single-remote branch August 7, 2026 22:10
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.

git-refs: push queue is drained by the first successful push — checkpoints never reach a second remote

4 participants