gastownhall/beads · error

close: id must not be empty

Error message

close: id must not be empty

What it means

Guard clause in closeChecked: the issue/wisp ID argument is the empty string, so there is nothing to close. Pure input validation — no storage access happens.

Source

Thrown at internal/storage/domain/issue.go:1665

}

func (u *issueUseCaseImpl) CloseWisp(ctx context.Context, id string, params CloseIssueParams, actor string) (CloseIssueResult, error) {
	return u.close(ctx, id, params, actor, true)
}

// CloseIssueChecked closes an issue through the shared guarded close path.
func (u *issueUseCaseImpl) CloseIssueChecked(ctx context.Context, id string, params CloseIssueParams, actor string, force bool) (CloseIssueResult, error) {
	return u.closeChecked(ctx, id, params, actor, force, false)
}

// CloseWispChecked is the wisp twin of CloseIssueChecked.
func (u *issueUseCaseImpl) CloseWispChecked(ctx context.Context, id string, params CloseIssueParams, actor string, force bool) (CloseIssueResult, error) {
	return u.closeChecked(ctx, id, params, actor, force, true)
}

func (u *issueUseCaseImpl) closeChecked(ctx context.Context, id string, params CloseIssueParams, actor string, force, useWisp bool) (CloseIssueResult, error) {
	if id == "" {
		return CloseIssueResult{}, fmt.Errorf("close: id must not be empty")
	}
	if actor == "" {
		return CloseIssueResult{}, fmt.Errorf("close: actor must not be empty")
	}
	row, err := u.issueRepo.CloseChecked(ctx, id, CloseRowParams{Reason: params.Reason, Session: params.Session}, actor, force)
	if err != nil {
		return CloseIssueResult{}, fmt.Errorf("close %s: %w", id, err)
	}
	issue, err := u.issueRepo.Get(ctx, id, IssueTableOpts{UseWispsTable: row.IsWisp || useWisp})
	if err != nil {
		return CloseIssueResult{}, fmt.Errorf("close %s: reload: %w", id, err)
	}
	return CloseIssueResult{Issue: issue, Closed: !row.AlreadyClosed, OpenChildren: row.OpenChildren}, nil
}

func (u *issueUseCaseImpl) close(ctx context.Context, id string, params CloseIssueParams, actor string, useWisp bool) (CloseIssueResult, error) {
	if id == "" {
		return CloseIssueResult{}, fmt.Errorf("close: id must not be empty")

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure the caller supplies a non-empty issue ID (e.g. bd-123).
  2. Guard scripts: `[ -n "$ID" ] || exit 1` before invoking close.
  3. Check the upstream command that produced the ID for silent failure.
  4. Validate parsed input before constructing CloseIssueParams.

Example fix

// before
uc.CloseIssueChecked(ctx, id, params, actor, false)

// after
if id == "" {
    return fmt.Errorf("cannot close: issue id is empty")
}
uc.CloseIssueChecked(ctx, id, params, actor, false)
Defensive patterns

Strategy: validation

Validate before calling

func validateCloseInput(id, actor string) error {
    if id == "" {
        return errors.New("close: issue id is required")
    }
    if actor == "" {
        return errors.New("close: actor is required")
    }
    return nil
}

Try / catch

if err != nil {
    if strings.Contains(err.Error(), "id must not be empty") {
        return fmt.Errorf("caller bug: no issue ID supplied; check upstream command output: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling CloseIssueChecked / CloseWispChecked (or CloseIssue/CloseWisp via close) with id="" — e.g. an unset variable, empty parse result, or upstream failure that produced an empty identifier.

Common situations: Shell scripts where `bd close "$ID"` has unset ID; automation that reads issue IDs from a prior command that returned nothing; JSON templates with missing id field.

Related errors


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