gastownhall/beads · error

--reason-file %q is empty; close reason is required

Error message

--reason-file %q is empty; close reason is required

What it means

After successfully reading --reason-file, resolveReasonFile rejects content that is empty or whitespace-only, since a close reason is mandatory. This catches silent failures like an empty file or a file containing only newlines.

Source

Thrown at cmd/bd/close.go:684

// Returns (content, true, nil) when --reason-file was set and read successfully.
// Returns (_, false, nil) when --reason-file was not set.
// Returns an error on conflict with an existing reason, file read failure, or empty content.
// Mirrors the --body-file pattern from `bd create` so agents can pass structured close
// templates without shell-escaping hell.
func resolveReasonFile(cmd *cobra.Command, hasExistingReason bool) (string, bool, error) {
	if !cmd.Flags().Changed("reason-file") {
		return "", false, nil
	}
	if hasExistingReason {
		return "", false, fmt.Errorf("cannot specify both --reason-file and --reason/--resolution/--message/--comment")
	}
	path, _ := cmd.Flags().GetString("reason-file")
	content, err := readBodyFile(path)
	if err != nil {
		return "", false, fmt.Errorf("reading reason file %q: %w", path, err)
	}
	if strings.TrimSpace(content) == "" {
		return "", false, fmt.Errorf("--reason-file %q is empty; close reason is required", path)
	}
	return content, true, nil
}

// resolveCloseTargets resolves a batch of partial issue IDs for `bd close`,
// preserving input order. For each ID it tries the local store first, then
// explicit prefix routing via routes.jsonl, then a shared contributor-routed
// store. This matches resolveAndGetIssueWithRouting's routing precedence.
//
// The contributor-routed handle is shared across the batch so bulk close does
// not repeatedly open the same planning store and every result has a clear store
// owner for subsequent close-time checks and writes.
//
// Each returned RoutedResult.Store points to whichever store actually owns the
// issue. The caller invokes cleanup() once when done; per-result Close() is a
// no-op for routed-via-shared-handle entries because they don't own the handle.
func resolveCloseTargets(ctx context.Context, localStore storage.DoltStorage, ids []string) ([]*RoutedResult, func(), error) {
	results := make([]*RoutedResult, 0, len(ids))

View on GitHub (pinned to 71377f2769)

Solutions

  1. Populate the reason file with non-empty text and retry.
  2. Check the upstream command that generated the file for silent failures.
  3. Fall back to an inline --reason for this invocation.

Example fix

// before
bd close bd-1 --reason-file empty.txt
// error: --reason-file "empty.txt" is empty
// after
echo "completed in sprint 12" > reason.txt && bd close bd-1 --reason-file reason.txt
Defensive patterns

Strategy: validation

Validate before calling

[ -s "$REASON_FILE" ] || { echo "reason file is empty" >&2; exit 1; }
# -s fails on zero-size; also check whitespace:
grep -q '[^[:space:]]' "$REASON_FILE" || exit 1

Try / catch

if err := runClose(...); err != nil {
	if strings.Contains(err.Error(), "is empty; close reason is required") {
		// regenerate the reason file or fall back to --reason
	}
}

Prevention

When it happens

Trigger: --reason-file points to an existing but empty (or whitespace-only) file, including output of a failed pipeline redirected into the file.

Common situations: CI step writing the reason failed silently and produced an empty file; an editor saved a blank note; stdin redirected from an empty stream with `--reason-file -`.

Related errors


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