From d8ec84c7288b9afbc41d12d99663d3b69ebddd3d Mon Sep 17 00:00:00 2001 From: jasper Date: Sat, 24 Jan 2026 15:07:17 -0800 Subject: [PATCH] fix(update): accept custom types from types.custom config (GH#hq-8hif1z) The `bd update --type` command was rejecting custom types (like role, agent, rig) even when configured via types.custom. The issue was that type validation used IsValid() which only checks the 5 core types, ignoring custom types. Changes: - CLI (update.go): Use IsValidWithCustom() with types from config - Storage (validators.go): Add validateIssueTypeWithCustom() function - Storage (queries.go, transaction.go): Fetch and pass custom types The error message now dynamically shows all valid types including custom ones. Co-Authored-By: Claude Opus 4.5 --- cmd/bd/update.go | 50 ++++++++++++++++++++++++-- internal/storage/sqlite/queries.go | 3 +- internal/storage/sqlite/transaction.go | 3 +- internal/storage/sqlite/validators.go | 43 ++++++++++------------ 4 files changed, 69 insertions(+), 30 deletions(-) diff --git a/cmd/bd/update.go b/cmd/bd/update.go index 61fbe6ad..3a1bd83c 100644 --- a/cmd/bd/update.go +++ b/cmd/bd/update.go @@ -4,9 +4,11 @@ import ( "encoding/json" "fmt" "os" + "strings" "time" "github.com/spf13/cobra" + "github.com/steveyegge/beads/internal/config" "github.com/steveyegge/beads/internal/hooks" "github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/timeparsing" @@ -113,8 +115,12 @@ create, update, show, or close operation).`, issueType, _ := cmd.Flags().GetString("type") // Normalize aliases (e.g., "enhancement" -> "feature") before validating issueType = util.NormalizeIssueType(issueType) - if !types.IssueType(issueType).IsValid() { - FatalErrorRespectJSON("invalid issue type %q. Valid types: bug, feature, task, epic, chore, merge-request, molecule, gate, agent, role, rig, convoy, event, slot", issueType) + // Get custom types from config to validate non-core types (GH#hq-8hif1z) + customTypes := getCustomTypesForValidation() + if !types.IssueType(issueType).IsValidWithCustom(customTypes) { + validTypes := []string{"bug", "feature", "task", "epic", "chore"} + validTypes = append(validTypes, customTypes...) + FatalErrorRespectJSON("invalid issue type %q. Valid types: %s", issueType, strings.Join(validTypes, ", ")) } updates["issue_type"] = issueType } @@ -643,6 +649,46 @@ create, update, show, or close operation).`, }, } +// getCustomTypesForValidation retrieves custom types for validation. +// Works in both daemon and direct modes, with fallback to config.yaml. +func getCustomTypesForValidation() []string { + ctx := rootCtx + + // Try daemon mode first + if daemonClient != nil { + resp, err := daemonClient.GetConfig(&rpc.GetConfigArgs{Key: "types.custom"}) + if err == nil && resp.Value != "" { + return parseCustomTypesList(resp.Value) + } + } + + // Direct mode - use store + if store != nil { + if ct, err := store.GetCustomTypes(ctx); err == nil && len(ct) > 0 { + return ct + } + } + + // Fallback to config.yaml + return config.GetCustomTypesFromYAML() +} + +// parseCustomTypesList splits a comma-separated string into custom types. +func parseCustomTypesList(value string) []string { + if value == "" { + return nil + } + parts := strings.Split(value, ",") + result := make([]string, 0, len(parts)) + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + func init() { updateCmd.Flags().StringP("status", "s", "", "New status") registerPriorityFlag(updateCmd, "") diff --git a/internal/storage/sqlite/queries.go b/internal/storage/sqlite/queries.go index 555c37d6..9ac0ef84 100644 --- a/internal/storage/sqlite/queries.go +++ b/internal/storage/sqlite/queries.go @@ -887,12 +887,11 @@ func (s *SQLiteStorage) UpdateIssue(ctx context.Context, id string, updates map[ return fmt.Errorf("issue %s not found", id) } - // Fetch custom statuses and types for validation + // Fetch custom statuses and types for validation (GH#hq-8hif1z) customStatuses, err := s.GetCustomStatuses(ctx) if err != nil { return wrapDBError("get custom statuses", err) } - customTypes, err := s.GetCustomTypes(ctx) if err != nil { return wrapDBError("get custom types", err) diff --git a/internal/storage/sqlite/transaction.go b/internal/storage/sqlite/transaction.go index 60794d13..6271f58c 100644 --- a/internal/storage/sqlite/transaction.go +++ b/internal/storage/sqlite/transaction.go @@ -400,12 +400,11 @@ func (t *sqliteTxStorage) UpdateIssue(ctx context.Context, id string, updates ma return fmt.Errorf("issue %s not found", id) } - // Fetch custom statuses and types for validation + // Fetch custom statuses and types for validation (GH#hq-8hif1z) customStatuses, err := t.GetCustomStatuses(ctx) if err != nil { return fmt.Errorf("failed to get custom statuses: %w", err) } - customTypes, err := t.GetCustomTypes(ctx) if err != nil { return fmt.Errorf("failed to get custom types: %w", err) diff --git a/internal/storage/sqlite/validators.go b/internal/storage/sqlite/validators.go index af7e0549..bc3e53f3 100644 --- a/internal/storage/sqlite/validators.go +++ b/internal/storage/sqlite/validators.go @@ -37,11 +37,17 @@ func validateStatusWithCustom(value interface{}, customStatuses []string) error return nil } -// validateIssueType validates an issue type value +// validateIssueType validates an issue type value (built-in types only) func validateIssueType(value interface{}) error { + return validateIssueTypeWithCustom(value, nil) +} + +// validateIssueTypeWithCustom validates an issue type value, allowing custom types. +// Custom types are configured via bd config set types.custom "type1,type2,..." +func validateIssueTypeWithCustom(value interface{}, customTypes []string) error { if issueType, ok := value.(string); ok { // Normalize first to support aliases like "enhancement" -> "feature" - if !types.IssueType(issueType).Normalize().IsValid() { + if !types.IssueType(issueType).Normalize().IsValidWithCustom(customTypes) { return fmt.Errorf("invalid issue type: %s", issueType) } } @@ -77,19 +83,26 @@ var fieldValidators = map[string]func(interface{}) error{ "estimated_minutes": validateEstimatedMinutes, } -// validateFieldUpdate validates a field update value (built-in statuses only) +// validateFieldUpdate validates a field update value (built-in statuses and types only) func validateFieldUpdate(key string, value interface{}) error { - return validateFieldUpdateWithCustomStatuses(key, value, nil) + return validateFieldUpdateWithCustom(key, value, nil, nil) +} + +// validateFieldUpdateWithCustomStatuses validates a field update value, +// allowing custom statuses for status field validation. +// DEPRECATED: Use validateFieldUpdateWithCustom instead which also handles custom types. +func validateFieldUpdateWithCustomStatuses(key string, value interface{}, customStatuses []string) error { + return validateFieldUpdateWithCustom(key, value, customStatuses, nil) } // validateFieldUpdateWithCustom validates a field update value, -// allowing custom statuses and custom types for their respective field validations. +// allowing custom statuses and custom types for validation (GH#hq-8hif1z). func validateFieldUpdateWithCustom(key string, value interface{}, customStatuses, customTypes []string) error { // Special handling for status field to support custom statuses if key == "status" { return validateStatusWithCustom(value, customStatuses) } - // Special handling for issue_type field to support custom types (federation trust model) + // Special handling for issue_type field to support custom types if key == "issue_type" { return validateIssueTypeWithCustom(value, customTypes) } @@ -98,21 +111,3 @@ func validateFieldUpdateWithCustom(key string, value interface{}, customStatuses } return nil } - -// validateFieldUpdateWithCustomStatuses validates a field update value, -// allowing custom statuses for status field validation. -func validateFieldUpdateWithCustomStatuses(key string, value interface{}, customStatuses []string) error { - return validateFieldUpdateWithCustom(key, value, customStatuses, nil) -} - -// validateIssueTypeWithCustom validates an issue type value, allowing custom types. -func validateIssueTypeWithCustom(value interface{}, customTypes []string) error { - if issueType, ok := value.(string); ok { - // Normalize first to support aliases like "enhancement" -> "feature" - normalized := types.IssueType(issueType).Normalize() - if !normalized.IsValidWithCustom(customTypes) { - return fmt.Errorf("invalid issue type: %s", issueType) - } - } - return nil -}