{"record":{"id":"aa177c49bd51bec3","repo":"gastownhall/beads","slug":"cannot-close-s-assignee-is-q-actor-is-q-recl","errorCode":null,"errorMessage":"cannot close %s: assignee is %q, actor is %q; reclaim or use --force to override","messagePattern":"cannot close (.+?): assignee is %q, actor is %q; reclaim or use --force to override","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/validation/issue.go","lineNumber":173,"sourceCode":"// CanonicalActor, so two principals sharing one canonical actor name both\n// pass — including the same principal spelled two different ways by two\n// different layers of Gas Town (ga-wzl83). bd has no identity layer, so this\n// matches the existing semantics of the actor field — the guard removes\n// silent cross-actor closes without adding new identity guarantees.\n//\n// This guards against the silent-success bug where actor A closes a bead that\n// was concurrently re-claimed by actor B: storage accepts the close (id-only\n// WHERE clause), so without this check bd reports \"✓ Closed\" even though A had\n// no authority over the bead. (be-035)\nfunc AssigneeMatches(actor string, force bool) IssueValidator {\n\treturn func(id string, issue *types.Issue) error {\n\t\tif issue == nil || force {\n\t\t\treturn nil\n\t\t}\n\t\tif issue.Assignee == \"\" || ActorMatches(issue.Assignee, actor) {\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"cannot close %s: assignee is %q, actor is %q; reclaim or use --force to override\", id, issue.Assignee, actor)\n\t}\n}\n\n// AssigneeNotStolen validates that a plain assignee update does not silently\n// overwrite another actor's live claim (bd-98s5c). newAssignee is the value\n// the write would set (may be \"\" for an unassign — fenced the same way, since\n// stripping a live claim is what bd unclaim refuses without --force).\n//\n// The refusal fires only when every clause holds; each one is load-bearing:\n//   - Assignee != \"\"                    — unassigned issues are freely\n//     assignable.\n//   - Assignee doesn't canonicalize to actor       — an actor editing its\n//     own claim is untouched, under any spelling of its own identity.\n//   - Assignee doesn't canonicalize to newAssignee — an idempotent re-assert\n//     of the current holder stays a success (retry/replay safety on the\n//     proxied path), even when the re-assert names the holder under a\n//     different spelling.\n//   - Status == in_progress   — reassigning an OPEN bead stays frictionless:","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/validation/issue.go#L155-L191","documentation":"The close-assignee validator prevents an actor from closing an issue assigned to someone else. If the issue has a non-empty assignee that does not match the acting identity, the close is rejected until the actor reclaims or forces.","triggerScenarios":"Running 'bd close <id>' (without --force) where issue.Assignee is set, non-empty, and ActorMatches(assignee, actor) is false.","commonSituations":"Two agents on the same issue: one finishes and closes while the other still holds the claim; a human closing an agent's in-progress issue; identity configured differently (different actor string) than the one that claimed the issue.","solutions":["Run 'bd reclaim <id>' to take over the claim, then close","Pass --force if you are certain the assignee's work is done or abandoned","Close as the correct actor (match the assignee identity, e.g. same agent name)","Coordinate with the assignee (bd mail) before closing"],"exampleFix":"// before\nbd close bd-42  // actor=alice, assignee=agent-1 → blocked\n// after\nbd reclaim bd-42\nbd close bd-42","handlingStrategy":"validation","validationCode":"if issue.Assignee != \"\" && issue.Assignee != currentActor {\n    return fmt.Errorf(\"%s is claimed by %s; run 'bd reclaim' first\", issue.ID, issue.Assignee)\n}","typeGuard":"func actorOwns(issue *types.Issue, actor string) bool { return issue != nil && (issue.Assignee == \"\" || issue.Assignee == actor) }","tryCatchPattern":"if err := closeIssue(id); err != nil {\n    if strings.Contains(err.Error(), \"assignee is\") {\n        if err := reclaim(id); err != nil { return err }\n        return closeIssue(id)\n    }\n    return err\n}","preventionTips":["Always reclaim before operating on issues claimed by others","Keep your actor identity consistent across sessions and scripts","Coordinate via 'bd mail' when multiple agents share an issue"],"tags":["validation","assignee","concurrency","claim"],"backgroundTag":"assignee-mismatch","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}