gastownhall/beads · error · ErrAmbiguousID

%w: %q matches %d issues: %v Use more characters to disambig

Error message

%w: %q matches %d issues: %v
Use more characters to disambiguate

What it means

The partial ID matched more than one issue, so ResolvePartialID refuses to guess and returns an error wrapping ErrAmbiguousID (detectable with errors.Is). The message lists every matching ID (sorted deterministically) and asks for a longer fragment. This is by design to prevent operating on the wrong issue.

Source

Thrown at internal/utils/id_parser.go:224

				}
			}
			if exactMatch != "" {
				return exactMatch, nil
			}
		}
	}

	if len(matches) == 0 {
		return "", fmt.Errorf("no issue found matching %q", input)
	}

	// Sort so the ambiguity error lists IDs deterministically. SearchIssues return
	// order is not a contract for ambiguous matches, so sorting by ID pins the same
	// message for every storage implementation.
	sort.Strings(matches)

	if len(matches) > 1 {
		return "", fmt.Errorf("%w: %q matches %d issues: %v\nUse more characters to disambiguate", ErrAmbiguousID, input, len(matches), matches)
	}

	return matches[0], nil
}

func partialIDSearchPart(hashPart string) (string, bool) {
	if !looksLikePartialIDHash(hashPart) {
		return "", false
	}
	searchPart := hashPart
	if idx := strings.LastIndex(hashPart, "-"); idx >= 0 && idx < len(hashPart)-1 {
		suffix := hashPart[idx+1:]
		if looksLikePartialIDHash(suffix) {
			searchPart = suffix
		}
	}
	return searchPart, true
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Use more characters of the hash until exactly one match remains (as the error suggests)
  2. Use the full ID copied from `bd show` or `bd list` output
  3. Filter first with `bd search <keywords>` to narrow to one issue, then use its ID
  4. In code, test with errors.Is(err, utils.ErrAmbiguousID) and prompt the user to choose from the listed matches

Example fix

// before
id, err := utils.ResolvePartialID(ctx, store, "a3", cfg) // ambiguous
// after
id, err := utils.ResolvePartialID(ctx, store, "a3f8e9", cfg) // longer fragment
if errors.Is(err, utils.ErrAmbiguousID) {
    return fmt.Errorf("pick one: %v", err)
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Pre-check uniqueness before resolving:
ids, err := store.SearchIssueIDs(ctx, hashFragment, types.IssueFilter{})
if err == nil && len(ids) > 1 {
    return fmt.Errorf("fragment %q is ambiguous: %v", hashFragment, ids)
}

Type guard

func isAmbiguousID(err error) bool {
    return errors.Is(err, utils.ErrAmbiguousID)
}

Try / catch

id, err := utils.ResolvePartialID(ctx, store, frag, cfg)
if isAmbiguousID(err) {
    // parse the listed matches and ask the user to pick
    return interactivePick(err.Error())
}
if err != nil { return err }

Prevention

When it happens

Trigger: Calling ResolvePartialID with a hash fragment that is a leading prefix of two or more issue hashes — e.g. "a3" when both "bd-a3f8e9" and "bd-a3f11" exist. Only leading-prefix matches count (HasPrefix), so interior substrings do not trigger this.

Common situations: Using 1-2 character abbreviations in scripts on databases with thousands of issues; copy-pasting only the first few characters of a hash; a freshly created issue whose hash happens to share a prefix with an existing one.

Related errors


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