diff --git a/cmd/bd/doctor/multirepo.go b/cmd/bd/doctor/multirepo.go index 1cfb111d..6b54905a 100644 --- a/cmd/bd/doctor/multirepo.go +++ b/cmd/bd/doctor/multirepo.go @@ -209,10 +209,10 @@ func findUnknownTypesInHydratedIssues(repoPath string, multiRepo *config.MultiRe } defer db.Close() - // Collect all known types (built-in + parent custom + all child custom) + // Collect all known types (core work types + parent custom + all child custom) + // Only core work types are built-in; Gas Town types require types.custom config. knownTypes := map[string]bool{ "bug": true, "feature": true, "task": true, "epic": true, "chore": true, - "message": true, "merge-request": true, "molecule": true, "gate": true, "event": true, } // Add parent's custom types diff --git a/cmd/bd/mol_ready_gated_test.go b/cmd/bd/mol_ready_gated_test.go index 9e74412e..608f7dfc 100644 --- a/cmd/bd/mol_ready_gated_test.go +++ b/cmd/bd/mol_ready_gated_test.go @@ -35,6 +35,13 @@ func setupGatedTestDB(t *testing.T) (*sqlite.SQLiteStorage, func()) { t.Fatalf("Failed to set issue_prefix: %v", err) } + // Configure Gas Town custom types for test compatibility (bd-find4) + if err := store.SetConfig(ctx, "types.custom", "molecule,gate,convoy,merge-request,slot,agent,role,rig,event,message"); err != nil { + store.Close() + os.RemoveAll(tmpDir) + t.Fatalf("Failed to set types.custom: %v", err) + } + cleanup := func() { store.Close() os.RemoveAll(tmpDir) diff --git a/cmd/bd/show_unit_helpers_test.go b/cmd/bd/show_unit_helpers_test.go index e1bef58d..916e8d62 100644 --- a/cmd/bd/show_unit_helpers_test.go +++ b/cmd/bd/show_unit_helpers_test.go @@ -100,6 +100,10 @@ func TestFindRepliesToAndReplies_WorksWithMemoryStorage(t *testing.T) { if err := st.SetConfig(ctx, "issue_prefix", "test"); err != nil { t.Fatalf("SetConfig: %v", err) } + // Configure Gas Town custom types for test compatibility (bd-find4) + if err := st.SetConfig(ctx, "types.custom", "message"); err != nil { + t.Fatalf("SetConfig types.custom: %v", err) + } root := &types.Issue{Title: "root", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeMessage, Sender: "a", Assignee: "b"} reply1 := &types.Issue{Title: "r1", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeMessage, Sender: "b", Assignee: "a"} diff --git a/cmd/bd/test_helpers_test.go b/cmd/bd/test_helpers_test.go index 56b38abf..47f7783c 100644 --- a/cmd/bd/test_helpers_test.go +++ b/cmd/bd/test_helpers_test.go @@ -84,26 +84,32 @@ func failIfProductionDatabase(t *testing.T, dbPath string) { // This prevents "database not initialized" errors in tests func newTestStore(t *testing.T, dbPath string) *sqlite.SQLiteStorage { t.Helper() - + // CRITICAL (bd-2c5a): Ensure we're not polluting production database failIfProductionDatabase(t, dbPath) - + if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { t.Fatalf("Failed to create database directory: %v", err) } - + store, err := sqlite.New(context.Background(), dbPath) if err != nil { t.Fatalf("Failed to create test database: %v", err) } - + // CRITICAL (bd-166): Set issue_prefix to prevent "database not initialized" errors ctx := context.Background() if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil { store.Close() t.Fatalf("Failed to set issue_prefix: %v", err) } - + + // Configure Gas Town custom types for test compatibility (bd-find4) + if err := store.SetConfig(ctx, "types.custom", "molecule,gate,convoy,merge-request,slot,agent,role,rig,event,message"); err != nil { + store.Close() + t.Fatalf("Failed to set types.custom: %v", err) + } + t.Cleanup(func() { store.Close() }) return store } @@ -130,7 +136,13 @@ func newTestStoreWithPrefix(t *testing.T, dbPath string, prefix string) *sqlite. store.Close() t.Fatalf("Failed to set issue_prefix: %v", err) } - + + // Configure Gas Town custom types for test compatibility (bd-find4) + if err := store.SetConfig(ctx, "types.custom", "molecule,gate,convoy,merge-request,slot,agent,role,rig,event,message"); err != nil { + store.Close() + t.Fatalf("Failed to set types.custom: %v", err) + } + t.Cleanup(func() { store.Close() }) return store } diff --git a/cmd/bd/types.go b/cmd/bd/types.go index ec8ff607..43f28be5 100644 --- a/cmd/bd/types.go +++ b/cmd/bd/types.go @@ -1,33 +1,37 @@ package main import ( + "context" "fmt" + "strings" "github.com/spf13/cobra" "github.com/steveyegge/beads/internal/types" ) -// AllIssueTypes returns all valid built-in issue types with descriptions. -// Ordered by typical usage frequency: work types first, then system types. -var allIssueTypes = []struct { +// coreWorkTypes are the built-in types that beads validates without configuration. +var coreWorkTypes = []struct { Type types.IssueType Description string }{ - // Work types (common user-facing types) {types.TypeTask, "General work item (default)"}, {types.TypeBug, "Bug report or defect"}, {types.TypeFeature, "New feature or enhancement"}, {types.TypeChore, "Maintenance or housekeeping"}, {types.TypeEpic, "Large body of work spanning multiple issues"}, +} - // System types (used by tooling) +// wellKnownCustomTypes are commonly used types that require types.custom configuration. +// These are used by Gas Town and other infrastructure that extends beads. +var wellKnownCustomTypes = []struct { + Type types.IssueType + Description string +}{ {types.TypeMolecule, "Template for issue hierarchies"}, {types.TypeGate, "Async coordination gate"}, {types.TypeConvoy, "Cross-project tracking with reactive completion"}, {types.TypeMergeRequest, "Merge queue entry for refinery processing"}, {types.TypeSlot, "Exclusive access slot (merge-slot gate)"}, - - // Agent types (Gas Town infrastructure) {types.TypeAgent, "Agent identity bead"}, {types.TypeRole, "Agent role definition"}, {types.TypeRig, "Rig identity bead (multi-repo workspace)"}, @@ -41,55 +45,85 @@ var typesCmd = &cobra.Command{ Short: "List valid issue types", Long: `List all valid issue types that can be used with bd create --type. -Types are organized into categories: -- Work types: Common types for tracking work (task, bug, feature, etc.) -- System types: Used by beads tooling (molecule, gate, convoy, etc.) -- Agent types: Used by Gas Town agent infrastructure +Core work types (bug, task, feature, chore, epic) are always valid. +Additional types require configuration via types.custom in .beads/config.yaml. Examples: bd types # List all types with descriptions bd types --json # Output as JSON `, Run: func(cmd *cobra.Command, args []string) { + // Get custom types from config + var customTypes []string + ctx := context.Background() + if store != nil { + if ct, err := store.GetCustomTypes(ctx); err == nil { + customTypes = ct + } + } + if jsonOutput { result := struct { - Types []struct { - Name string `json:"name"` - Description string `json:"description"` - } `json:"types"` + CoreTypes []typeInfo `json:"core_types"` + CustomTypes []string `json:"custom_types,omitempty"` }{} - for _, t := range allIssueTypes { - result.Types = append(result.Types, struct { - Name string `json:"name"` - Description string `json:"description"` - }{ + for _, t := range coreWorkTypes { + result.CoreTypes = append(result.CoreTypes, typeInfo{ Name: string(t.Type), Description: t.Description, }) } + result.CustomTypes = customTypes outputJSON(result) return } - // Text output with categories - fmt.Println("Work types:") - for _, t := range allIssueTypes[:5] { + // Text output + fmt.Println("Core work types (built-in):") + for _, t := range coreWorkTypes { fmt.Printf(" %-14s %s\n", t.Type, t.Description) } - fmt.Println("\nSystem types:") - for _, t := range allIssueTypes[5:10] { - fmt.Printf(" %-14s %s\n", t.Type, t.Description) + if len(customTypes) > 0 { + fmt.Println("\nConfigured custom types:") + for _, t := range customTypes { + // Check if it's a well-known type and show description + desc := "" + for _, wk := range wellKnownCustomTypes { + if string(wk.Type) == t { + desc = wk.Description + break + } + } + if desc != "" { + fmt.Printf(" %-14s %s\n", t, desc) + } else { + fmt.Printf(" %s\n", t) + } + } + } else { + fmt.Println("\nNo custom types configured.") + fmt.Println("Configure with: bd config set types.custom \"type1,type2,...\"") } - fmt.Println("\nAgent types:") - for _, t := range allIssueTypes[10:] { - fmt.Printf(" %-14s %s\n", t.Type, t.Description) + // Show hint about well-known types if none are configured + if len(customTypes) == 0 { + fmt.Println("\nWell-known custom types (used by Gas Town):") + var typeNames []string + for _, t := range wellKnownCustomTypes { + typeNames = append(typeNames, string(t.Type)) + } + fmt.Printf(" %s\n", strings.Join(typeNames, ", ")) } }, } +type typeInfo struct { + Name string `json:"name"` + Description string `json:"description"` +} + func init() { rootCmd.AddCommand(typesCmd) } diff --git a/internal/molecules/molecules_test.go b/internal/molecules/molecules_test.go index bf351ce6..891ebca2 100644 --- a/internal/molecules/molecules_test.go +++ b/internal/molecules/molecules_test.go @@ -81,6 +81,11 @@ func TestLoader_LoadAll(t *testing.T) { t.Fatalf("Failed to set prefix: %v", err) } + // Configure custom types for Gas Town types (bd-find4) + if err := store.SetConfig(ctx, "types.custom", "molecule"); err != nil { + t.Fatalf("Failed to set types.custom: %v", err) + } + // Create a project-level molecules.jsonl moleculesPath := filepath.Join(beadsDir, "molecules.jsonl") content := `{"id":"mol-feature","title":"Feature Template","issue_type":"molecule","status":"open","description":"Standard feature workflow"} @@ -150,6 +155,11 @@ func TestLoader_SkipExistingMolecules(t *testing.T) { t.Fatalf("Failed to set prefix: %v", err) } + // Configure custom types for Gas Town types (bd-find4) + if err := store.SetConfig(ctx, "types.custom", "molecule"); err != nil { + t.Fatalf("Failed to set types.custom: %v", err) + } + // Pre-create a molecule in the database (skip prefix validation for mol-* IDs) existingMol := &types.Issue{ ID: "mol-existing", diff --git a/internal/rpc/rpc_test.go b/internal/rpc/rpc_test.go index b140e610..b26e312e 100644 --- a/internal/rpc/rpc_test.go +++ b/internal/rpc/rpc_test.go @@ -51,6 +51,13 @@ func setupTestServer(t *testing.T) (*Server, *Client, func()) { t.Fatalf("Failed to set issue_prefix: %v", err) } + // Configure Gas Town custom types for test compatibility (bd-find4) + if err := store.SetConfig(ctx, "types.custom", "molecule,gate,convoy,merge-request,slot,agent,role,rig,event,message"); err != nil { + store.Close() + os.RemoveAll(tmpDir) + t.Fatalf("Failed to set types.custom: %v", err) + } + server := NewServer(socketPath, store, tmpDir, dbPath) ctx, cancel := context.WithCancel(context.Background()) diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index 5eb68b4d..fe14fcbf 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -190,8 +190,17 @@ func (m *MemoryStorage) CreateIssue(ctx context.Context, issue *types.Issue, act m.mu.Lock() defer m.mu.Unlock() - // Validate - if err := issue.Validate(); err != nil { + // Get custom types and statuses for validation + var customTypes, customStatuses []string + if typeStr := m.config["types.custom"]; typeStr != "" { + customTypes = parseCustomStatuses(typeStr) + } + if statusStr := m.config["status.custom"]; statusStr != "" { + customStatuses = parseCustomStatuses(statusStr) + } + + // Validate with custom types + if err := issue.ValidateWithCustom(customStatuses, customTypes); err != nil { return fmt.Errorf("validation failed: %w", err) } @@ -243,9 +252,18 @@ func (m *MemoryStorage) CreateIssues(ctx context.Context, issues []*types.Issue, m.mu.Lock() defer m.mu.Unlock() + // Get custom types and statuses for validation + var customTypes, customStatuses []string + if typeStr := m.config["types.custom"]; typeStr != "" { + customTypes = parseCustomStatuses(typeStr) + } + if statusStr := m.config["status.custom"]; statusStr != "" { + customStatuses = parseCustomStatuses(statusStr) + } + // Validate all first for i, issue := range issues { - if err := issue.Validate(); err != nil { + if err := issue.ValidateWithCustom(customStatuses, customTypes); err != nil { return fmt.Errorf("validation failed for issue %d: %w", i, err) } } diff --git a/internal/storage/memory/memory_test.go b/internal/storage/memory/memory_test.go index 73ae42f1..09819cee 100644 --- a/internal/storage/memory/memory_test.go +++ b/internal/storage/memory/memory_test.go @@ -21,6 +21,12 @@ func setupTestMemory(t *testing.T) *MemoryStorage { t.Fatalf("failed to set issue_prefix: %v", err) } + // Configure Gas Town custom types for test compatibility (bd-find4) + // These types are no longer built-in but many tests use them + if err := store.SetConfig(ctx, "types.custom", "message,merge-request,molecule,gate,agent,role,rig,convoy,event,slot"); err != nil { + t.Fatalf("failed to set types.custom: %v", err) + } + return store } diff --git a/internal/storage/sqlite/gate_no_daemon_test.go b/internal/storage/sqlite/gate_no_daemon_test.go index 3c54de4c..8c828931 100644 --- a/internal/storage/sqlite/gate_no_daemon_test.go +++ b/internal/storage/sqlite/gate_no_daemon_test.go @@ -29,6 +29,11 @@ func TestGateFieldsPreservedAcrossConnections(t *testing.T) { t.Fatalf("failed to set issue_prefix: %v", err) } + // Configure custom types for Gas Town types (gate is not a core type) + if err := store1.SetConfig(ctx, "types.custom", "gate"); err != nil { + t.Fatalf("failed to set types.custom: %v", err) + } + gate := &types.Issue{ ID: "beads-test1", Title: "Test Gate", diff --git a/internal/storage/sqlite/sqlite_test.go b/internal/storage/sqlite/sqlite_test.go index dd4642de..d9bdcd63 100644 --- a/internal/storage/sqlite/sqlite_test.go +++ b/internal/storage/sqlite/sqlite_test.go @@ -39,6 +39,14 @@ func setupTestDB(t *testing.T) (*SQLiteStorage, func()) { t.Fatalf("failed to set issue_prefix: %v", err) } + // Configure Gas Town custom types for test compatibility (bd-find4) + // These types are no longer built-in but many tests use them + if err := store.SetConfig(ctx, "types.custom", "message,merge-request,molecule,gate,agent,role,rig,convoy,event,slot"); err != nil { + store.Close() + os.RemoveAll(tmpDir) + t.Fatalf("failed to set types.custom: %v", err) + } + cleanup := func() { store.Close() os.RemoveAll(tmpDir) diff --git a/internal/types/types.go b/internal/types/types.go index 979e73e1..9c682bd7 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -476,13 +476,20 @@ func (s Status) IsValidWithCustom(customStatuses []string) bool { // IssueType categorizes the kind of work type IssueType string -// Issue type constants +// Core work type constants - these are the built-in types that beads validates. +// All other types require configuration via types.custom in config.yaml. +const ( + TypeBug IssueType = "bug" + TypeFeature IssueType = "feature" + TypeTask IssueType = "task" + TypeEpic IssueType = "epic" + TypeChore IssueType = "chore" +) + +// Well-known custom types - constants for code convenience. +// These are NOT built-in types and require types.custom configuration for validation. +// Used by Gas Town and other infrastructure that extends beads. const ( - TypeBug IssueType = "bug" - TypeFeature IssueType = "feature" - TypeTask IssueType = "task" - TypeEpic IssueType = "epic" - TypeChore IssueType = "chore" TypeMessage IssueType = "message" // Ephemeral communication between workers TypeMergeRequest IssueType = "merge-request" // Merge queue entry for refinery processing TypeMolecule IssueType = "molecule" // Template molecule for issue hierarchies @@ -495,10 +502,12 @@ const ( TypeSlot IssueType = "slot" // Exclusive access slot (merge-slot gate) ) -// IsValid checks if the issue type value is valid +// IsValid checks if the issue type is a core work type. +// Only core work types (bug, feature, task, epic, chore) are built-in. +// Other types (molecule, gate, convoy, etc.) require types.custom configuration. func (t IssueType) IsValid() bool { switch t { - case TypeBug, TypeFeature, TypeTask, TypeEpic, TypeChore, TypeMessage, TypeMergeRequest, TypeMolecule, TypeGate, TypeAgent, TypeRole, TypeRig, TypeConvoy, TypeEvent, TypeSlot: + case TypeBug, TypeFeature, TypeTask, TypeEpic, TypeChore: return true } return false @@ -569,8 +578,7 @@ func (t IssueType) RequiredSections() []RequiredSection { {Heading: "## Success Criteria", Hint: "Define high-level success criteria"}, } default: - // Chore, message, molecule, gate, agent, role, convoy, event, merge-request - // have no required sections + // Chore and custom types have no required sections return nil } } diff --git a/internal/types/types_test.go b/internal/types/types_test.go index 8db642ed..ebfcb0b2 100644 --- a/internal/types/types_test.go +++ b/internal/types/types_test.go @@ -539,20 +539,24 @@ func TestIssueTypeIsValid(t *testing.T) { issueType IssueType valid bool }{ + // Core work types are always valid {TypeBug, true}, {TypeFeature, true}, {TypeTask, true}, {TypeEpic, true}, {TypeChore, true}, - {TypeMessage, true}, - {TypeMergeRequest, true}, - {TypeMolecule, true}, - {TypeGate, true}, - {TypeAgent, true}, - {TypeRole, true}, - {TypeConvoy, true}, - {TypeEvent, true}, - {TypeSlot, true}, + // Gas Town types require types.custom configuration + {TypeMessage, false}, + {TypeMergeRequest, false}, + {TypeMolecule, false}, + {TypeGate, false}, + {TypeAgent, false}, + {TypeRole, false}, + {TypeConvoy, false}, + {TypeEvent, false}, + {TypeSlot, false}, + {TypeRig, false}, + // Invalid types {IssueType("invalid"), false}, {IssueType(""), false}, } diff --git a/internal/validation/bead_test.go b/internal/validation/bead_test.go index 461d5749..71279e73 100644 --- a/internal/validation/bead_test.go +++ b/internal/validation/bead_test.go @@ -211,19 +211,18 @@ func TestParseIssueType(t *testing.T) { wantError bool errorContains string }{ - // Valid issue types + // Core work types (always valid) {"bug type", "bug", types.TypeBug, false, ""}, {"feature type", "feature", types.TypeFeature, false, ""}, {"task type", "task", types.TypeTask, false, ""}, {"epic type", "epic", types.TypeEpic, false, ""}, {"chore type", "chore", types.TypeChore, false, ""}, - {"merge-request type", "merge-request", types.TypeMergeRequest, false, ""}, - {"molecule type", "molecule", types.TypeMolecule, false, ""}, - {"gate type", "gate", types.TypeGate, false, ""}, - {"event type", "event", types.TypeEvent, false, ""}, - {"message type", "message", types.TypeMessage, false, ""}, - // Gas Town types (agent, role, rig, convoy, slot) have been removed - // They now require custom type configuration, + // Gas Town types require types.custom configuration (invalid without config) + {"merge-request type", "merge-request", types.TypeTask, true, "invalid issue type"}, + {"molecule type", "molecule", types.TypeTask, true, "invalid issue type"}, + {"gate type", "gate", types.TypeTask, true, "invalid issue type"}, + {"event type", "event", types.TypeTask, true, "invalid issue type"}, + {"message type", "message", types.TypeTask, true, "invalid issue type"}, // Case sensitivity (function is case-sensitive) {"uppercase bug", "BUG", types.TypeTask, true, "invalid issue type"},