gastownhall/beads · error

cannot close %s: assignee is %q, actor is %q; reclaim or use

Error message

cannot close %s: assignee is %q, actor is %q; reclaim or use --force to override

What it means

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.

Source

Thrown at internal/validation/issue.go:173

// CanonicalActor, so two principals sharing one canonical actor name both
// pass — including the same principal spelled two different ways by two
// different layers of Gas Town (ga-wzl83). bd has no identity layer, so this
// matches the existing semantics of the actor field — the guard removes
// silent cross-actor closes without adding new identity guarantees.
//
// This guards against the silent-success bug where actor A closes a bead that
// was concurrently re-claimed by actor B: storage accepts the close (id-only
// WHERE clause), so without this check bd reports "✓ Closed" even though A had
// no authority over the bead. (be-035)
func AssigneeMatches(actor string, force bool) IssueValidator {
	return func(id string, issue *types.Issue) error {
		if issue == nil || force {
			return nil
		}
		if issue.Assignee == "" || ActorMatches(issue.Assignee, actor) {
			return nil
		}
		return fmt.Errorf("cannot close %s: assignee is %q, actor is %q; reclaim or use --force to override", id, issue.Assignee, actor)
	}
}

// AssigneeNotStolen validates that a plain assignee update does not silently
// overwrite another actor's live claim (bd-98s5c). newAssignee is the value
// the write would set (may be "" for an unassign — fenced the same way, since
// stripping a live claim is what bd unclaim refuses without --force).
//
// The refusal fires only when every clause holds; each one is load-bearing:
//   - Assignee != ""                    — unassigned issues are freely
//     assignable.
//   - Assignee doesn't canonicalize to actor       — an actor editing its
//     own claim is untouched, under any spelling of its own identity.
//   - Assignee doesn't canonicalize to newAssignee — an idempotent re-assert
//     of the current holder stays a success (retry/replay safety on the
//     proxied path), even when the re-assert names the holder under a
//     different spelling.
//   - Status == in_progress   — reassigning an OPEN bead stays frictionless:

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run 'bd reclaim <id>' to take over the claim, then close
  2. Pass --force if you are certain the assignee's work is done or abandoned
  3. Close as the correct actor (match the assignee identity, e.g. same agent name)
  4. Coordinate with the assignee (bd mail) before closing

Example fix

// before
bd close bd-42  // actor=alice, assignee=agent-1 → blocked
// after
bd reclaim bd-42
bd close bd-42
Defensive patterns

Strategy: validation

Validate before calling

if issue.Assignee != "" && issue.Assignee != currentActor {
    return fmt.Errorf("%s is claimed by %s; run 'bd reclaim' first", issue.ID, issue.Assignee)
}

Type guard

func actorOwns(issue *types.Issue, actor string) bool { return issue != nil && (issue.Assignee == "" || issue.Assignee == actor) }

Try / catch

if err := closeIssue(id); err != nil {
    if strings.Contains(err.Error(), "assignee is") {
        if err := reclaim(id); err != nil { return err }
        return closeIssue(id)
    }
    return err
}

Prevention

When it happens

Trigger: Running 'bd close <id>' (without --force) where issue.Assignee is set, non-empty, and ActorMatches(assignee, actor) is false.

Common situations: 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.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/aa177c49bd51bec3. Report an issue: GitHub.