Skip to content

feat(checkpoint): add entire checkpoint migrate - #2327

Open
toby wants to merge 273 commits into
toby/entire-remote-electionfrom
toby/checkpoint-sync
Open

toby wants to merge 273 commits into
toby/entire-remote-electionfrom
toby/checkpoint-sync

Conversation

@toby

@toby toby commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

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

Stacked on #2335 (feat(checkpoint): elect the Entire remote for checkpoint sync). Review that first; this PR's base is its branch and will be retargeted to main once it merges. Best reviewed commit by commit: the four commits are layered so each builds and is reviewable on its own.

Why

Once an Entire remote is elected (#2335), new checkpoints flow there, but the ones already pushed to GitHub stay behind. This PR brings that backlog over, verifies it, and cleans up the old remote with consent, then rewards the user with what they have accumulated.

Commits

  1. feat(strategy): checkpoint artifact migration engine — inventory, hydrate into a tmp namespace and promote via SafelyAdvanceLocalRef, requeue, verify at the local hash, chunked delete. Removal addresses the old remote by its single push URL so the dedicated-checkpoint_remote rewrite can never apply, and refuses fan-out remotes. batchPushRefs chunks refspecs (a backlog publish overran ARG_MAX); PushQueue.EnqueueAll; remote.DeleteRefs (trail cleanup reuses it); the ls-remote parser moves into the checkpoint package. Review question: can anything be deleted that was not verified? Never force-pushed?
  2. feat(checkpoint): migration planner, copy and value summary — the pure planner over the repo's state (no remotes, dedicated store, fail-closed setting, choice among remotes, pinned elsewhere, already home, publish only, migrate, convert first) and every user-facing string in one file; the local value block; formatTokenCount gains M. Review question: are the states and the copy right?
  3. feat(checkpoint): add entire checkpoint migrate — the command. Two planning passes (local facts, then ls-remote inventories), pickers whose first option is always no-change, --to recorded in .entire/settings.local.json only when the election would not already pick it, git-branch repos converted to refs and flipped, publish through the OPF- and policy-gated queue flush, verify, then delete only verified refs and only with consent (--remove when non-interactive; --yes never implies it). Bare non-interactive runs report and change nothing. Classified user-owned and unlisted in agent-help. Review question: is the non-interactive contract safe for agents?
  4. feat(status): surface the checkpoint migration in status, doctor, enable and docs — the network-free status nudge and checkpoint_sync_migration JSON field, doctor's destination check, the re-enable pointer, the command name restored in the announcement, hint and topology advice, and the docs.

Decisions for reviewers

  • checkpoint migrate is user-owned and unlisted in agent-help: it pushes transcripts, deletes remote refs and writes settings.
  • The backend flip writes the project .entire/settings.json (a repo-wide format decision) and prints a commit hint; it is skipped under ENTIRE_CHECKPOINTS_PRIMARY.
  • The ledger records done once data is verified on the destination, and declined when cleanup is refused; both silence the status nudge.
  • No prompt inside git push; the hook announces and points at the command. Offering the migration from repo mirror use, which creates the remote, is a natural follow-up.

Testing

  • mise run lint: 0 issues. Full unit and integration suites (both backends) and the Vogon canary pass on the stack.
  • New: 14 engine tests over two local bares, 30-row planner table, executor tests against a fake engine (verify-missing stops before remove, remove failure keeps the ledger honest, decline records declined, --yes never removes, --dry-run only lists), status rendering tests, and seven end-to-end checkpoint migrate scenarios on both backends.
  • Copilot's earlier comment that for i := range count does not compile is incorrect (Go 1.22+; this module is Go 1.26) and its suggested patch would not compile; the test passes.

computermode and others added 17 commits September 4, 2026 10:25
Entire-Checkpoint: 01M1Q649KPEKJVEYBQEYHVSWAQ
E2E_TIMEOUT is documented in CLAUDE.md and e2e/README.md as the per-prompt
timeout, but only opencode read it — so on the other nine runners the variable
moved nothing. Two of the ten also disagreed with the documented 2m default
(cursor 90s; codex, copilot-cli and gemini 60s), and five accepted
WithPromptTimeout and then dropped it, which quietly made the per-test
overrides in split_commits_test.go and multi_session_test.go no-ops for half
the matrix.

Each runner had open-coded the same precedence chain, and the copies drifted.
Resolution now lives in agent.go: promptTimeout for the runners that need the
duration (cursor derives an absolute deadline, codex/copilot-cli/gemini a
separate promptCtx), and boundPrompt for those that just want a bounded ctx.
TestEveryRunPromptResolvesThroughPromptTimeout fails the build on a runner that
resolves its own, since both failure modes are invisible at runtime — you get a
run that ignores the budget you set.

No agent's current default changes. claude-code, droid, pi, vogon and
roger-roger get no per-prompt default, keeping the ForEachAgent scenario
timeout as their only deadline; imposing the documented 2m on them would be a
new and tighter bound, which is a behaviour change no free test leg can
validate. E2E_TIMEOUT and WithPromptTimeout now bound them when asked.

A malformed E2E_TIMEOUT is now an error rather than a silent fall back to the
default: ignoring it is how you widen a budget, watch the run fail at the old
ceiling, and blame the agent.

The docs said "default: 2m", which was true of one runner. They now name the
per-runner defaults and say which runners have none.

Verified: 10727 unit + 562 integration + 60 canary tests pass; lint clean;
GOOS=windows vet clean. Behaviourally, on the free canary leg —
E2E_TIMEOUT=1ms kills a vogon and roger-roger prompt in 0.2s where it
previously did nothing, E2E_TIMEOUT=4min now reports the typo instead of
ignoring it, and E2E_TIMEOUT=3m passes. The guard test was mutation-checked by
reverting cursor, pi and droid in turn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M1XB5X07Y4DNVR3MX1S1YDM0
Every existing E2E workflow tests a binary built from source in the same
checkout, so nothing exercises the artifact users actually install. This
adds one scheduled workflow that installs the published nightly the way a
user would — brew cask entire@nightly, install.sh --channel nightly, and
install.ps1 -Channel nightly — points the existing E2E harness at it via
E2E_ENTIRE_BIN, and runs one smoke test per agent.

The test is TestMultiSessionSequential: two prompts, each committing its
own file, asserting two commits, two distinct checkpoints on HEAD and
HEAD~1, distinct sessions, and no leftover shadow branches.

The checkout ref is derived from the installed binary's own version
stamp rather than from main, so the harness and the CLI under test always
come from one commit. It fails closed if the resolved version is not a
v*-nightly.* one, since silently testing main would hide exactly the skew
the pin exists to remove.

One job per OS with a sequential per-agent loop, rather than an agent
submatrix. copilot-cli runs in its own step so the GitHub token it needs
never enters the environment of the other agent processes, preserving the
isolation e2e.yml maintains.

Windows installs through the release archive rather than Scoop: the
bucket publishes stable only, which is the path install.ps1 already takes
for a nightly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two review findings on the nightly smoke workflow.

E2E_ARTIFACT_DIR was built from $PWD inside a `shell: bash` step. On
Windows that is /d/a/cli/cli, and the value is read by the Go test binary,
which resolves the leading slash against the current drive — so reports
landed in D:\d\a\cli\cli\... and the uploaded artifact was incomplete.
Use github.workspace, the way e2e-windows.yml already does. The relative
paths in the same steps are handled by Git Bash and upload-artifact, never
by Go, so they stay as they are.

notify-slack inherited the repository's default token scope to post to an
incoming webhook, which needs no token; give it permissions: {}.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were left out of the Windows list on the assumption that they had no
Windows installer, inherited from e2e.yml's Linux-shaped install block
rather than checked. Both ship one — opencode via npm (opencode-ai), and
Factory via an official PowerShell installer at app.factory.ai/cli/windows
— and both drive prompts headlessly, so neither needs tmux.

That leaves cursor-cli as the only agent absent from Windows, for a
structural reason rather than a packaging one: its E2E driver sends every
prompt through tmux because Cursor's headless -p mode does not fire the
beforeSubmitPrompt and stop hooks.

The Factory installer drops droid in ~\bin rather than ~\.local\bin and
updates only the stored user PATH, so both directories go on GITHUB_PATH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A new workflow_dispatch workflow is not dispatchable until its file is on
the default branch, so a path-scoped pull_request trigger is the only way
to execute this before it merges. Scoped to this file the way
install-ps1-e2e.yml scopes itself. Revert before merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two failures from the first real run of this workflow.

The Linux leg never got past install: install.sh resolves the nightly tag
through the GitHub releases API, and an unauthenticated call from a shared
runner IP is 403-ed by the 60/hour limit, which the script reports as
"check your internet connection". Both installers already honour
GITHUB_TOKEN; the workflow just never passed one.

The per-agent loop was written as `set -uo pipefail` on the assumption
that errexit was off, but GitHub invokes `shell: bash` as
`bash --noprofile --norc -e -o pipefail`. So the first failing agent
aborted the step before its result line was written and skipped every
agent after it — on Windows that silently cost the copilot-cli run. An
explicit `set +e` restores the intended behaviour: every agent runs, and
the Summarize step is what fails the job.

Also drops factoryai-droid from the Windows list for now. It fails there
and only there ("checkpoint state did not advance within 30s", twice
including the rerun) while passing on macOS, so it is a real finding
rather than a workflow bug and is tracked separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
opencode's install script resolves its version with a bare
`curl -s https://api.github.com/.../releases/latest` and offers no way to
authenticate it, so on a shared runner IP it is rate-limited at random and
reports the 403 as "Failed to fetch version information". That took out
the macOS leg on a run where the same step had passed 30 minutes earlier.

npm reaches the registry instead, which is the install the Windows leg
already uses, so both now agree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pull_request trigger existed only to execute this workflow before it
could be dispatched, which needs the file on the default branch. It has
done its job: run 34131195807 is green on all three OSes.

The factoryai-droid exclusion on Windows now carries its evidence rather
than a TEMP note — the failure mode, the two runs that measured it, and
the fact that the same agent passes on Linux and macOS. Its installer stays
in place so restoring it is a one-word change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ved last

`entire session current` answered "which session state moved most recently, preferring this worktree" and described it as "the active session for the current worktree". Worktrees share one session store, so in a worktree with no sessions of its own it silently fell back across the whole store and returned a live session belonging to somewhere else — that worktree's path right there in the JSON, indistinguishable from a real answer.

That is not a cosmetic mislabel. Agents reach for this command to find their own session id, read the id back, and pass it to `entire session adopt`, which moves the named session into the current worktree and resets its checkpoint bookkeeping. A wrong id there mutates a third party's running session.

Five agents already publish their session id into the environment of the processes they spawn, and Entire read none of them. `strategy.ResolveCallerSession` now answers the question directly, in tiers, each reported back so a caller can tell an identification from a guess: `caller-env` (the agent named it), `ancestry` (a session's recorded Owner is our ancestor — the same match commit linking performs, which is what covers the agents that publish nothing), `worktree`, then `other-worktree`. The weak tiers stay, because "what has been happening in this repo" is a real question; they just have to say that is what they answered. `SessionResolution.IsCaller()` is the gate for acting on a session rather than displaying it.

Three call sites, all of which were asking the caller question: `session current`, a bare `session tokens`, and the hook log-stamp in `withHookSession` — an agent-triggered git hook inherits the agent's variable, so log lines now name the session that ran the commit instead of whichever one moved last. `FindMostRecentSession` is untouched and serves as the bottom tiers.

The per-agent half is declarative: an agent implements `CallerSessionIdentifier` by naming its variable, and the `agent` package does the read, so the id is validated with `validation.ValidateAgentSessionID` in exactly one place. That is load-bearing rather than tidy — the id becomes a path component in `ResolveSessionFile`, so an unvalidated one out of the environment is a traversal sink. Declaring the name instead of the value also makes the set enumerable, which is what lets tests clear it.

Every variable was established against the shipped agent rather than inferred: Claude Code `CLAUDE_CODE_SESSION_ID`, Codex `CODEX_SESSION_ID` (the root-session identity — not `CODEX_THREAD_ID`, which follows forks and subagent threads), Cursor `CURSOR_CONVERSATION_ID`, Copilot CLI `COPILOT_AGENT_SESSION_ID`, pi `PI_SESSION_ID`. Gemini CLI and opencode publish nothing, which is a finding rather than a gap: Gemini passes its session id only to its own background-process bookkeeping, and opencode's shell tool augments no environment at all. That is why the ancestry tier is not optional.

Two states are kept distinct on the caller tier. An id the agent named but Entire holds no state for is reported as a diagnosis — "you are in session X and Entire is not recording it" — rather than falling through and answering with an unrelated session. And several claims at once is the normal nested case (a `codex exec` under Claude Code sees both variables), resolved by ancestry depth, then by recency.

The test harnesses now clear these variables, and that is not incidental: three existing tests failed while writing this because the developer's own agent session leaked into a spawned binary, which is the same class of failure the config/cache isolation in both TestMains exists to prevent. Derived from the registry so it cannot rot when an agent gains the capability.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M20MVC7XPS7EPSHR637S4EVX
Show installation progress and announce the forwarded command. Update the terminal renderer so completed confirmation prompts are cleared, with regression coverage for terminal cleanup and plugin install/dispatch behavior.

Entire-Checkpoint: 01M211S73J1V1YPNB123826DE8
…e-synchronous-mirror-create-path-and-the

CLI: remove synchronous mirror creation
The context name was a required argument, so switching logins meant
running `entire auth contexts` first and copying a name out of it. The
argument is now optional: with no argument, `entire auth use` shows the
saved contexts and asks which one to switch to.

Each row carries the name, handle, and login server, the current context
is marked "(active)", and the cursor starts on it. Picking one calls the
same auth.SetCurrentContext the positional form always did, so one
context stays active at a time. Ctrl+C/Esc prints "Switch cancelled."
and leaves current_context alone.

Follows selectPlacement in repo_clone.go (the picker behind `repo clone`
and `repo mirror use`): resolve directly when there is only one
candidate, prompt only when there is a real choice, and fail fast with a
pointer to the non-interactive form when there is no terminal. Uses
NewAccessibleForm so ACCESSIBLE=1 renders numbered text choices.

With several logins and no terminal it errors rather than hanging or
choosing for anyone, naming every candidate and how to pick one, so an
agent can still complete the workflow with `entire auth contexts` plus
`entire auth use <name>`.

The picker reads auth.StoredContexts, not auth.Contexts: `use` writes
current_context, so the stored pointer is what it replaces, and
resolving the effective identity instead fails outright on a dangling
--context/$ENTIRE_CONTEXT — one of the situations someone runs this
command to get out of.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M217XH923S9HQYGWPMME18A5
Copilot AI lite review requested due to automatic review settings September 8, 2026 19:45
@toby
toby requested a review from a team as a code owner September 8, 2026 19:45

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.

🟡 Changes recommended

A newly added test has a Go compilation error (for i := range count) that will fail builds until corrected.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds a new entire checkpoint sync command and extends checkpoint remote election with an “Entire remote” tier so checkpoints reliably land on a single intended destination (especially in multi-remote repos). It also introduces migration plumbing to move existing checkpoint backlogs between remotes (with verification + consented deletion), plus supporting UX updates in status/doctor/docs and a few git-utility improvements.

Changes:

  • Add entire checkpoint sync (planner + migration helpers) and document how it moves/backfills checkpoint refs/branches across remotes.
  • Add an “Entire remote” election tier (entire://…) and pre-push redirection so checkpoints sync to the elected Entire remote even when pushing code elsewhere.
  • Improve robustness for large backlogs (ref push chunking, push-queue bulk enqueue, remote ref deletion helper), and surface migration state in status/doctor.
File summaries
File Description
README.md Documents checkpoint sync, updates remote-election tier list and settings guidance.
docs/architecture/sessions-and-checkpoints.md Describes the new Entire tier behavior and migration flow details.
cmd/entire/cli/trail_cmd.go Reuses checkpoint remote deletion helper for branch cleanup.
cmd/entire/cli/strategy/refs_push_test.go Adds coverage for chunked ref pushing (large backlog).
cmd/entire/cli/strategy/refs_push_destination.go Updates multi-push-URL warning text to point at the correct setting/flow.
cmd/entire/cli/strategy/push_common.go Chunks batch pushing of checkpoint refs to avoid ARG_MAX overflows.
cmd/entire/cli/strategy/manual_commit_push.go Adds Entire-tier redirect/announce behavior; makes ref-push progress writer swappable.
cmd/entire/cli/strategy/git_remote_cache.go Caches remotes with raw URLs to support Entire-tier detection efficiently.
cmd/entire/cli/strategy/git_remote_cache_test.go Updates cache tests for the new cached remote snapshot shape.
cmd/entire/cli/strategy/checkpoint_sync_entire.go Implements Entire-tier redirect and per-clone state (announce once + migration ledger).
cmd/entire/cli/strategy/checkpoint_sync_capture.go Prevents push-habit capture from displacing the Entire tier.
cmd/entire/cli/strategy/checkpoint_remote.go Adds syncRemote to push settings and centralizes “push target” selection.
cmd/entire/cli/status.go Annotates Entire-tier destination and surfaces migration ledger in text/JSON output.
cmd/entire/cli/status_test.go Extends token formatting tests for the new M suffix behavior.
cmd/entire/cli/status_style.go Updates token formatting to support M suffix.
cmd/entire/cli/status_checkpoint_sync_test.go Adds tests for Entire-tier status annotation + migration field behavior.
cmd/entire/cli/setup.go Adds a local-only pointer to run checkpoint sync when the Entire tier is elected.
cmd/entire/cli/sessions_test.go Updates expected token formatting output to match M suffix.
cmd/entire/cli/repo_mirror.go Uses gitremote.ProtocolEntire constant for scheme checks.
cmd/entire/cli/remote_topology.go Treats “Entire remote elected” as resolving multi-remote ambiguity; improves messaging.
cmd/entire/cli/remote_topology_test.go Pins corrected multi-remote advice and the positive Entire-elected line.
cmd/entire/cli/gitremote/gitremote.go Adds IsEntireURL helper for scheme-level detection.
cmd/entire/cli/gitremote/gitremote_test.go Tests IsEntireURL.
cmd/entire/cli/git_operations.go Moves ls-remote parsing into the checkpoint package for reuse.
cmd/entire/cli/doctor.go Adds checkpoint destination reporting + guidance toward checkpoint sync.
cmd/entire/cli/doctor_migrate.go Refactors queued-ref push reporting for reuse by checkpoint sync.
cmd/entire/cli/checkpoint/remote/git.go Adds DeleteRefs helper and splits push execution into runPush.
cmd/entire/cli/checkpoint/remote/delete_refs_test.go Tests remote ref deletion behavior (full + short names, absent refs, errors).
cmd/entire/cli/checkpoint/refs_naming.go Adds ls-remote parsing helpers for checkpoint refs.
cmd/entire/cli/checkpoint/refs_lsremote_test.go Tests checkpoint ls-remote parsing helpers.
cmd/entire/cli/checkpoint/pushqueue.go Adds EnqueueAll for efficient bulk re-queueing during migrations.
cmd/entire/cli/checkpoint/pushqueue_enqueueall_test.go Tests PushQueue.EnqueueAll.
cmd/entire/cli/checkpoint_sync_value.go Computes a best-effort “value” summary for checkpoint sync output.
cmd/entire/cli/checkpoint_sync_value_test.go Tests checkpoint value computation and rendering.
cmd/entire/cli/checkpoint_group.go Registers the new checkpoint sync subcommand in the checkpoint group.
cmd/entire/cli/agent_help_cmd.go Classifies checkpoint sync as user-owned and unlisted for agent-help.
Review details
  • Files reviewed: 47/47 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/entire/cli/strategy/refs_push_test.go
@toby
toby force-pushed the toby/checkpoint-sync branch from 7842066 to d47b02e Compare September 8, 2026 20:43
@toby toby changed the title feat: Add entire checkpoint sync feat(checkpoint): elect the Entire remote and add entire checkpoint migrate Sep 8, 2026
Entire-Checkpoint: 01M228EVE8JJQ9J34JESFQX7BH
peyton-alt and others added 5 commits September 8, 2026 22:08
Entire-Checkpoint: 01M22923KW09CSPHDJCH64VAJ2
The active hooks directory was the last tree Entire wrote to with bare
os.ReadFile/os.WriteFile on a joined path. A symlink at a hook path was
therefore read through and then written through: the 0o755 os.WriteFile
truncated whatever the link named and replaced it with a shell script.

Anchor a root at git's own `--git-path hooks` answer, refusing a symlink at
the directory itself; route the four hook reads through
osroot.ReadFileNoFollow; and replace the write with
jsonutil.WriteFileAtomicIn, whose rename replaces a leaf link instead of
following it. osroot.OpenFileNoFollow is deliberately not used for the
write: it rejects O_TRUNC by design, precisely to push truncating writes
onto an atomic rename.

The hooks directory needs its own anchor rather than an existing one
because core.hooksPath can name any directory, inside the worktree or well
outside it, so allowedRootBases gains an entry with that reason.

A symlinked hook is now classified as foreign rather than as absent, so it
is backed up to <hook>.pre-entire and chained to exactly as a foreign
script is. Refusing to read through someone's link must not mean silently
discarding it, and the rename preserves the link itself as the backup.

doctor grows checkGitHookSymlinks, reporting a symlinked hooks directory
(which stops installation, and is otherwise invisible: every other command
just reports the hooks absent) and a symlinked hook file (which is not an
error, only a surprise worth naming). It is a separate function from
checkAgentDirSymlinks because that one scans worktree-relative paths
through the worktree root, and core.hooksPath can point outside the
worktree.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Entire-Checkpoint: 01M1PG9XDSAJ46T4Z7WCCC7GCT
…ents

summary_generation.provider is honored from the COMMITTED
.entire/settings.json, and discoverSummaryProviderIfMissing resolved an
unregistered name by executing entire-agent-<name>'s info subcommand. One
line in a pull request was therefore enough to run a binary of the
author's choosing on everyone who pulled it and ran `entire explain` — no
prompt, no grant, and never consulting enforceExternalAgentsTrust.

Resolving by name rather than sweeping $PATH bounds that to one binary; it
does not make it zero. Gate the lookup on settings.IsExternalAgentsEnabled.

The check sits after the already-registered early return, so it lands only
on the external case: a committed "provider": "claude-code" keeps working
with external agents off, which is the ordinary configuration and has
nothing to do with plugins.

When the gate declines, the resulting error gains a line naming the grant.
"unknown summary provider" about a plugin that is plainly installed is not
something a user can act on.

Two existing tests granted external_agents from the committed project
file, which the trust gate has never honored — they only passed because
this path bypassed it. Both now grant it from the untracked local layer.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Entire-Checkpoint: 01M1PGEEDG6HXD2HD0F3Y85690
BranchExistsOnRemote passes the name to `git ls-remote` and to go-git,
BranchExistsLocally to go-git, and CheckoutBranch to `git checkout` behind
nothing but a leading-dash guard. All three take names from CLI input or
from a trail's branch field.

The dash guard was the narrowest part of the problem: `git checkout` also
reads `--`, a pathspec, and `@{-1}`. ValidateBranchName covers those, and
it still admits an object id, so CheckoutBranch's documented "or commit"
behaviour survives even though no caller uses it.

Validating both existence helpers rather than only the remote one keeps
their answers symmetric: they are called side by side on the same name,
and "invalid" from one with "no such branch" from the other is worse than
either.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Entire-Checkpoint: 01M1PGJD97M4X8P0H3VSM2683T
requireAbsPluginParent applied the rule to ENTIRE_PLUGIN_DIR,
XDG_DATA_HOME and LOCALAPPDATA; ENTIRE_CONFIG_DIR and XDG_CACHE_HOME had
no such check, and they redirect the directories holding the login tokens
and the discovery caches. A relative value there resolves against the
working directory, so the same environment names a different directory in
every process — usually one inside whatever repository the command ran
from.

Lift the rule into userdirs.RequireAbsoluteOverride, which both reach
(userdirs cannot import cli, so calling the plugin copy was not an
option), and delete the plugin-local one.

Config() and Cache() keep their string signatures, so the refusal lands at
the roots instead: userdirs' own resolveUserRoot, which checks before
creating anything, plus contexts.configRoot and discovery.cacheFile.root,
which open their own roots on the directory they are handed. Those two
called filepath.Abs on it first, which turned the exact mistake being
guarded against into a plausible-looking absolute path.

Falling back to the platform default was considered and rejected: for the
config directory that default is the developer's real ~/.config/entire,
and silently substituting it for a mistyped override — a test harness's,
say — is worse than failing.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Entire-Checkpoint: 01M1PGWCMFTVSK4CT329EXGZV6
gtrrz-victor and others added 29 commits September 15, 2026 12:18
The package has one test, so a regex over it selects everything or
nothing. The mise task keeps its filter argument for local use.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2J95ZV95H4S1066PAY6PWBX
GitHub shows the key unpadded, but a base32 key copied from elsewhere
carries trailing "=", which the unpadded decoder rejected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2J9SPZP6J9RBTKE4QREAPM3
…utput

A create that succeeded but printed unusable JSON failed the test before
its cleanup was registered and left the resource behind on the shared
account. Cleanups now register by name as soon as the create returns and
switch to the ULID once decoded, since an already-deleted ULID exits 0
while a name that no longer resolves is an error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2JAMC63BH2AGBW1HWNZ7ZGC
…cleanup

t.Cleanup cannot run when the process is killed: a package timeout, a
cancelled job, or a lost runner. With a three-org quota on the test
account, one such run blocked every later one until someone deleted the
leftovers by hand. Each run now starts by deleting e2e-cp-* orgs older
than 30 minutes, with their projects and repos; the age gate leaves a
run in progress elsewhere alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2JB5PS1Y58CFQ6PN2N1QGXE
fix: install the managed plugin bin entry on Windows without a symlink
On Windows, answering a plugin confirmation needed a second keypress
before the confirmed action started. Bubble Tea only builds a
cancellable console reader for os.Stdin; the separately opened CONIN$
handle the prompt uses gets a fallback whose Cancel is a no-op, so the
read its loop issued after the answer stays pending, and Go's
os.File.Close on Windows waits for every pending operation — which a
console completes only on a keypress.

PromptTTY.Close now cancels pending I/O on the input (CancelIoEx) before
closing it, and the plugin confirmation opens its terminal through
interactive.OpenPromptTTY instead of tea.OpenTTY so it goes through that
Close. The login key prompt already did, so it is fixed by the same
change. The cancelled read reports io.EOF, so the close stays after the
form has returned.

Verified on Windows 11 (conhost) with a minimal program: Close blocked
until a key was injected before, returns immediately after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyJLbQP5sdFssuU1f1gSMU
Entire-Checkpoint: 01M2JCVA9BSMQX48YMV809YT1N
filterToUncommittedFiles decided "already committed" by comparing the
working tree's raw bytes against the HEAD blob. Under core.autocrlf —
the Git for Windows default, and what both testutil.InitRepo and
e2e/testutil/repo.go set — the working tree holds CRLF while the blob
holds LF, so every committed text file compared unequal and the filter
never dropped anything.

That matters because the filter is what stops an agent's mid-turn commit
from being checkpointed twice. With it broken, turn-end saw a file that
PostCommit had already condensed, missed the "no changes, skip" gate, and
minted a fresh shadow branch on the *new* HEAD seconds after PostCommit
deleted the old one. Nothing condenses that branch away — the session
ends and no further commit arrives — so it outlives the session. On the
nightly install smoke that surfaced as copilot-cli failing
TestMultiSessionSequential on windows-latest with "shadow branches should
be cleaned up within 10s after commit", every night since 09-11.

The condition was latent from the start and only became reachable when
copilot-cli's turn-end file list stopped being empty. The last green
nightly ran the same broken filter, so its list was genuinely empty
rather than correctly filtered; the only extraction change in that window
is #2341's restrictedProperties.filePaths fallback. That fallback follows
Copilot moving the field out of properties, so both halves are needed to
explain the flip — and neither is at fault: reporting a path the agent
later committed is correct at that layer, and narrowing it to what is
still uncommitted is this function's job. The three other Windows agents
produce no turn-end list at all and so never reached the comparison.

Compare through gitrepo.HashWorktreeFiles (git hash-object) instead, which
applies Git's path-specific clean filters. This also fixes .gitattributes
eol/text rules and clean filters such as Git LFS, which the byte
comparison got wrong for the same reason. Symlinks stay on the raw path:
hash-object follows the link and hashes the target's content, while a Git
symlink blob stores the target path. Anything Git cannot hash falls back
to the previous comparison, so the function still fails open — toward
keeping a file rather than dropping one.

Both call sites benefit: turn-end and the subagent task path, whose own
comment already described this exact failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2JJHKDTXB4SAFGYRC6FWRWJ
docs: install Homebrew casks by fully qualified name
checkpoint: accept case-folded shard directories in ParseRef
Checking only the HEAD entry's mode was not enough. git hash-object
follows a working-tree symlink and hashes the target's *content*, so a
tracked regular file replaced by a link to identical content hashes equal
to its HEAD blob and was dropped as "already committed" — while git status
calls it a typechange (" T"). The HEAD entry is still a regular file, so
the tree mode cannot catch it. Verified directly: after replacing a
committed foo.txt with a symlink to a file of equal content,
`git hash-object -- foo.txt` returns the HEAD blob hash exactly.

That drops a real change, which is the direction this function must never
fail in. A FIFO is worse than wrong: hash-object blocks reading it, and on
a hook path that costs the caller its whole budget.

strategy already had this rule and a test asserting it, so rather than add
a third copy the pair moves to worktreedir.HashableEntry, where both
callers reach it. It cannot live beside HashWorktreeFiles in gitrepo:
worktreedir's own test imports testutil, which imports gitrepo, so that
edge is an import cycle in the test binary. Its tests move with it, and
content_overlap_test keeps the half that is still its own — what the
confined fallback answers for a symlink.

Reported by Cursor Bugbot and Copilot on #2482, both pointing at
strategy.requiresConfinedWorktreeHash as the precedent. They were right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2JNBMD9081P0M1ZTX1KKXM2
…ails

PromptTTY.Close returned before closing either console handle when
CancelIoEx failed with anything but ERROR_NOT_FOUND, and every caller
discards Close's error, so both handles leaked silently. The release
failure is now joined with the close errors instead of replacing them.

The Windows regression test also asserts that the released read reports
io.EOF. Without that, a Close that ran before the read was pending still
passed: CancelIoEx found nothing, the read then hit a closed handle, and
both selects succeeded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2JNSSVQD3DSS99TVQRT0EP9
fix(lifecycle): compare committed files through Git's clean filters
…active

fix: release the pending console read before closing a prompt terminal (Windows double Enter)
`entire enable` in a non-git directory no longer creates a GitHub
repository. Creating a repo on a forge and publishing a directory's
contents are the user's calls to make with their own tooling, not a side
effect of enabling Entire.

Bootstrap keeps `git init`, the git-identity setup, and the initial
commit, including the two-phase split that defers the commit until after
agent setup so `.entire/` and `.claude/` land in it.

Gone: `gh repo create`, the `git push -u origin HEAD` step, the
owner/name/visibility resolvers and their prompts, the "create a GitHub
repo?" and "push?" confirms, and the `--repo-name`, `--repo-owner`,
`--repo-visibility`, `--no-github`, `--push` flags. The interactive menu
loses its "Yes, plus a private GitHub repository (pushed)" option, and
with it the `gh` probe that decided whether to offer it.

`gh` is still invoked in two places, neither of which creates anything:
sourcing user.name/user.email from `gh api user` when git identity is
unset, and `ghCurrentUser`, which `entire trail list --author me` needs.

setup_github.go is renamed to setup_bootstrap.go since nothing in it
touches GitHub any more, and `GitHubBootstrapOptions` /
`runGitHubBootstrap*` follow.

TestRunBootstrap_CreatesNoRemote replaces the old "stays local unless
asked" test and inverts its premise: it drives --yes, the most permissive
input there is, and asserts no `gh` call and no `git remote`/`git push`.
A future flag that reintroduces remote creation has to break it first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2JQSACME42VM25XASBM3Z5E
git-remote-entire: delete the wrong-cluster hint, which cannot fire
The sweep of leftover orgs is now best effort: a leftover it cannot list
or delete is logged and skipped instead of failing the current run, and
"swept" is logged only once the org delete succeeded. The device login
runs in the isolated state dir rather than the checkout, a stalled
about:blank page is named instead of printed as an empty host, and the
sweep no longer reuses one output variable across nested listings.

Restores the offline RFC 6238 vector test, now also covering a padded
key: the production workflow runs only on main and manual dispatch, so
this is the only check of the algorithm that runs on every PR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2JRD2XPQH9Y8VSHBTC88VNX
Add control-plane E2E tests with device-flow login
Three low-severity findings on trail 1334, all correct.

Drop the `errW` writer threaded through runBootstrapInit ->
runBootstrapInitWith -> ensureGitIdentity. Its only writers were the
gh-probe warnings removed in the previous commit; ensureGitIdentity
already discarded it. confirmInitRepo's unused writer goes too — same
dead-parameter shape one function over, and fixing one while leaving the
other reads like an oversight.

Fix two comments that still promised a push, on printBootstrapSection
and resolveCommitMessage, contradicting the BootstrapOptions doc three
lines into the same file.

TestRunBootstrap_CreatesNoRemote and TestRunBootstrap_YesAcceptsAllDefaults
ran a byte-identical `--yes` bootstrap against identical stubs and
differed only in their assertions. Rather than fold them — which would
blunt the guard by mixing positive assertions into it — CreatesNoRemote
now sweeps four input shapes (--yes, --init-repo, a custom message, a
skipped commit). The two tests genuinely differ now, and the guard covers
the case a remote would most plausibly return through: attached to one
option rather than all of them. Verified it fails on a reintroduced push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2JW4SP8Z4K8SQ65ESH30N3Q
Make enable bootstrap local-only: no GitHub repo creation
A sole `entire://` remote is now elected as the checkpoint sync remote after an
explicit `checkpoint_push_remote` or a captured election and before origin.
Detection reads the raw `remote.<n>.url` and `remote.<n>.pushurl` values from
`.git/config` — never `git remote get-url`, which expands url.<base>.insteadOf —
and a remote counts only when every URL is entire://, so a remote that fans
out to GitHub is never elected. Two or more Entire remotes make the tier
ambiguous and it does not apply. Capture cannot displace the Entire remote.

Once elected, the pre-push hook treats the Entire remote like the dedicated
checkpoint_remote URL mode: every push — any remote or raw URL — carries
checkpoints to it by name (`pushSettings.syncRemote`, honoured by
`pushTarget()`), the single-remote gate and capture are skipped, and the
git-branch empty-remote defer is skipped, since keying it on the Entire
remote's tracking refs would defer v1 forever for someone who never pushes
code there. Without this a `git push origin` habit would strand checkpoints
locally the moment the tier flipped the election. The first delivery prints
one stderr line, latched by `entire-checkpoint-sync-entire.json` in the git
common dir (`EntireSyncState`, which also carries a `migration` field for the
follow-up checkpoint migration command).

Also corrects two pieces of advice that named `checkpoint_remote` — the
`{provider, repo}` dedicated-repository object, which silently ignores a remote
name — where the remote picker `strategy_options.checkpoint_push_remote` in
`.entire/settings.local.json` was meant: the multi-remote note printed by
`entire enable` and `entire doctor`, and the git-refs multi-URL warning. With an
Entire remote elected the note says so instead of calling the destination
ambiguous. The pre-push hint gains a case for several Entire remotes, and
`entire status` annotates the Entire remote. `SetRefsPushProgressWriter` lets a
foreground caller silence the queue flush's progress dots.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M23PH4QCMG6PJT9T1Z7RC6ZQ
…directed push

Review follow-ups on the Entire tier.

The tier displaces whatever the default tiers would have elected. Origin was
already the read chain's legacy tier, but a displaced sole or first remote with
another name dropped out of the chain entirely: nothing deleted, nothing found,
nothing said. CheckpointReadRemotesWithElection now appends the displaced
remote (LegacyCheckpointRemote) under the Entire tier, and the first-delivery
announcement names it again — "Earlier checkpoints may still be on X; they stay
readable from there" — while the migration ledger is still pending.

Under a redirected push the git-branch OPF rewrite was bounded by the Entire
remote's v1 tip, which does not exist yet, so the whole local history counted
as unpushed: BootstrapTooLargeError above the cap aborted the user's `git push
origin`, below it the entire history was rewritten. The rewrite now falls back
to the displaced remote's tip as the bound when the target has none, and an
OPF failure under the redirect withholds the checkpoints with a stderr line
instead of aborting a push the user aimed elsewhere.

The empty-remote defer keyed on the redirect flag, so a direct `git push
entire` on an empty Entire remote still deferred; pushSettings now records the
tier itself (entireTier / targetsEntireRemote) and the defer keys on that. The
redirect's debug log redacts the raw hook target. The raw-config rationale is
corrected: git applies insteadOf before dispatch, so raw classification answers
"which remote did the user declare as Entire", not "where do bytes go" — a
user-local choice, and the reason tests can stand in a file:// bare. Under the
tier the topology note describes a multi-URL remote's fan-out as code only and
drops the first-URL marker, and the status counter no longer tells the user to
push the Entire remote specifically. EntireRemotes is unexported until it has a
caller.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M23SZ19ZMP19BS6CM34F9QC6
The read-chain fix derived the displaced remote from current config, on the
reasoning that it answers the same question while the remote exists. It answers
it as of now, and the answer moves when config does: LegacyCheckpointRemote
prefers origin unconditionally, so a repo that accumulated checkpoints on a sole
"gh", then gained an Entire remote, then gained an origin, saw the fallback swap
from the remote that holds the checkpoints to a brand-new empty one. Narrower
than the bug it replaced — it needs a later origin — but the same failure mode.

The tier now records what it displaced. EntireSyncState gains DisplacedRemote,
written once at the first push under the tier (in the redirect, before delivery:
"which remote did this tier displace" is true whether or not the push lands),
and DisplacedCheckpointRemote prefers the record while that remote is still
configured, falling back to the live answer when nothing is recorded yet or the
recorded remote is gone. The announcement and the OPF rewrite bound use it, so
neither can be re-pointed by a config change.

The read chain no longer names a single fallback at all: under the tier it
appends every non-Entire remote, recorded one first. Any of them may hold
checkpoints from before the tier took over, reads are best-effort and fail open
per candidate, and a single pick — however it is computed — is a choice that
moves when the remote set does.

Tests cover the three-step sequence (sole gh, tier takes over, origin added
later), write-once semantics, the fallback when the recorded remote is removed,
and an announcement that names the recorded remote rather than the live one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M240ADPNM5W6MGQ2J95EC8PR
…rected one

The OPF sites gated on "was this push redirected" (syncRemote != "", set only
when the user named some other remote) while the empty-remote defer already
gated on the tier. A direct `git push entire` therefore took the untreated
path: no displaced-remote bound, so on a git-branch repo with more v1 commits
than the bootstrap cap the whole local history read as unpushed and the push
aborted — the exact case the bounded rewrite was added for — and no withhold,
so a genuine OPF failure vetoed the push too.

All three sites now key on pushSettings.targetsEntireRemote(). The destination
is the same Entire remote either way, with the same missing v1, so the redirect
was never the thing that mattered; the recorded displaced remote is already
available on that push. redirectedToSyncRemote had no production caller left
and is removed, and skipRedirectedCheckpointPush becomes
withholdEntireCheckpointPush with the rationale restated: under the tier the
checkpoint delivery is machinery attached to a push aimed at the user's code,
so failing closed means withholding it, not vetoing their push — the stance
the git-refs backend already takes.

Two regression tests drive the real pre-push hook against a git-branch repo
whose origin holds the v1 history and whose Entire remote does not: a direct
push gets the fallback bound instead of BootstrapTooLargeError, and an OPF
misconfiguration is withheld with the stderr line instead of aborting. Both
fail with the predicate reverted to the redirect check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M242V41KJ1RAJA8YNGM4GRDX
Primitives for moving checkpoint artifacts between remotes, each separately
testable and with no caller yet (the `entire checkpoint migrate` command
follows): InventoryCheckpointArtifacts lists a remote's refs/entire/checkpoints/*
and its entire/checkpoints/v1 tip with one ls-remote by remote name;
HydrateCheckpointArtifacts fetches what is missing or differs locally into
refs/entire-sync-tmp/* and promotes through SafelyAdvanceLocalRef, the one
sanctioned path that advances local refs from a non-elected remote (foreground,
consented, and the source is about to be cleaned); RequeueAllCheckpointRefs
enqueues every local ref so the gated queue flush publishes the whole backlog;
VerifyCheckpointArtifacts compares the destination's advertised hash to the
local hash; RemoveCheckpointArtifacts deletes only the refs it is handed, in
chunks, addressing the old remote by its single push URL so
resolvePushCommandTarget's dedicated-checkpoint_remote rewrite can never apply,
refusing fan-out remotes, and pruning the stale v1 tracking ref.

Supporting changes: batchPushRefs chunks refspecs at 200 (a backlog publish
overran ARG_MAX), PushQueue.EnqueueAll takes one lock for many refs,
remote.DeleteRefs lists before deleting so already-absent refs are counted
rather than failed (trail cleanup reuses it), and the ls-remote parser moves
into the checkpoint package as ParseLsRemoteRefs / ParseCheckpointRefNames.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M23PK92NT94FCGHQ0B9AAQ21
The pure half of `entire checkpoint migrate`: planCheckpointSync turns a
snapshot of the repo (remotes with raw URLs and ls-remote inventories, the
election, the Entire remotes, backend, local refs, queue length, push_sessions,
the migration ledger, flags, interactivity) into a state and an ordered step
list, with no I/O. States: no remotes, dedicated checkpoint_remote (out of
scope, explained), fail-closed setting, a choice among remotes (none Entire, or
several Entire), pinned elsewhere by an explicit or captured election, already
home, publish only, migrate, and convert-first for git-branch repos. Every
user-facing string lives in this file so the copy can be reviewed in one place,
including the runtime lines for verify-missing, removal failed or declined,
push_sessions disabled, the combined and removal confirmations, the header,
the dry-run "Would …" lines, and the closing block.

checkpoint_migrate_value.go computes that closing block locally with a bounded
cost: checkpoint count from refs, sessions, agents and date range from the
store listing, and tokens of agent work across at most the 500 most recent
checkpoints under a four-second budget, phrased honestly when capped.
formatTokenCount gains an M suffix, which changes `tokens` output for large
totals (test expectations updated).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M23PMX6K2JECEY8M4SY5236P
Move this repo's checkpoints to a single remote — the Entire remote when there
is one — and bring the backlog with them. Two planning passes: the first uses
local facts and stops early for states that need no network or need a choice
(pickers whose first option is always the no-change default, because accessible
mode answers an unreadable prompt with the first option); the second adds an
ls-remote inventory of every remote and drives the engine in order: record
`--to` in .entire/settings.local.json only when the election would not already
pick it, hydrate from every old remote, convert a local v1 branch to refs and
flip the primary store (project file; commit hint printed), requeue, publish
through the OPF- and policy-gated queue flush with the progress dots silenced
under a spinner, verify at the local hash, then delete from the old remotes only
what was verified and only with consent — `--remove` when non-interactive,
`--yes` never implies it. A source that could not be listed or fetched is
reported and never removed from. The ledger records done (data is home) or
declined (cleanup refused) so status can stop nudging. A bare non-interactive run
reports the plan and the exact command and changes nothing; `--dry-run` reads
only; `--json` emits the plan and, with `--yes`, the result.

Classified user-owned and unlisted in agent-help: it pushes transcripts,
deletes remote refs and writes settings, so an agent must not run it
unprompted. `doctor migrate-checkpoints` points at it and exposes its
push-and-report helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M23PN643RV7PWD5WM0MT06CX
…ble and docs

`entire status` shows a network-free nudge — "older checkpoints may still be on
origin — run 'entire checkpoint migrate' to bring them over" — while the Entire
remote is elected, the migration ledger is pending, a non-Entire remote exists
and this clone holds checkpoints; `--json` gains checkpoint_sync_migration
(pending|done|declined). `entire doctor` reports the destination when there is
something to say: the elected Entire remote plus the same pointer, or the
REVIEW block ending in the command. Re-enable prints the pointer; a fresh setup
has nothing to move. The topology advice, the several-Entire-remotes pre-push
hint and the first-delivery announcement now name the command, which exists as
of this branch. README, CLAUDE.md and the architecture doc gain the command,
the "Moving checkpoints" section and the planner's sync-remote vocabulary note.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M23PNB6XK6F2FPMXX1H43REE
…d keep v1 while the store is git-branch

Review follow-ups on `entire checkpoint migrate`.

The requeue step was gated on local checkpoints counted before hydration, so
in any fresh clone — which holds none of refs/entire/checkpoints/* — hydration
fetched the refs, nothing was queued, publish pushed zero, and verify failed on
the first run. Requeue is now planned whenever data moves; dedup is free. A
fresh-clone integration test seeds origin, forgets every local artifact, and
requires the first run to land.

Conversion is a planned step (convert_v1) whenever a v1 branch will exist
locally after hydration, on either store, so --dry-run lists it and the
confirmation names it; the store flip stays its own step. Deleting an old
remote's v1 branch now requires both that this run converted it and that
git-refs is the store readers use: under ENTIRE_CHECKPOINTS_PRIMARY the
settings are not written, so the branch is kept and the run says why. The
never-false convertedAll interlock is gone.

Each remote is listed once: the planner's inventory keeps the advertised
hashes and execution acts on that listing, so plan and action see the same
remote state and --from excludes remotes from the listing as well as from
removal. An invalid --to or --from is returned as an ordinary error that main
prints. The already-home state records the ledger only with --yes or a human at
the terminal, and otherwise says how to record it. Declining the picker no
longer prints the header twice. The closing block says "whichever remote you
push your code to" only under the Entire tier; elsewhere it says pushes to the
destination carry checkpoints and others do not. Doctor reports fan-out first,
so an Entire remote with several push URLs still shows it.

Smaller: formatTokenCount rounds before choosing the unit (999,999 is 1M) and
goes up to B; the README lists fetch before convert; the one-caller
pushQueuedCheckpointRefsReporting refactor is reverted; legacyElectedRemote is
gone in favour of strategy.LegacyCheckpointRemote; formatDestinationUnchanged
is printed when --to names the elected remote; --json carries
checkpoint_sync_legacy_remote; a stale comment named the old command.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M23V29AQJ3EKCSXMCTYE4PSA
… enable

The nudges that say where older checkpoints still are now read
strategy.DisplacedCheckpointRemote, the remote the Entire tier recorded when it
took over, rather than recomputing it from the current remote set. Recomputing
prefers origin unconditionally, so adding an origin after the tier took over
re-pointed every one of these messages at a new empty remote while the
checkpoints sat elsewhere. checkpoint_sync_legacy_remote in --json follows the
same source.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M240M3B5W6BSQE05DA7GX0WV
@toby
toby force-pushed the toby/checkpoint-sync branch from 2cd23af to 73ef6f3 Compare September 15, 2026 18:35
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.