gastownhall/beads · error
assignee must not be empty for an assignee-scoped summary
Error message
assignee must not be empty for an assignee-scoped summary
What it means
ValidateStatsAssignee refuses an empty or whitespace-only assignee when building an assignee-scoped summary. An assignee is an opaque identifier this layer has no vocabulary for, so it is returned UNCHANGED when accepted — trimming would silently answer for a different actor than the caller named. There is no default assignee to fall back to.
Source
Thrown at internal/workapi/stats.go:28
// The shared half of issueops.StatsReporter: the assignee-scoped question,
// which every implementation answers by asking storage two questions and
// folding the answers, rather than by running one aggregate.
//
// The workspace-wide question needs nothing here — it is a single seam call on
// every backend. This file exists because the assignee-scoped answer is
// ASSEMBLED, and assembling it twice is how the two front doors would come to
// disagree about what "your work" means.
// ValidateStatsAssignee resolves the actor an assignee-scoped summary answers
// for, refusing an empty or whitespace-only one with ErrValidation.
//
// It returns the value UNCHANGED when it accepts: an assignee is an opaque
// identifier this layer has no vocabulary for, so trimming it would silently
// answer for a different actor than the caller named.
func ValidateStatsAssignee(assignee string) (string, error) {
if strings.TrimSpace(assignee) == "" {
return "", fmt.Errorf("assignee must not be empty for an assignee-scoped summary%.0w", issueops.ErrValidation)
}
return assignee, nil
}
// BuildStatsAssigneeIssueFilter is the predicate that selects one actor's rows
// for the fold below.
//
// It carries the assignee and NOTHING else, which is a decision and not an
// omission: no status restriction (the fold tallies every status, including
// closed), no limit (a capped scan would silently under-report a busy actor's
// total), and no wisp suppression — the search seam merges the ephemeral tier
// unless told not to.
func BuildStatsAssigneeIssueFilter(assignee string) types.IssueFilter {
return types.IssueFilter{Assignee: &assignee}
}
// BuildStatsAssigneeWorkFilter is the ready-work predicate for the same actor.
// It is a second filter type because ready work is a different question withView on GitHub (pinned to 71377f2769)
Solutions
- Pass a non-empty assignee identifier, or skip the assignee-scoped summary entirely when no assignee was requested
- Guard at the call site: only build an assignee-scoped summary when an assignee was actually provided
- Compare against errors.Is(err, issueops.ErrValidation) and prompt the user for the assignee
Example fix
// before
assignee, err := workapi.ValidateStatsAssignee(flagAssignee) // flagAssignee == ""
// after
if flagAssignee != "" {
assignee, err = workapi.ValidateStatsAssignee(flagAssignee)
} else {
// build an unscoped summary instead
} Defensive patterns
Strategy: validation
Validate before calling
if strings.TrimSpace(assignee) == "" {
return fmt.Errorf("assignee-scoped summary requires a non-empty assignee")
} Type guard
func isScopedSummary(assignee string) bool {
return strings.TrimSpace(assignee) != ""
} Try / catch
a, err := workapi.ValidateStatsAssignee(assignee)
if err != nil {
if errors.Is(err, issueops.ErrValidation) {
return fmt.Errorf("assignee required: %w", err)
}
return err
} Prevention
- Only request an assignee-scoped summary when an assignee was actually provided; use the unscoped path otherwise
- Treat assignee as an opaque string — never trim, default, or normalize it yourself
- Check flag/env sources for emptiness before passing them through
When it happens
Trigger: Calling workapi.ValidateStatsAssignee("") or ValidateStatsAssignee(" ") — directly or via a stats role that scopes a summary to one actor.
Common situations: A CLI handler passed an unset --assignee flag value straight through; a config/env lookup for the current user returned empty; a caller assumed an empty string meant 'all assignees' rather than omitting the scope.
Understand the failure class
Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.
Related errors
- no store is open for this workspace
- not found
- no absolute native user directory is available
- ExternalDoltConfig: set either Socket OR (Host, Port), not b
- ExternalDoltConfig: must set Socket or (Host, Port)
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/b616c2ea4ebae29f.
Report an issue: GitHub.