{"record":{"id":"5fd08b6d2529e838","repo":"gastownhall/beads","slug":"registered-target-identity-changed","errorCode":null,"errorMessage":"registered target identity changed","messagePattern":"registered target identity changed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/worktree_cmd.go","lineNumber":1401,"sourceCode":"\nfunc (plan *worktreeRemovalPlan) observeRevalidation(ctx context.Context) worktreeRevalidationObservation {\n\tfacts := worktreeremove.RevalidationFacts{}\n\tworktrees, err := listRegisteredWorktrees(ctx, plan.git, plan.executionRoot)\n\tif err != nil {\n\t\treturn worktreeRevalidationObservation{facts: facts, err: err}\n\t}\n\tcurrentEntry, found := findRegisteredWorktreeByPath(worktrees, plan.target.path)\n\tif !found {\n\t\tfacts.Registration = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target is no longer registered at %s\", plan.target.path)}\n\t}\n\tif worktreeRegistryIdentity(currentEntry) != plan.target.registryID {\n\t\tfacts.Registration = worktreeremove.InvariantChanged\n\t\tif currentEntry.locked != plan.target.locked || currentEntry.lockReason != plan.target.lockReason ||\n\t\t\tcurrentEntry.prunable != plan.target.prunable || currentEntry.pruneReason != plan.target.pruneReason {\n\t\t\tfacts.LockPrune = worktreeremove.InvariantChanged\n\t\t}\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"registered target identity changed\")}\n\t}\n\tfacts.Registration = worktreeremove.InvariantStable\n\tfacts.LockPrune = worktreeremove.InvariantStable\n\tfacts.TargetPath = worktreeremove.InvariantStable\n\n\tcurrentTarget, err := inspectWorktreeTarget(ctx, plan.git, currentEntry)\n\tif err != nil {\n\t\treturn worktreeRevalidationObservation{facts: facts, err: err}\n\t}\n\tif !sameWorktreePath(currentTarget.gitDir, plan.target.gitDir) {\n\t\tfacts.GitAdminDirectory = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target git directory changed\")}\n\t}\n\tfacts.GitAdminDirectory = worktreeremove.InvariantStable\n\tif !sameWorktreePath(currentTarget.commonDir, plan.commonDir) {\n\t\tfacts.CommonDirectory = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target common git directory changed\")}\n\t}","sourceCodeStart":1383,"sourceCodeEnd":1419,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/worktree_cmd.go#L1383-L1419","documentation":"During revalidation, bd recomputes a stable identity for the registered target worktree (path plus lock/lockReason/prunable/pruneReason). If it differs from the identity captured when the plan was built, the registry entry was mutated underneath bd — for example the worktree was locked, unlocked, or became prunable — and bd aborts rather than removing a worktree under changed conditions.","triggerScenarios":"`git worktree lock`/`unlock`/`prune` or `bd worktree lock/unlock` executed on the target between building the removal plan and executing it; another process altering the worktree's prune state (e.g. the directory temporarily disappearing and reappearing).","commonSituations":"A teammate locked the worktree while you were removing it; an IDE or agent locked worktrees automatically; a prune-capable state appeared because the directory was briefly renamed or unmounted.","solutions":["Re-run `bd worktree remove` to build a fresh plan reflecting the new registry state","Check `git worktree list --porcelain` and compare lock/prune flags; unlock with `git worktree unlock <path>` if locking blocks the removal","Avoid concurrent lock/unlock/prune operations on the same worktree while a removal is in flight"],"exampleFix":"// before: someone locked the worktree mid-plan\n$ bd worktree remove feature\n// error: registered target identity changed\n// after\n$ git worktree unlock /wt/feature\n$ bd worktree remove feature   # rebuild plan against current state","handlingStrategy":"retry","validationCode":"git worktree list --porcelain | grep -A3 '^worktree /wt/feature$' | grep -q '^locked' && { echo 'worktree locked; unlock first'; exit 1; }","typeGuard":"func identityStable(before, after worktreeEntry) bool {\n\treturn before.locked == after.locked && before.lockReason == after.lockReason &&\n\t\tbefore.prunable == after.prunable && before.pruneReason == after.pruneReason\n}","tryCatchPattern":"obs := revalidateWorktreePlan(ctx, plan)\nif obs.err != nil && strings.Contains(obs.err.Error(), \"identity changed\") {\n\tplan, err = buildWorktreeRemovalPlan(ctx, git, name, nil) // rebuild and retry once\n}","preventionTips":["Avoid lock/unlock/prune during an in-flight removal","Coordinate with teammates/agents working on the same worktree","Check lock state with `git worktree list --porcelain` before removing","Rebuild plans instead of reusing cached ones across sessions"],"tags":["git","worktree","race-condition","locking"],"backgroundTag":"stale-worktree-state","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}