Repository navigation
Confirm before session attach links, and link any commit by whether it's pushed - #2672
Conversation
`entire session attach` could only link HEAD, by amending it with an
Entire-Checkpoint trailer. An older or already-pushed commit, or a
headless attach that must not rewrite history, had no way to link, so
its checkpoint was written but referenced by nothing.
`--commit <rev>` links the session to that commit instead. A commit
that already has a checkpoint is linked by joining it; otherwise the
checkpoint records the commit in a new `linked_commits` field
({sha, repo}) and the commit is left untouched. The server treats a
verified entry like a trailer (attributing), unlike import's
`commit_sha` anchor.
A commit no remote branch holds is refused unless --allow-unpushed,
since the link would not follow a rebase or amend. The checkpoint is
pushed right away through the pre-push path, because no later push may
carry it. `explain <commit>` and `blame`/`why` fall back to linked
checkpoints when a commit has no trailer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M47HHA12WA2ZA9AT072E56SM
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ 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 28b573c. Configure here.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Link verification, cross-clone discovery, duplicate linkage, and push reporting have unresolved correctness and security issues.
Review effort: Balanced
Findings: 4
Open (4)
What changed in this PR
Adds trailer-free checkpoint links so session attach --commit can associate sessions with existing commits without rewriting history.
Changes:
- Adds
linked_commitscheckpoint metadata and preservation logic. - Extends attach, explain, blame, and why to use trailer-free links.
- Adds tests and architecture documentation for the workflow.
| File | Description |
|---|---|
docs/architecture/sessions-and-checkpoints.md |
Documents linked-commit semantics. |
cmd/entire/cli/explain.go |
Resolves commits through linked checkpoints. |
cmd/entire/cli/checkpoint/persistent.go |
Persists and reverse-resolves links. |
cmd/entire/cli/checkpoint/aliases.go |
Exposes the linked-commit type. |
cmd/entire/cli/attribution.go |
Adds blame/why link resolution. |
cmd/entire/cli/attach.go |
Implements --commit attachment and pushing. |
cmd/entire/cli/attach_commit_test.go |
Tests linked-commit workflows. |
api/checkpoint/metadata.go |
Defines linked-commit API metadata. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
attach linked by a flag: without --commit it amended HEAD (prompting, or -f), with --commit it always recorded the link in the checkpoint and refused unpushed commits unless --allow-unpushed. People and agents had to know which mechanism a commit needed, and a headless attach still left the checkpoint unlinked. The link now follows from the target commit (HEAD unless --commit): - it already has a checkpoint: join it; - HEAD, not pushed: amend in the trailer, no prompt (it survives a later rebase, and nobody else has the commit); - already pushed: record the link in the checkpoint, leave the commit alone, push the checkpoint; - older and not pushed: refuse before writing anything. The amend prompt and --allow-unpushed are gone; -f is kept hidden as a no-op so existing scripts keep working. A recorded link to a commit the local git author didn't write warns that it won't count. attach --help, the new agent-help guidance and the architecture doc state the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M49J19BRSP1N0FX6913KA335
A pushed commit linked by an earlier attach carries no trailer, so a second attach to it found no checkpoint and started a separate one for the same commit. It now finds the checkpoint whose recorded links name the commit and joins it, as the trailer paths do, and pushes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lookup for a checkpoint already linked to a pushed commit listed remote-tracking checkpoints too, and skipped the local-presence guard, so a second attach could rebuild a remote-only checkpoint under its ID and overwrite it on push. It now looks only at local checkpoints, before the availability guard runs. Deciding that HEAD is unpushed from remote-tracking refs alone could amend a commit someone had already pushed from another clone. attach now fetches each remote first, without credential prompts; if a fetch fails it says so and decides from the last fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From an adversarial review of the attach rule: - A failed fetch was taken as "unpushed", so attach could amend a commit another clone had already pushed. It now also asks each remote whether a branch tip is the commit (single-branch clones don't track every branch) and refuses to amend if any remote can't be reached. - PrePush is fail-soft, so attach reported "pushed" when push_sessions was off or the push was skipped. For a pushed commit the checkpoint is the only record of the link, so attach now checks the remote's ref matches and returns an error naming the reason when it doesn't. - A second attach to a linked commit looked only in the local store, so a fresh clone started a duplicate checkpoint. It now finds the checkpoint through every copy and fetches it into the local store through the availability guard before joining. - A recorded link names one exact commit and doesn't follow a rebase or amend; attach now says so, and the docs no longer call it equivalent to a trailer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
attach only matched a remote branch whose tip was the commit, so a commit someone pushed and then built on, on a branch this clone doesn't track, read as unpushed and could be amended. Each remote is now asked for its branch tips; when the commit isn't one, the branches are fetched by name without writing any ref and checked for ancestry. Reachability is decided by that direct query, so a configured refspec that names a branch the remote lacks no longer makes the remote look unreachable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hecks A session with its own checkpoint, attached to a HEAD carrying another checkpoint's trailer, skipped the push check and amended even a pushed HEAD. It now gets the same check and is refused there. Only the remotes the branch pushes to (upstream and push remotes, else origin) are fetched and asked, so an unrelated unreachable remote no longer blocks attach, and only branches whose tips aren't local are fetched again. Git-refs stubs discovered on a remote now carry linked_commits after hydration, and the linked-checkpoint lookup in attach, explain and blame reads stubs minted around or after the commit, so a remote-only link is joined instead of duplicated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4BSQ1H5FMQ553RA9CQ3A1WG
blame looked up recorded links per trailer-less commit, and each lookup got a fresh 30s budget to read remote-discovered stubs. Reads are now cached per run, so each stub is read once, under a single deadline for the whole run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4BTFTF5K4K06V9GQV72PBT0
…ny commit attach now decides from two facts about the target, the same for HEAD and --commit: does it already have a checkpoint (join it), and is it pushed (record a link) or not (add the trailer, rewriting it and the commits after it). The "session already has a checkpoint" special case is gone: a session can be attached to several commits, and each checkpoint records only the turns since the session's previous one. Before writing, attach prints what it will do and asks. Without a terminal it changes nothing and exits non-zero; agent-help tells agents to show the user that output and pass --force only once they agree. The trailer rewrite replays commits with git commit-tree (same trees, authors and messages), moves the branch with a compare-and-swap update-ref, and remaps session state through PostRewrite, so no commit hooks run and the worktree is untouched. Merges after the target and in-progress operations are refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4C1BX3WYSCG2YSHRZFB35Y3
…ording Found by running attach by hand: explain showed "(none on this branch)" for a checkpoint linked to a pushed commit, a no-op re-attach warned about "no new turns" first, and a one-commit rewrite said "Their SHAs change". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4C2FRS12EFZN934S82JY3P9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pushed commit may never be pushed again, so a session joined to its checkpoint would stay local. Whether the commit is pushed now decides when the checkpoint goes out for every attach, not only recorded links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4C9YFQ0R8PTAS24ASQ06646
A stale tracking ref from an unrelated remote made attach treat the commit as pushed there, and then push the checkpoint to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4CB09VBRNRD58CPSN5YHG8P
…commit-tree can't reproduce A commit upstream holds in a fork setup read as unpushed once only the branch's remotes were asked, and was rewritten. Any remote's tracking ref now marks it pushed; the checkpoint still goes to the branch's own remote. Commits with a non-UTF-8 encoding or extra headers are refused rather than silently changed by the replay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4E9CATTNE2F04E0BVFW4AWW
…ry on git-refs Asking each ls-remote tip with its own merge-base took ~25s on a repo with hundreds of branches. Tips a tracking ref already covers are skipped, and the rest are walked once with rev-list under the fetch timeout, stopping at history older than the commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4ECNZSEZJ8AJCJ81M2M9SE9
A rerun found the session already in its checkpoint and stopped, so a recorded link whose push had failed was never retried. For a pushed commit it now pushes the checkpoint again and confirms delivery. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4EE41XBNBSVCWHG1S157Z8D
A hex checkpoint on the git-refs primary can live on its own ref or on the metadata branch; check the first of its storage refs that exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4EESHCAW5KXDHY0D8PFD3V2
…known A rerun that found the session already in a pushed commit's checkpoint pushed the transcript with no prompt; it now pushes only when the checkpoint isn't on the remote, after the usual confirmation. A walk that failed or timed out counted as pushed and recorded a link to a commit the remote may not have; it now counts like an unreachable remote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4EM3FJ0SNA4M47DWW54JAPQ
Resolves conflicts with the shadow-branch and line-attribution removal and agent-home lookup: linked_commits stays on the root summary next to the legacy combined attribution, explain keeps naming how a commit is linked, and attach records the agent home it found the transcript under. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4EQH0GE6941B05REHG9KPQW
The replay set only the author, so every rewritten commit took the current user and time as committer. It now keeps the committer too, as git filter-branch does, and drops inherited GIT_AUTHOR_*/GIT_COMMITTER_* so the identity it sets is the only one git sees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4ERNSD11XY4BHQ8VB8SGYCX
The replayability check ran only on the multi-commit path, so the common case, HEAD, was rewritten without its encoding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4ESAF1DK7EM06861QY32SDX
- Branch names from ls-remote become fetch refspecs; keep only well-formed refs/heads names, so a hostile remote can't advertise "refs/heads/a:refs/heads/main" and write a local ref. - Re-check the tracking refs after the confirmation prompt, which can stay open while the commit gets pushed elsewhere. - Attribute a tracking ref to the longest matching remote name. - Ignore future-dated checkpoints when looking up recorded links; their ID is the pusher's choice and would always win as most recent. - Say that pending checkpoints, not only this one, are pushed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4EX2RQZEGVJYMTWF9Q9RHTD
- A recorded link is checkpoint metadata anyone who can push checkpoints can write, and only the server can verify it. explain lists such commits as "recorded link, unverified" and notes it when a commit resolves through one, blame marks those lines with !, why explains it, and attach says so before joining such a checkpoint. - The re-check after the confirmation asked only local tracking refs; it now asks the remotes directly, as the plan did, and refuses when one can't be reached. - Without an agent transcript position, the window ends at the line count, so the session's offset advances and a later attach doesn't record the same turns again. - Tests reproduce a push from another clone while the prompt is open and a remote advertising a branch name that is a refspec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4EZ3JZP196N20YBKNRV14HJ
Resolves the attach.go conflict with #2672 without bringing back the double count: main's attach counted tokens from the displayed window (CheckpointTranscriptStart), which carry-forward leaves at the session start, so attach after a partial commit recounted checkpointed turns. - The checkpoint's displayed window stays main's window.start; tokens come from strategy.AttachTokenUsage, counted from TokenStart(). - ConsumeAttachTokenWindow runs whether or not the session is active; main's rule still applies to CheckpointTranscriptStart, which a running session's hooks own. - Main now returns early when the checkpoint already holds the session, so attach never replaces its own entry and the replaced-entry token carry-over is unreachable; it is removed and the own-checkpoint test now checks the entry keeps its tokens and the next checkpoint counts the turn attach skipped. - New test: attach during an active turn stores the turn's tokens so far, leaves CheckpointTranscriptStart alone, and the turn's checkpoint doesn't count them again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4GVA294QN563TJP9TB6NBBS


https://entire.io/gh/entireio/cli/trails/1494
Problem
entire session attachcould only link HEAD, by amending it with anEntire-Checkpointtrailer:-f;Change
entire session attach <id> [--commit <rev>](default HEAD) decides how to link from two facts about the commit, the same for HEAD and--commit: does it already have a checkpoint, and is it pushed. In other words: would this rewrite history?git pushlinked_commits: [{sha, repo}]), pushes it now and confirms the remote has itConfirmation. Before writing anything, attach prints what it will do: which commits it rewrites, or which remote holds the commit and that the transcript is pushed now, plus author and rebase caveats. At a terminal it asks. Without one (agents, scripts) it changes nothing and exits non-zero. The agent-help entry tells agents to show the user that output and pass
--forceonly once they agree.git commit-treewith the same trees, authors and messages. It moves the branch with a compare-and-swapupdate-ref(the reflog names the attach) and remaps session state throughPostRewrite. No commit hooks run and the worktree is untouched. It refuses merges after the target, a target that isn't on the current branch, commits with a non-UTF-8 encoding or extra headers (whichcommit-treecan't reproduce), and running mid-rebase, merge, cherry-pick, revert or bisect.upstreamin a fork counts), or a branch on the branch's upstream or push remote does, else onorigin; attach asks those remotes directly, in one ancestry walk. If one can't be reached, attach refuses rather than risk rewriting a shared commit. The checkpoint goes to the branch's own remote, never to an unrelated one.CheckpointTranscriptStart. An ended session's offset then advances; a running session's offset is left to its hooks. Re-attaching a session to the checkpoint that already holds it changes nothing.explain <commit>andblame/whyfall back to recorded links when a commit has no trailer.attach --help, theagent-helpentry, andsessions-and-checkpoints.md.The server half is entire-api trail #197. This change needs nothing new from it.
Tests
--force, nothing is written for an unpushed commit or a pushed one.git push.explain/blamefallbacks; blame reads each remote checkpoint once per run.mise run checkpasses. I also hand-ran the no-terminal and--forceflows in a scratch repo.Not in v1
🤖 Generated with Claude Code