gastownhall/beads · error

%s

Error message

%s

What it means

This error surfaces the missingExplicitStateMapMessage: pushing to Linear requires explicit linear.state_map.* entries because defaults are safe for pulling but too ambiguous for mutations. It fires when config is nil or has no ExplicitStateMap entries, even though the state cache was populated.

Source

Thrown at internal/linear/mapping.go:380

		// when state_map values are custom status names like "review").
		if parsed == types.StatusOpen && normalizedMapped != "open" {
			return false
		}
		return true
	}
	return false
}

// ResolveStateIDForBeadsStatus returns the unique Linear workflow state ID to
// use when pushing the given beads status. Push only trusts explicit
// linear.state_map.* entries; defaults are safe for pull but too ambiguous for
// mutation.
func ResolveStateIDForBeadsStatus(cache *StateCache, status types.Status, config *MappingConfig) (string, error) {
	if cache == nil || len(cache.States) == 0 {
		return "", fmt.Errorf("no workflow states found")
	}
	if config == nil || len(config.ExplicitStateMap) == 0 {
		return "", fmt.Errorf("%s", missingExplicitStateMapMessage)
	}

	// Outbound override: an explicit linear.outbound_state_map.<status> entry
	// names the exact Linear workflow state to push to and short-circuits the
	// name/type matching below. This is the escape hatch when multiple Linear
	// states share a type (e.g. "In Progress" and "In Review" are both
	// "started") and the type-based fallback would otherwise be ambiguous.
	if outboundName, ok := config.OutboundStateMap[strings.ToLower(strings.TrimSpace(string(status)))]; ok {
		want := strings.ToLower(strings.TrimSpace(outboundName))
		for _, state := range cache.States {
			if strings.ToLower(strings.TrimSpace(state.Name)) == want {
				return state.ID, nil
			}
		}
		return "", fmt.Errorf("linear.outbound_state_map.%s = %q does not match any Linear workflow state", status, outboundName)
	}

	var nameMatches []State

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run 'bd linear link' to configure the status mapping interactively.
  2. Add explicit linear.state_map entries to the config mapping each beads status to a Linear state name or type.
  3. Ensure the MappingConfig passed to the resolver is non-nil and its ExplicitStateMap is populated.
  4. Validate the config at startup (ValidatePushStateMappings) before attempting a push.
  5. Check the config file for typos in the linear.state_map key prefix.

Example fix

// before
# config.toml
[linear]
api_key = "..."   # no state_map section
// after
# config.toml
[linear.state_map]
open = "Backlog"
in_progress = "In Progress"
closed = "Done"
Defensive patterns

Strategy: validation

Validate before calling

func requireExplicitStateMap(cfg *linear.MappingConfig) error {
	if cfg == nil || len(cfg.ExplicitStateMap) == 0 {
		return fmt.Errorf("linear.state_map not configured; run 'bd linear link' before pushing")
	}
	return nil
}

Type guard

func pushMappingConfigured(cfg *linear.MappingConfig) bool {
	return cfg != nil && len(cfg.ExplicitStateMap) > 0
}

Try / catch

stateID, err := linear.ResolveStateIDForBeadsStatus(cache, status, cfg)
if err != nil && strings.Contains(err.Error(), "bd linear link") {
	return fmt.Errorf("push blocked: %v", err)
}

Prevention

When it happens

Trigger: Calling ResolveStateIDForBeadsStatus with a populated cache but config == nil or len(config.ExplicitStateMap) == 0 — the user never ran 'bd linear link' or removed the state_map section from configuration.

Common situations: Fresh install where pull works with defaults but the first push fails; hand-edited config deleting linear.state_map keys; upgrading from a version that did not require explicit push mappings.

Related errors


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