Skip to content

Confirm before session attach links, and link any commit by whether it's pushed - #2672

Merged
Soph merged 23 commits into
mainfrom
peyton/attach-commit
Oct 9, 2026
Merged

Soph merged 23 commits into
mainfrom
peyton/attach-commit

Conversation

@peyton-alt

@peyton-alt peyton-alt commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

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

Problem

entire session attach could only link HEAD, by amending it with an Entire-Checkpoint trailer:

  • it prompted at a terminal, or amended with -f;
  • without a terminal it printed the trailer and left the checkpoint linked to nothing;
  • a commit that was already pushed could only be linked by rewriting it;
  • a session that already had a checkpoint couldn't be attached to another commit.

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?

Commit What attach does Rewrites history?
already has a checkpoint adds the session to it; pushes the checkpoint now if the commit is pushed, else with the next git push no
no checkpoint, pushed records the link in a new checkpoint (linked_commits: [{sha, repo}]), pushes it now and confirms the remote has it no
no checkpoint, unpushed adds the trailer, rewriting the commit and any commits after it on the current branch (content unchanged) yes

Confirmation. 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 --force only once they agree.

  • Rewriting. The replay uses git commit-tree with the same trees, authors and messages. It moves the branch with a compare-and-swap update-ref (the reflog names the attach) and remaps session state through PostRewrite. 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 (which commit-tree can't reproduce), and running mid-rebase, merge, cherry-pick, revert or bisect.
  • "Pushed" means any remote-tracking ref contains the commit (so upstream in a fork counts), or a branch on the branch's upstream or push remote does, else on origin; 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.
  • One session, several commits. The "session already has a checkpoint" special case is gone. Each checkpoint records the turns since the session's previous one: prompts, turn count and tokens are scoped from 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.
  • Author warning. A recorded link counts only when the commit's author attaches it; the server credits it only when the checkpoint pusher authored the commit.
  • Second attach to a linked commit joins that checkpoint, including one that exists only on the remote (both backends), instead of starting a duplicate.
  • Rebases. A recorded link names one exact commit and doesn't follow a rebase the way a trailer does. Attach says so.
  • Readers. explain <commit> and blame/why fall back to recorded links when a commit has no trailer.
  • Docs. attach --help, the agent-help entry, and sessions-and-checkpoints.md.

The server half is entire-api trail #197. This change needs nothing new from it.

Tests

  • Confirmation: without a terminal or --force, nothing is written for an unpushed commit or a pushed one.
  • Rewrite: an unpushed HEAD and an older unpushed commit get the trailer, with descendants replayed and trees unchanged. A merge in the range is refused.
  • Pushed commits: pushed HEAD and pushed older commit are linked in the checkpoint, and nothing is rewritten. A pushed HEAD with a checkpoint is joined, not rewritten.
  • Sessions: a session can be attached to a second commit, recording only its new turn. Re-attaching to the same checkpoint is a no-op.
  • Remotes: refuses when a fetch fails; ignores an unrelated, unreachable remote; sees commits pushed to untracked branches; never rewrites a commit another remote's tracking ref holds, and pushes its checkpoint to the branch's remote; errors when the checkpoint isn't delivered; delivery is confirmed on the git-refs backend too.
  • Rewrite limits: a commit with a non-UTF-8 encoding is refused.
  • Push timing: joining a pushed commit's checkpoint pushes it now; joining an unpushed one waits for git push.
  • Linked checkpoints: a remote-only linked checkpoint is joined, including with the refs backend.
  • Readers and blame: the explain/blame fallbacks; blame reads each remote checkpoint once per run.
  • mise run check passes. I also hand-ran the no-terminal and --force flows in a scratch repo.

Not in v1

  • Linking multiple commits or a range.
  • Detach or undo.
  • Remapping recorded links after a rebase or amend.
  • Sessions that never produce a commit.

🤖 Generated with Claude Code

`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
@peyton-alt
peyton-alt requested a review from a team as a code owner October 6, 2026 02:44
Copilot AI balanced review requested due to automatic review settings October 6, 2026 02:44

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 28b573c. Configure here.

Comment thread cmd/entire/cli/attach.go
Comment thread cmd/entire/cli/attach.go Outdated
Comment thread cmd/entire/cli/attach.go

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.

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 Medium severity

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_commits checkpoint 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.

Comment thread cmd/entire/cli/attach.go Outdated
Comment thread cmd/entire/cli/attribution.go
Comment thread cmd/entire/cli/checkpoint/persistent.go Outdated
Comment thread cmd/entire/cli/explain.go
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
@peyton-alt peyton-alt changed the title Let session attach link a commit without rewriting it Let session attach decide how to link from the commit Oct 6, 2026
peyton-alt and others added 7 commits October 6, 2026 15:20
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
@peyton-alt peyton-alt changed the title Let session attach decide how to link from the commit Confirm before session attach links, and link any commit by whether it's pushed Oct 7, 2026
peyton-alt and others added 5 commits October 7, 2026 13:57
…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
peyton-alt and others added 7 commits October 8, 2026 11:33
…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
peyton-alt and others added 2 commits October 8, 2026 16:20
- 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
@Soph
Soph merged commit 80ac12c into main Oct 9, 2026
18 checks passed
@Soph
Soph deleted the peyton/attach-commit branch October 9, 2026 08:00
peyton-alt added a commit that referenced this pull request Oct 9, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants