Recognize agent worktrees in transcript, settings, and session lookup#1159
Open
matthiaswenz wants to merge 7 commits into
Open
Recognize agent worktrees in transcript, settings, and session lookup#1159matthiaswenz wants to merge 7 commits into
matthiaswenz wants to merge 7 commits into
Conversation
Claude Code's worktree feature reports transcript_path encoded from the worktree CWD (.claude/worktrees/<branch>) but writes the actual transcript under the parent repo's project dir. The TurnEnd handler aborted on the file-exists check, so no checkpoint condensed and commits had no Entire-Checkpoint trailer. When the reported path doesn't exist, scan ~/.claude/projects/*/ for the session ID and use the match. No-op when the path resolves normally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 710ca10c1821
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes dropped checkpoints for Claude Code sessions when Claude’s transcript_path points to a non-existent worktree-encoded project directory by adding a fallback that scans the Claude projects base directory for the real <sessionID>.jsonl location.
Changes:
- Resolve Claude transcript paths via a fallback scan when the reported
transcript_pathdoes not exist. - Add a helper to find a transcript by session ID under
~/.claude/projects/*/. - Add unit tests covering the resolver and scanning behavior, including the worktree mismatch scenario.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| cmd/entire/cli/agent/claudecode/lifecycle.go | Adds transcript path resolution with a fallback scan and a helper to locate transcripts by session ID. |
| cmd/entire/cli/agent/claudecode/lifecycle_test.go | Adds unit tests for the new scan helper and transcript-path resolver (including worktree fallback). |
- Only fall back on os.IsNotExist; pass through other Stat errors so permission/IO problems aren't masked by a directory scan. - Validate sessionID with validation.ValidateAgentSessionID before using it in filepath.Join, so a hostile hook payload can't traverse out of the projects base dir during the scan. - Skip the scan when the reported path isn't under the Claude projects base dir; for any other path the scan can't produce a better answer. Adds unit tests covering the outside-base-dir and traversal-id paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 3aa3df224dc5
Entire-Checkpoint: 74920b2829ad
Git hooks fired from inside a linked git worktree (e.g. Claude Code's .claude/worktrees/<branch> feature) silently bailed: settings.IsSetUp resolved .entire/settings.json via show-toplevel of the linked worktree, which has no .entire/, so IsSetUpAndEnabled returned false and prepare-commit-msg/post-commit/pre-push all skipped their work. User commits made inside the worktree never got an Entire-Checkpoint trailer, were never condensed, and entire/checkpoints/v1 never advanced on push. paths.AbsPath now routes any .entire/* relative path through a new MainWorktreeRoot helper (parent of git --git-common-dir), keeping all other paths anchored at WorktreeRoot. .entire/ is a main-repo concern — linked worktrees should share the canonical config and state directory. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 84a26a76972c
Even after .entire/ paths were anchored at the main worktree, hooks firing from inside an agent-managed worktree (e.g. Claude Code's .claude/worktrees/<branch>) still silently bailed at the next layer: findSessionsForWorktree strict-matched on WorktreePath, so a commit made in the linked worktree could not find the session whose UserPromptSubmit had fired from the main worktree. The user-visible symptom: no Entire-Checkpoint trailer on the commit, no condensation, no metadata pushed for that session. findSessionsForWorktree now widens to also accept sessions registered against the main worktree when invoked from a linked worktree. The widening is one-directional — sibling linked worktrees don't bleed into each other, and main-worktree lookups are unchanged — so commits on main can't accidentally link to an unrelated agent session. logging.Init also moves from WorktreeRoot to MainWorktreeRoot so hook log output writes to the canonical .entire/logs/entire.log instead of spawning an orphan .entire/logs directory inside each linked worktree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 6f37d51c79a0
- Drop isEntireRelPath in favor of the existing IsSubpath helper. Same semantics on every test input (siblings, parent escapes, absolute paths, look-alikes), so no behavior change — just less code reinventing path containment. - Trim resolveTranscriptPath and worktreeParentCandidate docstrings: cut the bulleted gating recital and the WHAT-it-does narration, keep the WHY (Claude Code transcript_path bug, LastIndex rationale inline at the choice site). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: b001dc12437e
findSessionsForWorktree was unconditionally widening linked → main:
from any linked worktree it returned both linked-worktree sessions AND
main-worktree sessions. That fixed Claude's worktrees-feature path
(outer session in main, inner subprocess commits from linked) but had
two unintended consequences for users who run agents in linked
worktrees independently:
1. Bundling. A commit in `git worktree add ../wt && cd ../wt && agent`
would silently attach any active main-worktree session to its
checkpoint, mixing unrelated session context. The multi-session
model is documented as "sessions in the same directory interleave"
— different worktrees are different directories.
2. BaseCommit corruption. postCommitUpdateBaseCommitOnly (fired when a
commit has no Entire-Checkpoint trailer) clobbers state.BaseCommit
to the new HEAD for every session it sees. With the widened lookup,
a no-trailer commit in a linked worktree would advance main's
active session's BaseCommit to a commit on the linked branch.
Future prepare-commit-msg on main then filters that session out
(BaseCommit != HEAD), silently killing it.
Switch to fallback-only semantics: prefer strict-equal matches, fall
back to main only when the linked worktree has no session of its own.
Preserves the Claude's-worktrees-feature fix (linked has no session →
fallback finds the outer session in main), while keeping
linked-worktree sessions and main-worktree sessions isolated when both
exist.
The fallback stays one-directional. Main-worktree lookups are unchanged
and sibling linked worktrees still never bleed into each other.
Tests:
- Rename _LinkedWorktreeMatchesMain → _LinkedWorktreeFallsBackToMain
(the semantic is now fallback, not widening).
- Add _LinkedWorktreeWithOwnSessionIgnoresMain to lock in property 1.
- Add _LinkedFallbackSkipsSiblings: when neither linked nor main has a
session, return empty rather than wandering into a sibling worktree's
session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b6ccb16e9ce3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix dropped checkpoints when Claude Code runs from inside its
.claude/worktrees/<branch>/feature. Three independent issues conspired to make agent work in linked worktrees invisible to Entire — fixing only one wasn't enough. This PR addresses all three.The user-visible symptom: a Claude session that does work in a linked worktree finishes cleanly, but the commit lands without an
Entire-Checkpointtrailer, no metadata appears onentire/checkpoints/v1, andentire statusfrom inside the worktree reportsnot set up.What was wrong
1. Transcript path mismatch. Claude Code reports
transcript_pathencoded from the worktree CWD, e.g.~/.claude/projects/-Users-foo-Development-repo--claude-worktrees-feature/<id>.jsonl, but the file is actually written under the parent repo's project dir at~/.claude/projects/-Users-foo-Development-repo/<id>.jsonl. The TurnEnd handler aborted at the file-exists check, so no shadow-branch commit was created.2.
.entire/resolved against the wrong root.paths.AbsPath(".entire/settings.json")usedgit rev-parse --show-toplevel, which from inside a linked worktree returns the worktree dir — not the main repo where.entire/actually lives. Every git hook firing from inside a linked worktree sawIsSetUpAndEnabled = falseand silently bailed.entire statusfrom the worktree reportednot set up.3. Session matching was strict-equal on
WorktreePath. Even with the gate unblocked,findSessionsForWorktreecouldn't find the active session:UserPromptSubmitfired from the main worktree (so the session'sWorktreePathis the main repo), butprepare-commit-msg/post-commitfire from inside the linked worktree where the commit happens. Strict equality returned no matches, and hooks loggedno active sessionsat DEBUG and bailed.Resolution
Transcript fallback (
cmd/entire/cli/agent/claudecode/lifecycle.go).When the reported path doesn't exist, the resolver strips the trailing
--claude-worktrees-<branch>marker from the project segment and stats the parent-repo candidate. Deterministic — no directory scanning, no chance of crossing into an unrelated project that happens to share a session ID.strings.LastIndexis used (notIndex) so repos whose sanitized root already contains the token (e.g. checked out underacme--claude-worktrees-tools/) only have the trailing synthetic marker stripped. Gates: triggers only onos.IsNotExist, only when the reported path is under~/.claude/projects/, only when the project segment carries the marker; session ID validated beforefilepath.Join; candidate must exist before being returned..entire/anchored at the main worktree (cmd/entire/cli/paths/paths.go,cmd/entire/cli/logging/logger.go).New
paths.MainWorktreeRootderives the main repo root fromgit rev-parse --git-common-dir's parent.paths.AbsPathnow routes any.entire/*relative path through it; all other paths still resolve againstWorktreeRoot.logging.Initlikewise usesMainWorktreeRootso hook log output writes to the canonical.entire/logs/entire.loginstead of spawning an orphan.entire/logs/inside each linked worktree.Session lookup widens linked → main (
cmd/entire/cli/strategy/manual_commit_session.go).findSessionsForWorktreenow also accepts sessions whoseWorktreePathis the main worktree when invoked from a linked worktree. The widening is one-directional — sibling linked worktrees still don't bleed into each other, and main-worktree lookups are unchanged — so commits on main can't accidentally pick up an unrelated agent session running in.claude/worktrees/<branch>.Test plan
mise run fmt && mise run lint— cleanmise run test:ci— unit + integration + canary greenTestClaudeCode_WorktreePathFallback(integration): drivesentire hooks claude-code stopwith a fakeHOMEand a worktree-encodedtranscript_paththat doesn't exist; verifies a checkpoint is created end-to-endTestMainWorktreeRoot_LinkedWorktree,TestAbsPath_EntireAnchoredAtMainWorktree,TestIsEntireRelPath: path resolution from inside a linked worktreeTestLinkedWorktree_EntireSetUpVisible(integration):entire statusfrom a linked worktree reports the active configuration instead ofnot set upTestFindSessionsForWorktree_LinkedWorktreeMatchesMain/_MainWorktreeUnchanged: widening is one-directionalentire statusfrom inside/path/to/repo/.claude/worktrees/<branch>and from real prepare-commit-msg invocation; verified hook now logsprepare-commit-msg hook invokedand reaches the session-matching layerNotes
transcript_pathshould match the file's actual storage location.UserPromptSubmitbecause the linked worktree has no.claude/settings.json(onlysettings.local.jsonwith permissions). Commits from inside the worktree therefore link to the outer session, not a dedicated sub-session. That's a separate ergonomic limitation upstream of this PR — either Claude needs to carry project hooks into worktrees, or Entire needs to install hooks at~/.claude/settings.json.agent.claudecode resolved transcript via worktree fallback), observable in.entire/logs/.