feat(binding): bind, adopt, and checkpoint sessions across repositories - #1959
feat(binding): bind, adopt, and checkpoint sessions across repositories#1959peyton-alt wants to merge 30 commits into
Conversation
New binding package, pure plumbing for activity-time session binding: a cached resolver mapping absolute evidence paths to their repo (worktree root + absolutized common dir), and a flock-guarded session record under userdirs.Config()/sessions recording which repos a session's activity has touched. Nothing reads the record yet; the additive-adopt slice consumes it. Review fixups: rev-parse now uses --path-format=absolute (symlink-canonical CommonDir preserves the clone-wide-key invariant; pre-2.31 gits fall back to a flagless path with EvalSymlinks canonicalization, selected by a once-per- process git-version probe so misses cost one fork), mutateRecord refuses to rewrite records with a newer schema Version and normalizes legacy/zero versions upward on write, RecordBinding takes an Evidence struct instead of adjacent (repo, enabled) args, the flagless fallback is pinned byte-identical to the flag path by a direct test, and the ancestor walk bound + resolver cache staleness assumptions are documented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Out-of-repo absolute paths from transcript extraction and tool-use events were silently discarded at FilterAndNormalizePaths; a collecting sibling now diverts them to the binding resolver, and evidence landing in a different clone is recorded in the machine-level session record (enabled-status noted, capped per turn, best-effort). Zero behavior change for capture: kept paths are identical, and empty-foreign turns add no IO. Review fixups: resolution attempts are bounded per invocation (a turn full of non-repo paths previously forked git once per distinct directory; now at most 16 resolutions AND at most 8 recorded repos, whichever trips first), the own-worktree skip keys on the worktree root alone (two clones cannot share a root) with the tap's git-common-dir plumbing removed entirely, the tap runs under its own binding_tap perf span instead of polluting filter_and_normalize_paths, and a deferred recover makes the never-block- capture promise hold for panics as well as errors. The resolution budget counts DISTINCT directories (filepath.Dir as a cheap proxy for the resolver's cache key), so many paths inside one foreign repo spend it once and cannot starve a later repo's evidence, and the panic log carries the stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 a99d5dd. Configure here.
There was a problem hiding this comment.
Pull request overview
Adds an observation-only “binding” layer that records evidence when an agent session touches paths outside the current repo, laying groundwork for later cross-repo adoption without changing capture behavior today.
Changes:
- Introduces a new
bindingpackage to resolve absolute evidence paths to repo identity (worktree root + canonical git common dir) with caching and git-version-aware behavior. - Adds a machine-level, versioned session record store under the user config directory to persist cross-repo bindings (timestamps, counts, enabled-at-observation).
- Taps TurnEnd and ToolUse hook paths to collect “foreign” (out-of-repo) paths and record distinct foreign repos with per-turn caps and panic containment.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/state.go | Splits path filtering into “kept” vs “foreign” to preserve existing behavior while surfacing out-of-repo evidence. |
| cmd/entire/cli/settings/settings.go | Adds IsSetUpAtRoot helper for checking .entire setup in an explicit worktree root (foreign repo enablement check). |
| cmd/entire/cli/settings/settings_test.go | Adds unit tests for IsSetUpAtRoot. |
| cmd/entire/cli/lifecycle.go | Hooks in foreign-path collection/recording for ToolUse and TurnEnd. |
| cmd/entire/cli/binding/resolve.go | New resolver that maps absolute paths to repo identity via cached git rev-parse. |
| cmd/entire/cli/binding/resolve_test.go | Tests resolver behavior (repo hits/misses, symlink invariants, worktree common-dir sharing, caching). |
| cmd/entire/cli/binding/record.go | New session record store (versioned JSON, file locking, atomic write). |
| cmd/entire/cli/binding/record_test.go | Tests record creation, upserts, version gate, and concurrent serialization behavior. |
| cmd/entire/cli/binding_tap.go | Implements the bounded, panic-contained tap that resolves/records foreign evidence. |
| cmd/entire/cli/binding_tap_test.go | Tests tap behavior (enabled/disabled repos, caps, misses bound, behavior-preservation pin). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Hooks firing with a non-repo cwd previously bailed before any evidence could be read, so parent-dir launches (~/dev/acme over sibling repos — the loudest #1098 scenario) produced nothing for binding to act on. The bail now feeds a best-effort, evidence-only path: parse the event (parsing is repo-free), extract file paths (tool-use payloads directly; turn-end transcripts via a cursor stored in the machine session record, CurrentRecordVersion 2), and record foreign repos through the existing bounded tap. No session state, no checkpoints, no logging.Init, no repo writes; non-evidence verbs cost one parse over today's silent bail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZSG8Z1VZ7J5QF6C9V55N1PG
…gle-clock mutations Three reviewed findings on the session-record store: - 019ff357-aec4: BoundRepo entries are keyed by CommonDir, but a match from a different worktree of the same clone updated Enabled/counters while leaving the embedded RepoIdentity.WorktreeRoot first-seen, making the stored (WorktreeRoot, Enabled) pair incoherent. The match branch now refreshes the whole embedded identity with the flag. - 019ff357-ecda: mutateRecord's flock acquisition blocked indefinitely on deadline-free hook contexts, contradicting the tap's never-block promise. The lock wait is now bounded at 2s (matching the strategy session-lock ceiling); on timeout callers Debug-and-skip as designed. - 019ff358-01d1: UpdatedAt and First/LastEvidenceAt came from separate time.Now calls, so UpdatedAt could predate the evidence timestamps. One clock reading per mutation is now threaded through the mutation callback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The timeout's comment claimed tests shrink it, but the lock-contention test deliberately runs the real 2s ceiling — keep the documentation honest and the value immutable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iling The single-clock check now asserts exact equality across all first-write timestamps and documents that the callback signature — not this assertion — is the real enforcement (consecutive Now() calls can share a tick). The contention test pins recordLockTimeout at its contractual 2s and bounds elapsed relative to it instead of a loose 10s window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The evidence tap only saw paths the FilterAndNormalizePaths clamp rejected as outside the session repo's root. A repo NESTED under that root is path-wise inside, so its files were always kept and never became evidence. On a machine where $HOME is itself a git repo (a common dotfiles setup), every path on the machine is inside the session repo when the agent runs from ~, so the in-repo evidence path could never fire at all. recordForeignEvidence now also receives the kept (repo-root-relative) lists and, inside its existing recover/span/budget scope, runs a stat-only ancestor walk per distinct kept directory: only when a .git entry (dir or gitfile) exists strictly between the path's directory and the session root is the path handed to the budgeted git resolver, which returns the innermost repo — the same identity the no-repo branch records for the same path. The no-nested-repos common case costs zero git forks, and nested evidence shares the existing 16-distinct-dir/8-repo per-turn budget. Repos registered in the session repo's .gitmodules are part of the parent project and excluded; unregistered nested repos (the dotfiles case) are recorded. The kept lists are only read — the capture inputs stay byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZVJD6E2D9WHJQFZGGZR3HHT
…clusion
Two review findings on the nested-repo evidence detection:
Per-root dedupe: nestedRepoEvidencePaths emitted one candidate per distinct
kept DIRECTORY, so a turn editing many directories of one busy nested repo
spent one git fork per directory — up to the whole 16-resolution budget on
redundant resolutions of the same repo (fresh hook processes have an empty
resolver cache, so every ToolUse hook paid it again), starving a second
nested repo later in the same turn. Candidates are now deduped by detected
nested root: one representative path, one fork, per repo.
Transitive submodules: only the session root's .gitmodules was consulted,
so a repo nested under a registered submodule path (the submodule's own
submodule) was recorded as foreign — against the exclusion's own "part of
the parent project" rationale. A detected nested root lying UNDER a
registered path ("/"-boundary prefix containment: vendor/sub covers
vendor/sub/inner but not the sibling vendor/subextra) is now skipped
without parsing every submodule's .gitmodules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 01KZVPYKT0WPGVB13G4F1K22EF
# Conflicts: # cmd/entire/cli/lifecycle.go
…cked mutation scanTranscriptForeign advanced the transcript scan cursor in its own record write BEFORE the evidence derived from that scan was recorded (recordForeignEvidence -> RecordBinding, one write per repo). A failed evidence write -- record-lock contention/timeout, transient I/O, a killed hook -- left the cursor past transcript lines whose evidence never persisted, and an advanced cursor means those lines are never rescanned: silent evidence loss on the no-repo path (the #1098 parent-dir scenario). Fix shape: binding.RecordEvidenceAndAdvanceCursor persists all of a scan's evidence plus the new cursor inside the single flock'd mutateRecord, so the pair is all-or-nothing. A failure writes neither; the next turn-end rescans the same span, and because nothing was recorded the retry cannot double-count -- no idempotence bookkeeping needed. scanTranscriptForeign is now a pure read returning the scan result, and resolveForeignRepos is split out of recordForeignEvidence so the no-repo turn-end path can resolve repos before its one write. Extractor-native cursor coordinates (claude=lines, gemini=message index) and reset-on-truncation semantics are unchanged; AdvanceTranscriptCursor is subsumed by the combined call and removed (no remaining callers, so the split-write pattern cannot be reached). Regression pinned by TestRecordNoRepoEvidence_FailedWriteLeavesCursorForRescan: evidence write fails (held record flock) -> cursor must not advance, and the next turn-end re-reads the same lines and records the evidence exactly once. Trail finding: 019ff775-9965-7d46-a6f1-46500d66446f Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZW6VK9T0K5CY9D5T5ABZK73
e3866e4 to
393e776
Compare
393e776 to
5f40167
Compare
…y, token attribution Review findings on trail 1018 (code-reviewer, silent-failure-hunter, pr-test-analyzer) plus the open trail threads, verified against the code: Adoption - A linked worktree of the launching clone is never an adoption target: session state lives in the git common dir, so the "target store" was the source's own store — the fast path read the source state as a replica and the replay child ran the session's hooks in a worktree it did not belong to. - The adopted marker is a hint, not the authority: the existence check now runs under the target's session-state lock for marked and unmarked repos alike, closing the unlocked-check → deleted-state → "already adopted" race. - The 2s lock timeout bounds only the lock acquire. The state build ran under it and read git status through StatusWithBudget, whose breach latch is process-wide — a slow target put the launching repo's own capture into degraded mode for the rest of the hook. The build gets an uncancellable ctx; a missing untracked baseline is logged instead of silently dropped. - MarkRepoAdopted failure (record rewritten concurrently, lock timeout) no longer withholds the replay: the replica exists, which is what the answer stands for; the failure is logged. - runRevParse cleans the resolved root as it already did the common dir. Replay - Each replayed hook child is bounded (30s, WaitDelay 2s); its stderr is captured (capped) into the error; failures log at Warn — a failed replay is a checkpoint the target repo silently missed. - A replay child without a pre-prompt baseline credits the turn only with transcript-evidenced files: the git-status fallback for Modified is withheld and New/Deleted are narrowed, so the target's pre-existing dirty edits and deletions are not attributed to the session. - Tokens: a turn attributed to another repo's checkpoint still accumulates into this repo's session-wide total (StepContext.TokensAttributedElsewhere); only the checkpoint delta skips it. Zeroing the whole step under-reported `entire status` for every non-primary repo. Logging - The no-repo hook path attaches logging.Discard(): with no file sink, Info/Warn from shared code fell through to slog's default handler — the agent's stderr. In-repo binding failures are Warn, not Debug. Tests: adoption into a globally tracked repo with the tier-off / excluded / vetoed converses, sibling worktree not adopted, marker failure still replicates, session total vs checkpoint delta, capped buffer, and the formerly evidence-only tests now commit in the target and assert adoption (an unborn HEAD made adoption fail silently while they passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 8940eac)
…s, isolated status budget
Findings from the self-review of the previous commit, fixed before pushing:
- Tokens: an attributed-elsewhere turn also excluded only the main-agent
delta; the cumulative SubagentTokens rescope ran unconditionally and wrote
that turn's subagent growth into the non-primary repo's checkpoint (and a
later primary turn re-absorbed it). The growth is now folded into
SubagentTokensBaseline and the rescope is skipped on those turns. Test
covers a primary / elsewhere / primary sequence.
- Replay children never recorded deletions: Deleted was narrowed to
transcript-evidenced paths, and transcripts never name deletions. The
replica now snapshots the target's already-dirty tracked paths at adoption
(State.DirtyTrackedFilesAtStart, from the same status walk as the untracked
baseline) and the replayed turn-end subtracts that baseline from
Modified/Deleted instead — the user's pending edit stays out, the agent's
deletion stays in. Test: TestReplayedTurnEnd_UsesReplicaDirtyBaseline...
- Adoption ran its state build under context.WithoutCancel, which bounded the
target's status walk only by the 20s StatusWalkBudget while holding the
target's session-state lock and ignoring the hook deadline and Ctrl-C. New
gitrepo.StatusWithIsolatedBudget (own budget, never arms the process-wide
latch) with a 5s budget, and the work runs under the hook's own ctx.
- binding/resolve: Clean after the emptiness guard (Clean("") is ".").
- Replay: a cancelled parent is reported as cancellation, not as a 30s timeout.
- Docs: replay rules and known limits (token loss on primary-replay failure,
per-turn lock cost).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5f40167)
…ngTurnPrimary Every bindingTurnCollector method already returns early on a nil receiver, so lifecycle callers never guard for nil; document that on the type and pin it with a test that drives selectBindingTurnPrimary and every method through a nil collector. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit f1af640)
…ngTurnPrimary Behavior-preserving: a nil collector already behaved as an empty one through the nil-safe methods; an explicit empty collector says so at the call sites. Also documents why an empty DirtyTrackedFilesAtStart means nothing was dirty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit ebabc32)
…option on a failed baseline walk Second pre-push review round on the replica baseline: - The dirty-tracked baseline was snapshotted once at adoption and never refreshed, while a replay child never gets a pre-prompt baseline (only TurnEnd is replayed) — so anything the user edited in the bound repo AFTER adoption was credited to the agent's next turn. Every replayed turn now rewrites the baselines to the post-turn tree (alongside the existing untracked repair), so the next turn measures against the tree this one left. - Deleted and dirty paths are tracked apart (DeletedTrackedFilesAtStart): a file that was merely dirty at the baseline and is deleted by the agent is a deletion, not something to subtract. - A failed baseline walk (budget, cancellation, or the process-wide latch already armed by the launching repo's own walk) no longer persists a replica with empty baselines — that would credit every pre-existing change to the session for the rest of it. Adoption returns an error and the next turn retries; the adopted marker was already written only after success. - Subagent-token baseline folding happens only for a step that carried a snapshot (a nil one cannot have grown the cumulative). - Replay: a child that exited on its own is reported with its stderr even if the parent was cancelled meanwhile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 0c1c187)
…ed adoption without the turn's own evidence Third pre-push review round: - The per-turn baseline rewrite stored the FULL post-turn dirty/deleted sets, including the files this turn was just credited with. The baseline stores paths, so a later change to such a file that no transcript names (sed in Bash, a formatter) was subtracted as "still dirty from the user" and — the shadow tree is built from the credited list — its content never reached the checkpoint. The next baseline is now post-turn dirty/deleted MINUS this turn's credited files; a skipped or failed SaveStep then re-credits them next turn instead of losing them. The rewrite is also skipped when nothing changed (ErrMutationSkip). - Adoption runs from the turn that produced the evidence, so the target's tree already holds that turn's agent work; the evidenced paths are excluded from the seeded baselines so the replay of that same turn does not subtract the agent's own deletions or new files. - cloneAdoptSourceState clones the two new slices; test nits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit b202968)
…cked baseline whole Fourth pre-push review round: - The refreshed tracked baselines are sorted before the unchanged-set check and the store: git status is a map, so without a canonical order the skip never fired past one dirty file and the persisted list churned each turn. - The untracked baseline is rewind's preserve-list, and the replay child narrows new files by transcript evidence (and prunes created ones) itself, so it is seeded whole again: evidence cannot tell "the agent created this" from "the agent edited the user's untracked file", and dropping the latter would let a later rewind delete it if the replay never ran. Only the two tracked baselines exclude the adopting turn's evidence. - Removed an unreachable ErrMutationSkip guard (MutateSessionState maps it to nil); tests cover the sorted compare and the tracked-seed exclusion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit b08e992)
Five conflicts, all resolved by keeping both sides: - strategy/strategy.go and lifecycle.go: the branch's TokensAttributedElsewhere and main's SubagentLedgerVersion are independent StepContext fields. Kept both; main's drop of MetadataDirAbs is preserved. - strategy/session_state.go: the branch made the session flock context-aware, main made it rooted. Main already has flock.AcquireContextIn, which is both. - strategy/manual_commit_git.go: both guarded the same subagent-delta block; both guards apply. - redact/betterleaks_env_test.go: both quoted the repo path, main with %q. Took main's. Notably absent: hook_registry.go and setup.go, which conflict on the global-stacked lineage. This branch carries no global-enable code, so they merge clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M29A65K8NK74V74ARVA8HP7P
…1959) The session record lives under the user config directory, outside any repo, and recordPath built that path with filepath.Join and created it with a plain os.MkdirAll. A pre-planted symlink at <config>/sessions therefore redirected both the create and the write, which is finding #1959 on this PR. Route it through a root instead: sessionsRoot opens the user config dir and creates 'sessions' with osroot.MkdirAllNoSymlink, then takes it as a checked child via osroot.SharedChild; reads go through osroot.ReadFileNoFollow so a planted link at the leaf is refused rather than followed. Carried across from the global-stacked lineage, where it was applied inside a merge commit (c334e55) rather than as its own change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M29A6N7S8C502PE33344KMBA
Carried the second half of the #1959 hardening (userdirs.ConfigDirChecked, from d18ad7a) so record.go no longer calls userdirs.Config() unchecked, which the userdirs consumer guard requires. Audited binding's git-metadata traversals in gitrepo's guard: resolve.go's rev-parse --git-common-dir/--show-toplevel (binding discovers clone identity and the owning worktree from arbitrary evidence paths) and binding_tap.go's .git-entry inspection (nested repository boundaries). Dropped MetadataDirAbs from a strategy test; main removed that StepContext field. Removed TestRecordForeignEvidence_GloballyTrackedRepoIsAdopted: it asserts that a repo with no .entire is adopted because the user-global tier covers it. This branch has no global tier -- IsSetUpAtRoot only looks for .entire files -- so the test asserts a capability that does not exist here. It belongs on the global-enable branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M29AGFZVA913QRN81RT9SWPR
ensureSessionReplicated re-checked the target repo immediately before writing to it, with a comment saying this kept 'a concurrent explicit disable an absolute veto'. It did not. The check was IsSetUpAtRoot, which only Lstats for a settings file — and `entire disable` sets enabled:false and leaves the file in place, so a disabled repo still answered true. The veto never fired. The effect: an agent touching a repository the user had turned Entire off in still got the session replicated into it, writing session state under that repo's .git and checkpointing into it. Add settings.IsEnabledAtRoot — the at-root sibling of IsSetUpAndEnabled — and use it at both binding call sites. It loads that root's merged settings and honors enabled:false, failing closed on a read error, so an unparseable repo is not one we write into either. TestRecordForeignEvidence_DisabledRepoIsNotAdopted pins all three halves of the old behavior: a disabled repo was recorded Enabled, adopted, and given replicated session state. This is pre-existing on the off-main lineage, not introduced by the restore. The global lineage lost it incidentally in ceacbb7 by switching to the repository policy classifier; when that classifier is available here it should supersede this helper, since it also honors the user-global tier and its exclude lists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M29BJBSC9GN8T3REPGV4ZS9W
1d28a57 to
5cee951
Compare
5cee951 to
7b5a649
Compare
The machine-level record store grew without bound. Every session with a turn-end writes a record under the user config directory, including one that never touches a repo — the no-repo scan advances its cursor on a successful scan to keep repeat scans cheap, so a chatty no-repo session leaves a cursor-only record behind. Nothing removed them: they live outside every repo, so neither `entire clean` nor the strategy's cleanup reaches them. PruneStaleRecords removes a record whose UpdatedAt is more than RecordRetention (30 days) old, along with the lock file mutateRecord keeps beside it — leaving the locks would let the directory grow at exactly the same rate. The window is deliberately generous against the failure that would hurt: a record removed under a live session loses its cross-repo evidence and rescans its transcript from zero, and sessions run for days. A file that will not parse as a record is left where it is; this directory is in the user's config dir. It runs in the detached session sweep, and nominates that sweep itself. The sweep is spawned when a session-state file looks like a zombie, and the machines whose store grows are exactly the ones with no zombies to find — so RetentionDue nominates a spawn once a day. The marker is written by the prune rather than by the check, so a nomination whose sweep never runs does not consume the window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M29P34ZAYQ04B41Y3NTBY1KQ
Binding asks "does Entire capture sessions here?" about a repository the agent merely touched. That question has one right name in this codebase — settings.IsActiveAtRoot, the spelling the forbidigo rule already points callers at — and the helper answering it was called IsEnabledAtRoot, which is a different and narrower question. The rename is not cosmetic. The full predicate is "the repo's own settings enable it OR global tracking covers it", and the second half needs the repository-policy classifier, which is not on this branch: there is no user-global tier here for it to consult, so the repo-level answer is the whole answer until there is one. Taking the final name now means the classifier-backed body replaces this one when the branches meet and no call site moves — the alternative was every binding call site changing at merge time, in a conflict. IsSetUpAtRoot becomes unexported with it. Presence is the first half of the predicate without its qualifier, never an answer a caller wants, which is what the forbidigo rule says; nothing outside the package used it. Documented at the helper: when the classifier arrives it also honors the tier and its exclude lists, and it resolves the repository first, so a root that is not a git repository answers false instead of being judged on stray settings files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M29QS8S3T9XAJYAX554Z1XTQ
The activation gate's two halves are each unit-tested, one on this branch and one with the classifier, and nothing covers them composed: that binding actually carries a repo the user-global tier alone makes active. That is the half no settings file can express — a globally tracked repo has no .entire, so a gate that reads settings files answers false for every repo the user asked to have tracked machine-wide, and nothing visibly breaks. Evidence is still recorded, merely as not enabled, and no session state is ever replicated. The classifier is not on this branch and neither landing order is decided, so the test probes for the capability instead of assuming it: it asks IsActiveAtRoot about a repository with no .entire while the tier is on, which only the classifier-backed implementation can answer true. Inert until the halves meet, running for real the moment they do, with nobody having to remember it. Verified both ways rather than assumed: on this branch both tests skip; on a throwaway merge with the classifier present (never committed) both run and pass, and reverting the gate to a settings-file presence check fails them. loadTargetState takes the session ID it loads instead of hardcoding one, so these tests can use their own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M29RJ42KSZ1M1M6XYW86DKSK

https://entire.io/gh/entireio/cli/trails/1018
What
Makes sessions follow the agent's activity across repositories instead of treating the hook's launch directory as permanent ownership. Covers sessions launched from a repo, from a parent/non-repo directory, repos created during a session, nested repos, linked worktrees, and cross-repo switches.
Related: #1439, #529, and #1098.
How
~/.config/entire/sessions/<session-id>.json, keyed by canonical git common-dir identity. Records evidence timestamps/counts, files, activation state, transcript cursor, and additive-adoption markers.Current scope
This PR remains independently mergeable from the global-enable stack. On its own, adoption is deliberately limited to repositories with explicit repo-level Entire setup (
.entire/settings.jsonor equivalent local activation).When composed with the global-enable stack (#1953 → #1954 → #1955 → #1957 → #2030), the remaining integration is to let the foreign-repo eligibility check recognize global-tier activation and trigger the stack's lazy invisible setup before replication. That interaction is not claimed by this independent PR.
Still tracked separately:
Verification
At the current head, lint, unit tests, integration shards, Windows tests, binary-size/license checks, and both deterministic canary jobs pass. The binding-specific tests cover canonical identity, bounded resolution, no-repo cursor atomicity, additive/idempotent replication, multi-repo replay, primary token attribution, and state-lock timeout propagation.
🤖 Generated with Claude Code and Codex