gastownhall/beads · error
a durable sweep requires a closed-before cutoff or an id pat
Error message
a durable sweep requires a closed-before cutoff or an id pattern; pass the pattern "*" to sweep every closed issue deliberately
What it means
ValidateSweepRequest refuses a durable-tier sweep that has neither a ClosedBefore cutoff nor an IDPattern. This is a safety invariant (deliberately enforced in the shared validator, not the CLI): a durable sweep with no filter would delete every closed durable issue, so the caller must state a cutoff or an explicit pattern — "*" acknowledges the intent to sweep everything closed.
Source
Thrown at internal/workapi/sweep.go:51
case "":
return fmt.Errorf("%w: sweep requires a tier (%q or %q), and has no default",
issueops.ErrValidation, issueops.SweepEphemeral, issueops.SweepDurable)
default:
return fmt.Errorf("%w: %q is not a sweep tier; use %q or %q",
issueops.ErrValidation, in.Tier, issueops.SweepEphemeral, issueops.SweepDurable)
}
if in.IDPattern != "" {
// filepath.Match reports a malformed pattern on any subject, so one
// probe against the empty string classifies the pattern itself. The
// front doors used to discard this error, which turned `--pattern '['`
// into "nothing matched" on a command whose job is to delete matches.
if _, err := filepath.Match(in.IDPattern, ""); err != nil {
return fmt.Errorf("%w: --pattern %q is not a valid glob: %v",
issueops.ErrValidation, in.IDPattern, err)
}
}
if in.Tier == issueops.SweepDurable && in.ClosedBefore == nil && in.IDPattern == "" {
return fmt.Errorf("%w: a durable sweep requires a closed-before cutoff or an id pattern; "+
"pass the pattern \"*\" to sweep every closed issue deliberately",
issueops.ErrValidation)
}
return nil
}
// BuildSweepCandidateFilter turns a sweep request into the storage-level
// filter that selects its CANDIDATES: the closed rows of one tier, bounded by
// the cutoff.
//
// The pattern is deliberately NOT in the filter. Globs are matched in Go
// (MatchesSweepPattern) because a LIKE translation would silently disagree with
// filepath.Match on `[...]` and on the escape rules — a disagreement that, on
// this operation, decides which rows are deleted.
//
// Call ValidateSweepRequest first; this builder assumes a validated request
// and does not re-refuse one.
func BuildSweepCandidateFilter(in issueops.SweepRequest) types.IssueFilter {View on GitHub (pinned to 71377f2769)
Solutions
- Pass ClosedBefore (a *time.Time cutoff) so only issues closed before it are swept
- Pass an IDPattern to scope which closed issues are eligible; use "*" to deliberately sweep every closed issue
- Switch to issueops.SweepEphemeral if the unfiltered intent was really about ephemeral wisps
- Handle errors.Is(err, issueops.ErrValidation) and prompt for a cutoff or pattern
Example fix
// before
req := issueops.SweepRequest{Tier: issueops.SweepDurable} // no filter
// after
cutoff := time.Now().AddDate(0, -6, 0)
req := issueops.SweepRequest{Tier: issueops.SweepDurable, ClosedBefore: &cutoff}
// or, deliberately sweep all closed:
req := issueops.SweepRequest{Tier: issueops.SweepDurable, IDPattern: "*"} Defensive patterns
Strategy: validation
Validate before calling
if req.Tier == issueops.SweepDurable && req.ClosedBefore == nil && req.IDPattern == "" {
return fmt.Errorf("durable sweep requires a closed-before cutoff or an id pattern (use \"*\" to sweep all)")
} Type guard
func durableSweepIsScoped(req issueops.SweepRequest) bool {
return req.Tier != issueops.SweepDurable || req.ClosedBefore != nil || req.IDPattern != ""
} Try / catch
if err := sweeper.Sweep(ctx, req); err != nil {
if errors.Is(err, issueops.ErrValidation) {
return fmt.Errorf("unscoped durable sweep refused: %w", err)
}
return err
} Prevention
- Always pass a cutoff or pattern for durable sweeps; make it a required flag in wrappers
- Use ClosedBefore with a conservative (recent) cutoff for routine pruning
- Reserve IDPattern "*" for the explicit, deliberate sweep-everything case
- Add integration tests asserting an unscoped durable sweep is refused
When it happens
Trigger: Calling a Sweeper with issueops.SweepRequest{Tier: issueops.SweepDurable, ClosedBefore: nil, IDPattern: ""} — a durable sweep with no scope at all.
Common situations: A caller wanted 'prune old closed issues' but forgot to pass --closed-before; a wrapper built the request programmatically and left both filters unset; a user expected the ephemeral-tier behavior (no filter required) to apply to durable sweeps.
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
- sweep requires a tier (%q or %q), and has no default
- %q is not a sweep tier; use %q or %q
- --pattern %q is not a valid glob: %v
- no store is open for this workspace
- not found
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/ed4820d88a6a4f41.
Report an issue: GitHub.