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 <noreply@anthropic.com>
This commit is contained in:
collins
2026-01-21 10:30:38 -08:00
committed by Steve Yegge
parent 4a0f4abc70
commit 7cf67153de
25 changed files with 99 additions and 162 deletions
+6 -7
View File
@@ -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
+1 -1
View File
@@ -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,
}
+1 -1
View File
@@ -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)
}
+5 -5
View File
@@ -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),
+2 -1
View File
@@ -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
}
}
@@ -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)
}
@@ -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",
+10 -10
View File
@@ -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,
}
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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 {
+4 -20
View File
@@ -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
}
+19 -19
View File
@@ -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 {
+2 -2
View File
@@ -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,
},