{"record":{"id":"cf432a9aad62ae57","repo":"gastownhall/beads","slug":"target-cleanliness-changed","errorCode":null,"errorMessage":"target cleanliness changed","messagePattern":"target cleanliness changed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/worktree_cmd.go","lineNumber":1433,"sourceCode":"\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}\n\tfacts.CommonDirectory = worktreeremove.InvariantStable\n\tif currentTarget.headOID != plan.target.headOID {\n\t\tfacts.Head = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target HEAD changed from %s to %s\", plan.target.headOID, currentTarget.headOID)}\n\t}\n\tfacts.Head = worktreeremove.InvariantStable\n\tif currentTarget.status != plan.target.status {\n\t\tif (currentTarget.status == \"\") != (plan.target.status == \"\") {\n\t\t\tfacts.Cleanliness = worktreeremove.InvariantChanged\n\t\t} else {\n\t\t\tfacts.Cleanliness = worktreeremove.InvariantStable\n\t\t}\n\t\tfacts.StatusBytes = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target cleanliness changed\")}\n\t}\n\tfacts.Cleanliness = worktreeremove.InvariantStable\n\tfacts.StatusBytes = worktreeremove.InvariantStable\n\tif currentTarget.statusFingerprint != plan.target.statusFingerprint {\n\t\tfacts.DirtyFileFingerprint = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target changed files changed\")}\n\t}\n\tfacts.DirtyFileFingerprint = worktreeremove.InvariantStable\n\tif !plan.force && currentTarget.status != \"\" {\n\t\tfacts.Cleanliness = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target is no longer clean\")}\n\t}\n\tif !os.SameFile(currentTarget.pathInfo, plan.target.pathInfo) ||\n\t\t!samePinnedFileMetadata(currentTarget.pathInfo, plan.target.pathInfo) {\n\t\tfacts.TargetDirectory = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target directory identity changed\")}\n\t}\n\tfacts.TargetDirectory = worktreeremove.InvariantStable","sourceCodeStart":1415,"sourceCodeEnd":1451,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/worktree_cmd.go#L1415-L1451","documentation":"This error is thrown by bd's worktree revalidation when the target worktree's cleanliness transitioned relative to the plan: `git status` output was empty at plan time and non-empty at execution time (or vice versa). bd records this as the Cleanliness/StatusBytes invariant changing and aborts the operation because the planned snapshot no longer matches reality.","triggerScenarios":"Executing a planned worktree operation when the presence/absence of `git status` output on the target flipped between plan and execution — files were modified/created/deleted in an initially clean worktree, or a previously dirty worktree was cleaned (checkout/reset/clean) in the meantime.","commonSituations":"An editor or build tool wrote files into the worktree after the plan was made; the user ran `git checkout .`/`git clean` making a dirty tree clean; a concurrent agent committed or reverted changes; long gap between planning and executing in a scripted flow while the worktree was actively used.","solutions":["Re-run the bd command so it re-snapshots the current worktree state","Inspect `git -C <worktree> status` and stabilize the tree (commit, stash, or clean) so it matches the planned cleanliness","If the state change is expected and acceptable, pass the force flag to bypass the invariant check","Investigate what process modified the worktree between plan and execution and serialize access"],"exampleFix":"// before: worktree dirtied after planning\nplan := snapshot(target) // clean\n// ... build writes temp files into worktree ...\nexecute(plan) // error: target cleanliness changed\n// after: keep the worktree clean or re-snapshot\nrun(\"git\", \"-C\", wt, \"status\", \"--porcelain\") == \"\" || run(\"git\", \"-C\", wt, \"clean\", \"-fd\")\nplan := snapshot(target) // fresh\nexecute(plan)","handlingStrategy":"validation","validationCode":"out, _ := exec.Command(\"git\", \"-C\", wt, \"status\", \"--porcelain\").Output()\nwasClean := plan.Target.Status == \"\"\nisClean := len(out) == 0\nif wasClean != isClean {\n    // cleanliness flipped; re-plan before calling bd\n}","typeGuard":"func cleanlinessMatches(plannedStatus string, wt string) bool {\n    out, err := exec.Command(\"git\", \"-C\", wt, \"status\", \"--porcelain\").Output()\n    if err != nil { return false }\n    return (len(out) == 0) == (plannedStatus == \"\")\n}","tryCatchPattern":"err := bdRemoveWorktree(plan)\nif err != nil && strings.Contains(err.Error(), \"target cleanliness changed\") {\n    plan = rebuildPlan(plan.Target)\n    err = bdRemoveWorktree(plan)\n}","preventionTips":["Keep the worktree clean (commit or stash) before planning bd operations","Disable build/codegen or formatters that write into the worktree during the operation window","Minimize the gap between plan creation and execution","Re-snapshot rather than reusing a stale plan across sessions"],"tags":["git","worktree","dirty-worktree","stale-state","race-condition"],"backgroundTag":"git-worktree-state-changed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}