From 7cf67153de316c799f9fc5cba037293976e20c7e Mon Sep 17 00:00:00 2001 From: collins Date: Wed, 21 Jan 2026 10:30:38 -0800 Subject: [PATCH] refactor(types): remove Gas Town type constants from beads core (bd-w2zz4) Remove Gas Town-specific type constants (TypeMolecule, TypeGate, TypeConvoy, TypeMergeRequest, TypeSlot, TypeAgent, TypeRole, TypeRig, TypeEvent, TypeMessage) from internal/types/types.go. Beads now only has core work types built-in: - bug, feature, task, epic, chore All Gas Town types are now purely custom types with no special handling in beads. Use string literals like "gate" or "molecule" when needed, and configure types.custom in config.yaml for validation. Changes: - Remove Gas Town type constants from types.go - Remove mr/mol aliases from Normalize() - Update bd types command to only show core types - Replace all constant usages with string literals throughout codebase - Update tests to use string literals This decouples beads from Gas Town, making it a generic issue tracker. Co-Authored-By: Claude Opus 4.5 --- cmd/bd/cli_coverage_show_test.go | 6 +-- cmd/bd/cook.go | 2 +- cmd/bd/export_obsidian.go | 20 +++------ cmd/bd/gate.go | 10 ++--- cmd/bd/gate_discover.go | 2 +- cmd/bd/list.go | 2 +- cmd/bd/mol_ready_gated.go | 2 +- cmd/bd/mol_ready_gated_test.go | 8 ++-- cmd/bd/show_unit_helpers_test.go | 6 +-- cmd/bd/swarm.go | 12 +++--- cmd/bd/thread_test.go | 18 ++++---- cmd/bd/types.go | 43 +------------------ internal/beads/beads.go | 13 +++--- internal/molecules/molecules_test.go | 2 +- internal/rpc/client_gate_shutdown_test.go | 2 +- internal/rpc/server_issues_epics.go | 10 ++--- internal/storage/memory/memory.go | 3 +- .../memory/memory_more_coverage_test.go | 4 +- .../storage/sqlite/gate_no_daemon_test.go | 2 +- internal/storage/sqlite/graph_links_test.go | 20 ++++----- internal/storage/sqlite/multirepo_test.go | 4 +- internal/storage/sqlite/transaction_test.go | 4 +- internal/types/types.go | 24 ++--------- internal/types/types_test.go | 38 ++++++++-------- internal/validation/template_test.go | 4 +- 25 files changed, 99 insertions(+), 162 deletions(-) diff --git a/cmd/bd/cli_coverage_show_test.go b/cmd/bd/cli_coverage_show_test.go index 6eb4d661..7d9a5308 100644 --- a/cmd/bd/cli_coverage_show_test.go +++ b/cmd/bd/cli_coverage_show_test.go @@ -391,9 +391,9 @@ func TestCoverage_ShowThread(t *testing.T) { } ctx := context.Background() - root := &types.Issue{Title: "Root message", IssueType: types.TypeMessage, Status: types.StatusOpen, Sender: "alice", Assignee: "bob"} - reply1 := &types.Issue{Title: "Re: Root", IssueType: types.TypeMessage, Status: types.StatusOpen, Sender: "bob", Assignee: "alice"} - reply2 := &types.Issue{Title: "Re: Re: Root", IssueType: types.TypeMessage, Status: types.StatusOpen, Sender: "alice", Assignee: "bob"} + root := &types.Issue{Title: "Root message", IssueType: "message", Status: types.StatusOpen, Sender: "alice", Assignee: "bob"} + reply1 := &types.Issue{Title: "Re: Root", IssueType: "message", Status: types.StatusOpen, Sender: "bob", Assignee: "alice"} + reply2 := &types.Issue{Title: "Re: Re: Root", IssueType: "message", Status: types.StatusOpen, Sender: "alice", Assignee: "bob"} if err := s.CreateIssue(ctx, root, "test-user"); err != nil { s.Close() t.Fatalf("CreateIssue root: %v", err) diff --git a/cmd/bd/cook.go b/cmd/bd/cook.go index efc2d0c5..42a18af1 100644 --- a/cmd/bd/cook.go +++ b/cmd/bd/cook.go @@ -521,7 +521,7 @@ func createGateIssue(step *formula.Step, parentID string) *types.Issue { Description: fmt.Sprintf("Async gate for step %s", step.ID), Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeGate, + IssueType: "gate", AwaitType: step.Gate.Type, AwaitID: step.Gate.ID, Timeout: timeout, diff --git a/cmd/bd/export_obsidian.go b/cmd/bd/export_obsidian.go index bbde8778..3d8a21ba 100644 --- a/cmd/bd/export_obsidian.go +++ b/cmd/bd/export_obsidian.go @@ -32,20 +32,14 @@ var obsidianPriority = []string{ "⏬", // 4 = backlog/lowest } -// obsidianTypeTag maps bd issue type to Obsidian tag -// Note: Gas Town-specific types (agent, role, rig, convoy, slot) are now labels. -// The labels will be converted to tags automatically via the label->tag logic. +// obsidianTypeTag maps bd issue type to Obsidian tag (core types only) +// Gas Town types are custom types and will use their issue_type value as a tag. var obsidianTypeTag = map[types.IssueType]string{ - types.TypeBug: "#Bug", - types.TypeFeature: "#Feature", - types.TypeTask: "#Task", - types.TypeEpic: "#Epic", - types.TypeChore: "#Chore", - types.TypeMessage: "#Message", - types.TypeMergeRequest: "#MergeRequest", - types.TypeMolecule: "#Molecule", - types.TypeGate: "#Gate", - types.TypeEvent: "#Event", + types.TypeBug: "#Bug", + types.TypeFeature: "#Feature", + types.TypeTask: "#Task", + types.TypeEpic: "#Epic", + types.TypeChore: "#Chore", } // formatObsidianTask converts a single issue to Obsidian Tasks format diff --git a/cmd/bd/gate.go b/cmd/bd/gate.go index b08ac73e..d52f286f 100644 --- a/cmd/bd/gate.go +++ b/cmd/bd/gate.go @@ -61,7 +61,7 @@ By default, shows only open gates. Use --all to include closed gates.`, limit, _ := cmd.Flags().GetInt("limit") // Build filter for gate type issues - gateType := types.TypeGate + gateType := types.IssueType("gate") filter := types.IssueFilter{ IssueType: &gateType, Limit: limit, @@ -243,7 +243,7 @@ This is used by 'gt done --phase-complete' to register for gate wake notificatio } } - if issue.IssueType != types.TypeGate { + if issue.IssueType != "gate" { fmt.Fprintf(os.Stderr, "Error: %s is not a gate issue (type=%s)\n", gateID, issue.IssueType) os.Exit(1) } @@ -329,7 +329,7 @@ This is similar to 'bd show' but validates that the issue is a gate.`, } } - if issue.IssueType != types.TypeGate { + if issue.IssueType != "gate" { fmt.Fprintf(os.Stderr, "Error: %s is not a gate issue (type=%s)\n", gateID, issue.IssueType) os.Exit(1) } @@ -410,7 +410,7 @@ Use --reason to provide context for why the gate was resolved.`, } } - if issue.IssueType != types.TypeGate { + if issue.IssueType != "gate" { fmt.Fprintf(os.Stderr, "Error: %s is not a gate issue (type=%s)\n", gateID, issue.IssueType) os.Exit(1) } @@ -492,7 +492,7 @@ Examples: limit, _ := cmd.Flags().GetInt("limit") // Get open gates - gateType := types.TypeGate + gateType := types.IssueType("gate") filter := types.IssueFilter{ IssueType: &gateType, ExcludeStatus: []types.Status{types.StatusClosed}, diff --git a/cmd/bd/gate_discover.go b/cmd/bd/gate_discover.go index 571ba973..4bd63ab4 100644 --- a/cmd/bd/gate_discover.go +++ b/cmd/bd/gate_discover.go @@ -228,7 +228,7 @@ func findPendingGates() ([]*types.Issue, error) { } } else { // Direct mode - gateType := types.TypeGate + gateType := types.IssueType("gate") filter := types.IssueFilter{ IssueType: &gateType, ExcludeStatus: []types.Status{types.StatusClosed}, diff --git a/cmd/bd/list.go b/cmd/bd/list.go index 918ed666..1769c4e3 100644 --- a/cmd/bd/list.go +++ b/cmd/bd/list.go @@ -796,7 +796,7 @@ var listCmd = &cobra.Command{ // Gate filtering: exclude gate issues by default (bd-7zka.2) // Use --include-gates or --type gate to show gate issues if !includeGates && issueType != "gate" { - filter.ExcludeTypes = append(filter.ExcludeTypes, types.TypeGate) + filter.ExcludeTypes = append(filter.ExcludeTypes, "gate") } // Parent filtering: filter children by parent issue diff --git a/cmd/bd/mol_ready_gated.go b/cmd/bd/mol_ready_gated.go index deae7468..b4092ef1 100644 --- a/cmd/bd/mol_ready_gated.go +++ b/cmd/bd/mol_ready_gated.go @@ -113,7 +113,7 @@ func runMolReadyGated(cmd *cobra.Command, args []string) { // 5. Filter out molecules that are already hooked by someone func findGateReadyMolecules(ctx context.Context, s storage.Storage) ([]*GatedMolecule, error) { // Step 1: Find all closed gate beads - gateType := types.TypeGate + gateType := types.IssueType("gate") closedStatus := types.StatusClosed gateFilter := types.IssueFilter{ IssueType: &gateType, diff --git a/cmd/bd/mol_ready_gated_test.go b/cmd/bd/mol_ready_gated_test.go index 608f7dfc..7b6a8a27 100644 --- a/cmd/bd/mol_ready_gated_test.go +++ b/cmd/bd/mol_ready_gated_test.go @@ -127,7 +127,7 @@ func TestFindGateReadyMolecules_ClosedGate(t *testing.T) { gate := &types.Issue{ ID: "test-mol-002.gate-await-ci", Title: "Gate: gh:run ci-workflow", - IssueType: types.TypeGate, + IssueType: "gate", Status: types.StatusClosed, // Gate has closed AwaitType: "gh:run", AwaitID: "ci-workflow", @@ -222,7 +222,7 @@ func TestFindGateReadyMolecules_OpenGate(t *testing.T) { gate := &types.Issue{ ID: "test-mol-003.gate-await-ci", Title: "Gate: gh:run ci-workflow", - IssueType: types.TypeGate, + IssueType: "gate", Status: types.StatusOpen, // Gate is still open AwaitType: "gh:run", AwaitID: "ci-workflow", @@ -302,7 +302,7 @@ func TestFindGateReadyMolecules_HookedMolecule(t *testing.T) { gate := &types.Issue{ ID: "test-mol-004.gate-await-ci", Title: "Gate: gh:run ci-workflow", - IssueType: types.TypeGate, + IssueType: "gate", Status: types.StatusClosed, AwaitType: "gh:run", AwaitID: "ci-workflow", @@ -384,7 +384,7 @@ func TestFindGateReadyMolecules_MultipleGates(t *testing.T) { gate := &types.Issue{ ID: fmt.Sprintf("%s.gate", molID), Title: "Gate: gh:run", - IssueType: types.TypeGate, + IssueType: "gate", Status: types.StatusClosed, AwaitType: "gh:run", CreatedAt: time.Now(), diff --git a/cmd/bd/show_unit_helpers_test.go b/cmd/bd/show_unit_helpers_test.go index 916e8d62..913b4831 100644 --- a/cmd/bd/show_unit_helpers_test.go +++ b/cmd/bd/show_unit_helpers_test.go @@ -105,9 +105,9 @@ func TestFindRepliesToAndReplies_WorksWithMemoryStorage(t *testing.T) { 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"} - reply2 := &types.Issue{Title: "r2", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeMessage, Sender: "a", Assignee: "b"} + root := &types.Issue{Title: "root", Status: types.StatusOpen, Priority: 2, IssueType: "message", Sender: "a", Assignee: "b"} + reply1 := &types.Issue{Title: "r1", Status: types.StatusOpen, Priority: 2, IssueType: "message", Sender: "b", Assignee: "a"} + reply2 := &types.Issue{Title: "r2", Status: types.StatusOpen, Priority: 2, IssueType: "message", Sender: "a", Assignee: "b"} if err := st.CreateIssue(ctx, root, "tester"); err != nil { t.Fatalf("CreateIssue(root): %v", err) } diff --git a/cmd/bd/swarm.go b/cmd/bd/swarm.go index 6ec3d5dd..d0be69a9 100644 --- a/cmd/bd/swarm.go +++ b/cmd/bd/swarm.go @@ -77,7 +77,7 @@ func findExistingSwarm(ctx context.Context, s SwarmStorage, epicID string) (*typ // Find a swarm molecule with relates-to dependency to this epic for _, dep := range dependents { // Only consider molecules (GetDependents doesn't populate mol_type, so we fetch full issue) - if dep.IssueType != types.TypeMolecule { + if dep.IssueType != "molecule" { continue } @@ -187,7 +187,7 @@ Examples: } // Verify it's an epic - if epic.IssueType != types.TypeEpic && epic.IssueType != types.TypeMolecule { + if epic.IssueType != types.TypeEpic && epic.IssueType != "molecule" { FatalErrorRespectJSON("'%s' is not an epic or molecule (type: %s)", epicID, epic.IssueType) } @@ -655,7 +655,7 @@ Examples: var epic *types.Issue // Check if it's a swarm molecule - if so, follow the link to the epic - if issue.IssueType == types.TypeMolecule && issue.MolType == types.MolTypeSwarm { + if issue.IssueType == "molecule" && issue.MolType == types.MolTypeSwarm { // Find linked epic via relates-to dependency deps, err := store.GetDependencyRecords(ctx, issue.ID) if err != nil { @@ -673,7 +673,7 @@ Examples: if epic == nil { FatalErrorRespectJSON("swarm molecule '%s' has no linked epic", issueID) } - } else if issue.IssueType == types.TypeEpic || issue.IssueType == types.TypeMolecule { + } else if issue.IssueType == types.TypeEpic || issue.IssueType == "molecule" { epic = issue } else { FatalErrorRespectJSON("'%s' is not an epic or swarm molecule (type: %s)", issueID, issue.IssueType) @@ -950,7 +950,7 @@ Examples: var epicTitle string // Check if it's an epic or single issue that needs wrapping - if issue.IssueType == types.TypeEpic || issue.IssueType == types.TypeMolecule { + if issue.IssueType == types.TypeEpic || issue.IssueType == "molecule" { epicID = issue.ID epicTitle = issue.Title } else { @@ -1042,7 +1042,7 @@ Examples: Description: fmt.Sprintf("Swarm molecule orchestrating epic %s.\n\nEpic: %s\nCoordinator: %s", epicID, epicID, coordinator), Status: types.StatusOpen, Priority: epic.Priority, - IssueType: types.TypeMolecule, + IssueType: "molecule", MolType: types.MolTypeSwarm, Assignee: coordinator, CreatedBy: actor, diff --git a/cmd/bd/thread_test.go b/cmd/bd/thread_test.go index 9b2881fc..b7f5b751 100644 --- a/cmd/bd/thread_test.go +++ b/cmd/bd/thread_test.go @@ -24,7 +24,7 @@ func TestThreadTraversal(t *testing.T) { Description: "This is the original message", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Assignee: "worker", Sender: "manager", Ephemeral: true, @@ -40,7 +40,7 @@ func TestThreadTraversal(t *testing.T) { Description: "This is reply 1", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Assignee: "manager", Sender: "worker", Ephemeral: true, @@ -56,7 +56,7 @@ func TestThreadTraversal(t *testing.T) { Description: "This is reply 2", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Assignee: "worker", Sender: "manager", Ephemeral: true, @@ -187,7 +187,7 @@ func TestThreadTraversalEmptyThread(t *testing.T) { Description: "This message has no thread", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Assignee: "user", Sender: "sender", Ephemeral: true, @@ -225,7 +225,7 @@ func TestThreadTraversalBranching(t *testing.T) { Description: "This message will have multiple replies", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Assignee: "user", Sender: "sender", Ephemeral: true, @@ -242,7 +242,7 @@ func TestThreadTraversalBranching(t *testing.T) { Description: "First branch reply", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Assignee: "sender", Sender: "user", Ephemeral: true, @@ -258,7 +258,7 @@ func TestThreadTraversalBranching(t *testing.T) { Description: "Second branch reply", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Assignee: "sender", Sender: "another-user", Ephemeral: true, @@ -361,7 +361,7 @@ func TestThreadTraversalOnlyRepliesTo(t *testing.T) { Description: "First message (target of blocks dep)", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Assignee: "user", Sender: "sender", Ephemeral: true, @@ -377,7 +377,7 @@ func TestThreadTraversalOnlyRepliesTo(t *testing.T) { Description: "Second message with blocks dependency to msg1", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Assignee: "user", Sender: "sender", Ephemeral: true, diff --git a/cmd/bd/types.go b/cmd/bd/types.go index 69bc98e6..171bd431 100644 --- a/cmd/bd/types.go +++ b/cmd/bd/types.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "strings" "github.com/spf13/cobra" "github.com/steveyegge/beads/internal/types" @@ -22,24 +21,6 @@ var coreWorkTypes = []struct { {types.TypeEpic, "Large body of work spanning multiple issues"}, } -// 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)"}, - {types.TypeAgent, "Agent identity bead"}, - {types.TypeRole, "Agent role definition"}, - {types.TypeRig, "Rig identity bead (multi-repo workspace)"}, - {types.TypeEvent, "Operational state change record"}, - {types.TypeMessage, "Ephemeral communication between workers"}, -} - var typesCmd = &cobra.Command{ Use: "types", GroupID: "views", @@ -96,34 +77,12 @@ Examples: 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) - } + fmt.Printf(" %s\n", t) } } else { fmt.Println("\nNo custom types configured.") fmt.Println("Configure with: bd config set types.custom \"type1,type2,...\"") } - - // 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, ", ")) - } }, } diff --git a/internal/beads/beads.go b/internal/beads/beads.go index 65e84e5e..82061586 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -342,14 +342,13 @@ const ( StatusClosed = types.StatusClosed ) -// IssueType constants +// IssueType constants (core types only - Gas Town types removed) const ( - TypeBug = types.TypeBug - TypeFeature = types.TypeFeature - TypeTask = types.TypeTask - TypeEpic = types.TypeEpic - TypeChore = types.TypeChore - TypeMolecule = types.TypeMolecule + TypeBug = types.TypeBug + TypeFeature = types.TypeFeature + TypeTask = types.TypeTask + TypeEpic = types.TypeEpic + TypeChore = types.TypeChore ) // DependencyType constants diff --git a/internal/molecules/molecules_test.go b/internal/molecules/molecules_test.go index 891ebca2..9e358235 100644 --- a/internal/molecules/molecules_test.go +++ b/internal/molecules/molecules_test.go @@ -164,7 +164,7 @@ func TestLoader_SkipExistingMolecules(t *testing.T) { existingMol := &types.Issue{ ID: "mol-existing", Title: "Existing Molecule", - IssueType: types.TypeMolecule, + IssueType: "molecule", Status: types.StatusOpen, IsTemplate: true, } diff --git a/internal/rpc/client_gate_shutdown_test.go b/internal/rpc/client_gate_shutdown_test.go index 66cacfe5..37d75a60 100644 --- a/internal/rpc/client_gate_shutdown_test.go +++ b/internal/rpc/client_gate_shutdown_test.go @@ -51,7 +51,7 @@ func TestClient_GateLifecycleAndShutdown(t *testing.T) { if err := json.Unmarshal(showResp.Data, &gate); err != nil { t.Fatalf("unmarshal GateShow: %v", err) } - if gate.ID != created.ID || gate.IssueType != types.TypeGate { + if gate.ID != created.ID || gate.IssueType != "gate" { t.Fatalf("unexpected gate: %+v", gate) } diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index 546d32bb..05fffd80 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -2053,7 +2053,7 @@ func (s *Server) handleGateCreate(req *Request) Response { // Create gate issue gate := &types.Issue{ Title: args.Title, - IssueType: types.TypeGate, + IssueType: "gate", Status: types.StatusOpen, Priority: 1, // Gates are typically high priority Assignee: "deacon/", @@ -2104,7 +2104,7 @@ func (s *Server) handleGateList(req *Request) Response { ctx := s.reqCtx(req) // Build filter for gates - gateType := types.TypeGate + gateType := types.IssueType("gate") filter := types.IssueFilter{ IssueType: &gateType, } @@ -2169,7 +2169,7 @@ func (s *Server) handleGateShow(req *Request) Response { Error: fmt.Sprintf("gate %s not found", gateID), } } - if gate.IssueType != types.TypeGate { + if gate.IssueType != "gate" { return Response{ Success: false, Error: fmt.Sprintf("%s is not a gate (type: %s)", gateID, gate.IssueType), @@ -2225,7 +2225,7 @@ func (s *Server) handleGateClose(req *Request) Response { Error: fmt.Sprintf("gate %s not found", gateID), } } - if gate.IssueType != types.TypeGate { + if gate.IssueType != "gate" { return Response{ Success: false, Error: fmt.Sprintf("%s is not a gate (type: %s)", gateID, gate.IssueType), @@ -2304,7 +2304,7 @@ func (s *Server) handleGateWait(req *Request) Response { Error: fmt.Sprintf("gate %s not found", gateID), } } - if gate.IssueType != types.TypeGate { + if gate.IssueType != "gate" { return Response{ Success: false, Error: fmt.Sprintf("%s is not a gate (type: %s)", gateID, gate.IssueType), diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index fe14fcbf..96905dfd 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -1085,8 +1085,9 @@ func (m *MemoryStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte } else { // Exclude workflow types from ready work by default // These are internal workflow items, not work for polecats to claim + // (Gas Town types - not built into beads core) switch issue.IssueType { - case types.TypeMergeRequest, types.TypeGate, types.TypeMolecule, types.TypeMessage: + case "merge-request", "gate", "molecule", "message": continue } } diff --git a/internal/storage/memory/memory_more_coverage_test.go b/internal/storage/memory/memory_more_coverage_test.go index fd300b81..cb817c0d 100644 --- a/internal/storage/memory/memory_more_coverage_test.go +++ b/internal/storage/memory/memory_more_coverage_test.go @@ -594,7 +594,7 @@ func TestMemoryStorage_UpdateIssue_SearchIssues_ReadyWork_BlockedIssues(t *testi child := &types.Issue{ID: "bd-2", Title: "Child", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask, Assignee: assignee} blocker := &types.Issue{ID: "bd-3", Title: "Blocker", Status: types.StatusOpen, Priority: 3, IssueType: types.TypeTask} pinned := &types.Issue{ID: "bd-4", Title: "Pinned", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, Pinned: true} - workflow := &types.Issue{ID: "bd-5", Title: "Workflow", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeMergeRequest} + workflow := &types.Issue{ID: "bd-5", Title: "Workflow", Status: types.StatusOpen, Priority: 1, IssueType: "merge-request"} for _, iss := range []*types.Issue{parent, child, blocker, pinned, workflow} { if err := store.CreateIssue(ctx, iss, "actor"); err != nil { t.Fatalf("CreateIssue %s: %v", iss.ID, err) @@ -720,7 +720,7 @@ func TestMemoryStorage_UpdateIssue_SearchIssues_ReadyWork_BlockedIssues(t *testi } // Filter by workflow type explicitly. - ready, err = store.GetReadyWork(ctx, types.WorkFilter{Type: string(types.TypeMergeRequest)}) + ready, err = store.GetReadyWork(ctx, types.WorkFilter{Type: "merge-request"}) if err != nil { t.Fatalf("GetReadyWork type: %v", err) } diff --git a/internal/storage/sqlite/gate_no_daemon_test.go b/internal/storage/sqlite/gate_no_daemon_test.go index 8c828931..86f19e97 100644 --- a/internal/storage/sqlite/gate_no_daemon_test.go +++ b/internal/storage/sqlite/gate_no_daemon_test.go @@ -39,7 +39,7 @@ func TestGateFieldsPreservedAcrossConnections(t *testing.T) { Title: "Test Gate", Status: types.StatusOpen, Priority: 1, - IssueType: types.TypeGate, + IssueType: "gate", Ephemeral: true, AwaitType: "timer", AwaitID: "5s", diff --git a/internal/storage/sqlite/graph_links_test.go b/internal/storage/sqlite/graph_links_test.go index d3c42dab..263007e6 100644 --- a/internal/storage/sqlite/graph_links_test.go +++ b/internal/storage/sqlite/graph_links_test.go @@ -292,7 +292,7 @@ func TestRepliesTo(t *testing.T) { Description: "Original content", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Sender: "alice", Assignee: "bob", Ephemeral: true, @@ -304,7 +304,7 @@ func TestRepliesTo(t *testing.T) { Description: "Reply content", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Sender: "bob", Assignee: "alice", Ephemeral: true, @@ -360,7 +360,7 @@ func TestRepliesTo_Chain(t *testing.T) { Title: "Message", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Sender: "user", Assignee: "inbox", Ephemeral: true, @@ -414,7 +414,7 @@ func TestWispField(t *testing.T) { Title: "Wisp Issue", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Ephemeral: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), @@ -467,7 +467,7 @@ func TestWispFilter(t *testing.T) { Title: "Wisp", Status: types.StatusClosed, // Closed for cleanup test Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Ephemeral: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), @@ -534,7 +534,7 @@ func TestSenderField(t *testing.T) { Title: "Message", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Sender: "alice@example.com", Assignee: "bob@example.com", CreatedAt: time.Now(), @@ -565,7 +565,7 @@ func TestMessageType(t *testing.T) { Title: "Test Message", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -579,12 +579,12 @@ func TestMessageType(t *testing.T) { if err != nil { t.Fatalf("GetIssue failed: %v", err) } - if saved.IssueType != types.TypeMessage { - t.Errorf("IssueType = %q, want %q", saved.IssueType, types.TypeMessage) + if saved.IssueType != "message" { + t.Errorf("IssueType = %q, want %q", saved.IssueType, "message") } // Filter by message type - messageType := types.TypeMessage + messageType := types.IssueType("message") filter := types.IssueFilter{ IssueType: &messageType, } diff --git a/internal/storage/sqlite/multirepo_test.go b/internal/storage/sqlite/multirepo_test.go index 95db2640..a532e089 100644 --- a/internal/storage/sqlite/multirepo_test.go +++ b/internal/storage/sqlite/multirepo_test.go @@ -1128,7 +1128,7 @@ func TestUpsertPreservesGateFields(t *testing.T) { Title: "Test Gate", Status: types.StatusOpen, Priority: 1, - IssueType: types.TypeGate, + IssueType: "gate", Ephemeral: true, AwaitType: "gh:run", AwaitID: "123456789", @@ -1170,7 +1170,7 @@ func TestUpsertPreservesGateFields(t *testing.T) { Title: "Test Gate Updated", // Different title to trigger update Status: types.StatusOpen, Priority: 1, - IssueType: types.TypeGate, + IssueType: "gate", AwaitType: "", // Empty - simulating JSONL without await fields AwaitID: "", // Empty Timeout: 0, diff --git a/internal/storage/sqlite/transaction_test.go b/internal/storage/sqlite/transaction_test.go index 1c2d1959..f18cfc0c 100644 --- a/internal/storage/sqlite/transaction_test.go +++ b/internal/storage/sqlite/transaction_test.go @@ -524,14 +524,14 @@ func TestTransactionAddDependency_RepliesTo(t *testing.T) { Title: "Original Message", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Sender: "alice", } reply := &types.Issue{ Title: "Re: Original Message", Status: types.StatusOpen, Priority: 2, - IssueType: types.TypeMessage, + IssueType: "message", Sender: "bob", } if err := store.CreateIssue(ctx, original, "test-actor"); err != nil { diff --git a/internal/types/types.go b/internal/types/types.go index 9c682bd7..ff7c42d0 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -486,21 +486,9 @@ const ( 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 ( - 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 - TypeGate IssueType = "gate" // Async coordination gate - TypeAgent IssueType = "agent" // Agent identity bead - TypeRole IssueType = "role" // Agent role definition - TypeRig IssueType = "rig" // Rig identity bead (multi-repo workspace) - TypeConvoy IssueType = "convoy" // Cross-project tracking with reactive completion - TypeEvent IssueType = "event" // Operational state change record - TypeSlot IssueType = "slot" // Exclusive access slot (merge-slot gate) -) +// Note: Gas Town types (molecule, gate, convoy, merge-request, slot, agent, role, rig, event, message) +// were removed from beads core. They are now purely custom types with no built-in constants. +// Use string literals like types.IssueType("molecule") if needed, and configure types.custom. // IsValid checks if the issue type is a core work type. // Only core work types (bug, feature, task, epic, chore) are built-in. @@ -538,16 +526,12 @@ func (t IssueType) IsValidWithCustom(customTypes []string) bool { } // Normalize maps issue type aliases to their canonical form. -// For example, "enhancement" -> "feature", "mr" -> "merge-request". +// For example, "enhancement" -> "feature". // Case-insensitive to match util.NormalizeIssueType behavior. func (t IssueType) Normalize() IssueType { switch strings.ToLower(string(t)) { case "enhancement", "feat": return TypeFeature - case "mr": - return TypeMergeRequest - case "mol": - return TypeMolecule default: return t } diff --git a/internal/types/types_test.go b/internal/types/types_test.go index ebfcb0b2..622e75d7 100644 --- a/internal/types/types_test.go +++ b/internal/types/types_test.go @@ -442,12 +442,12 @@ func TestValidateForImport(t *testing.T) { wantErr: false, // Should pass - federation trust model }, { - name: "built-in type agent passes", + name: "custom type passes (federation trust)", issue: Issue{ Title: "Test Issue", Status: StatusOpen, Priority: 1, - IssueType: TypeAgent, // Gas Town built-in type + IssueType: IssueType("agent"), // Custom type (no longer built-in) }, wantErr: false, }, @@ -545,17 +545,17 @@ func TestIssueTypeIsValid(t *testing.T) { {TypeTask, true}, {TypeEpic, true}, {TypeChore, 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}, + // Gas Town types are now custom types (not built-in) + {IssueType("message"), false}, + {IssueType("merge-request"), false}, + {IssueType("molecule"), false}, + {IssueType("gate"), false}, + {IssueType("agent"), false}, + {IssueType("role"), false}, + {IssueType("convoy"), false}, + {IssueType("event"), false}, + {IssueType("slot"), false}, + {IssueType("rig"), false}, // Invalid types {IssueType("invalid"), false}, {IssueType(""), false}, @@ -581,12 +581,12 @@ func TestIssueTypeRequiredSections(t *testing.T) { {TypeTask, 1, "## Acceptance Criteria"}, {TypeEpic, 1, "## Success Criteria"}, {TypeChore, 0, ""}, - {TypeMessage, 0, ""}, - {TypeMolecule, 0, ""}, - {TypeGate, 0, ""}, - {TypeEvent, 0, ""}, - {TypeMergeRequest, 0, ""}, - // Gas Town types (agent, role, rig, convoy, slot) have been removed + // Gas Town types are now custom and have no required sections + {IssueType("message"), 0, ""}, + {IssueType("molecule"), 0, ""}, + {IssueType("gate"), 0, ""}, + {IssueType("event"), 0, ""}, + {IssueType("merge-request"), 0, ""}, } for _, tt := range tests { diff --git a/internal/validation/template_test.go b/internal/validation/template_test.go index 73dbab5b..f91e4627 100644 --- a/internal/validation/template_test.go +++ b/internal/validation/template_test.go @@ -123,13 +123,13 @@ Widget displays correctly`, }, { name: "message has no requirements", - issueType: types.TypeMessage, + issueType: "message", description: "Hello", wantErr: false, }, { name: "molecule has no requirements", - issueType: types.TypeMolecule, + issueType: "molecule", description: "", wantErr: false, },