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
+3 -3
View File
@@ -391,9 +391,9 @@ func TestCoverage_ShowThread(t *testing.T) {
} }
ctx := context.Background() ctx := context.Background()
root := &types.Issue{Title: "Root message", 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: types.TypeMessage, Status: types.StatusOpen, Sender: "bob", Assignee: "alice"} reply1 := &types.Issue{Title: "Re: Root", IssueType: "message", Status: types.StatusOpen, Sender: "bob", Assignee: "alice"}
reply2 := &types.Issue{Title: "Re: Re: Root", IssueType: types.TypeMessage, Status: types.StatusOpen, Sender: "alice", Assignee: "bob"} 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 { if err := s.CreateIssue(ctx, root, "test-user"); err != nil {
s.Close() s.Close()
t.Fatalf("CreateIssue root: %v", err) t.Fatalf("CreateIssue root: %v", err)
+1 -1
View File
@@ -521,7 +521,7 @@ func createGateIssue(step *formula.Step, parentID string) *types.Issue {
Description: fmt.Sprintf("Async gate for step %s", step.ID), Description: fmt.Sprintf("Async gate for step %s", step.ID),
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeGate, IssueType: "gate",
AwaitType: step.Gate.Type, AwaitType: step.Gate.Type,
AwaitID: step.Gate.ID, AwaitID: step.Gate.ID,
Timeout: timeout, Timeout: timeout,
+2 -8
View File
@@ -32,20 +32,14 @@ var obsidianPriority = []string{
"⏬", // 4 = backlog/lowest "⏬", // 4 = backlog/lowest
} }
// obsidianTypeTag maps bd issue type to Obsidian tag // obsidianTypeTag maps bd issue type to Obsidian tag (core types only)
// Note: Gas Town-specific types (agent, role, rig, convoy, slot) are now labels. // Gas Town types are custom types and will use their issue_type value as a tag.
// The labels will be converted to tags automatically via the label->tag logic.
var obsidianTypeTag = map[types.IssueType]string{ var obsidianTypeTag = map[types.IssueType]string{
types.TypeBug: "#Bug", types.TypeBug: "#Bug",
types.TypeFeature: "#Feature", types.TypeFeature: "#Feature",
types.TypeTask: "#Task", types.TypeTask: "#Task",
types.TypeEpic: "#Epic", types.TypeEpic: "#Epic",
types.TypeChore: "#Chore", types.TypeChore: "#Chore",
types.TypeMessage: "#Message",
types.TypeMergeRequest: "#MergeRequest",
types.TypeMolecule: "#Molecule",
types.TypeGate: "#Gate",
types.TypeEvent: "#Event",
} }
// formatObsidianTask converts a single issue to Obsidian Tasks format // formatObsidianTask converts a single issue to Obsidian Tasks format
+5 -5
View File
@@ -61,7 +61,7 @@ By default, shows only open gates. Use --all to include closed gates.`,
limit, _ := cmd.Flags().GetInt("limit") limit, _ := cmd.Flags().GetInt("limit")
// Build filter for gate type issues // Build filter for gate type issues
gateType := types.TypeGate gateType := types.IssueType("gate")
filter := types.IssueFilter{ filter := types.IssueFilter{
IssueType: &gateType, IssueType: &gateType,
Limit: limit, 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) fmt.Fprintf(os.Stderr, "Error: %s is not a gate issue (type=%s)\n", gateID, issue.IssueType)
os.Exit(1) 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) fmt.Fprintf(os.Stderr, "Error: %s is not a gate issue (type=%s)\n", gateID, issue.IssueType)
os.Exit(1) 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) fmt.Fprintf(os.Stderr, "Error: %s is not a gate issue (type=%s)\n", gateID, issue.IssueType)
os.Exit(1) os.Exit(1)
} }
@@ -492,7 +492,7 @@ Examples:
limit, _ := cmd.Flags().GetInt("limit") limit, _ := cmd.Flags().GetInt("limit")
// Get open gates // Get open gates
gateType := types.TypeGate gateType := types.IssueType("gate")
filter := types.IssueFilter{ filter := types.IssueFilter{
IssueType: &gateType, IssueType: &gateType,
ExcludeStatus: []types.Status{types.StatusClosed}, ExcludeStatus: []types.Status{types.StatusClosed},
+1 -1
View File
@@ -228,7 +228,7 @@ func findPendingGates() ([]*types.Issue, error) {
} }
} else { } else {
// Direct mode // Direct mode
gateType := types.TypeGate gateType := types.IssueType("gate")
filter := types.IssueFilter{ filter := types.IssueFilter{
IssueType: &gateType, IssueType: &gateType,
ExcludeStatus: []types.Status{types.StatusClosed}, ExcludeStatus: []types.Status{types.StatusClosed},
+1 -1
View File
@@ -796,7 +796,7 @@ var listCmd = &cobra.Command{
// Gate filtering: exclude gate issues by default (bd-7zka.2) // Gate filtering: exclude gate issues by default (bd-7zka.2)
// Use --include-gates or --type gate to show gate issues // Use --include-gates or --type gate to show gate issues
if !includeGates && issueType != "gate" { if !includeGates && issueType != "gate" {
filter.ExcludeTypes = append(filter.ExcludeTypes, types.TypeGate) filter.ExcludeTypes = append(filter.ExcludeTypes, "gate")
} }
// Parent filtering: filter children by parent issue // Parent filtering: filter children by parent issue
+1 -1
View File
@@ -113,7 +113,7 @@ func runMolReadyGated(cmd *cobra.Command, args []string) {
// 5. Filter out molecules that are already hooked by someone // 5. Filter out molecules that are already hooked by someone
func findGateReadyMolecules(ctx context.Context, s storage.Storage) ([]*GatedMolecule, error) { func findGateReadyMolecules(ctx context.Context, s storage.Storage) ([]*GatedMolecule, error) {
// Step 1: Find all closed gate beads // Step 1: Find all closed gate beads
gateType := types.TypeGate gateType := types.IssueType("gate")
closedStatus := types.StatusClosed closedStatus := types.StatusClosed
gateFilter := types.IssueFilter{ gateFilter := types.IssueFilter{
IssueType: &gateType, IssueType: &gateType,
+4 -4
View File
@@ -127,7 +127,7 @@ func TestFindGateReadyMolecules_ClosedGate(t *testing.T) {
gate := &types.Issue{ gate := &types.Issue{
ID: "test-mol-002.gate-await-ci", ID: "test-mol-002.gate-await-ci",
Title: "Gate: gh:run ci-workflow", Title: "Gate: gh:run ci-workflow",
IssueType: types.TypeGate, IssueType: "gate",
Status: types.StatusClosed, // Gate has closed Status: types.StatusClosed, // Gate has closed
AwaitType: "gh:run", AwaitType: "gh:run",
AwaitID: "ci-workflow", AwaitID: "ci-workflow",
@@ -222,7 +222,7 @@ func TestFindGateReadyMolecules_OpenGate(t *testing.T) {
gate := &types.Issue{ gate := &types.Issue{
ID: "test-mol-003.gate-await-ci", ID: "test-mol-003.gate-await-ci",
Title: "Gate: gh:run ci-workflow", Title: "Gate: gh:run ci-workflow",
IssueType: types.TypeGate, IssueType: "gate",
Status: types.StatusOpen, // Gate is still open Status: types.StatusOpen, // Gate is still open
AwaitType: "gh:run", AwaitType: "gh:run",
AwaitID: "ci-workflow", AwaitID: "ci-workflow",
@@ -302,7 +302,7 @@ func TestFindGateReadyMolecules_HookedMolecule(t *testing.T) {
gate := &types.Issue{ gate := &types.Issue{
ID: "test-mol-004.gate-await-ci", ID: "test-mol-004.gate-await-ci",
Title: "Gate: gh:run ci-workflow", Title: "Gate: gh:run ci-workflow",
IssueType: types.TypeGate, IssueType: "gate",
Status: types.StatusClosed, Status: types.StatusClosed,
AwaitType: "gh:run", AwaitType: "gh:run",
AwaitID: "ci-workflow", AwaitID: "ci-workflow",
@@ -384,7 +384,7 @@ func TestFindGateReadyMolecules_MultipleGates(t *testing.T) {
gate := &types.Issue{ gate := &types.Issue{
ID: fmt.Sprintf("%s.gate", molID), ID: fmt.Sprintf("%s.gate", molID),
Title: "Gate: gh:run", Title: "Gate: gh:run",
IssueType: types.TypeGate, IssueType: "gate",
Status: types.StatusClosed, Status: types.StatusClosed,
AwaitType: "gh:run", AwaitType: "gh:run",
CreatedAt: time.Now(), CreatedAt: time.Now(),
+3 -3
View File
@@ -105,9 +105,9 @@ func TestFindRepliesToAndReplies_WorksWithMemoryStorage(t *testing.T) {
t.Fatalf("SetConfig types.custom: %v", err) t.Fatalf("SetConfig types.custom: %v", err)
} }
root := &types.Issue{Title: "root", 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: types.TypeMessage, Sender: "b", Assignee: "a"} 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: types.TypeMessage, Sender: "a", Assignee: "b"} 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 { if err := st.CreateIssue(ctx, root, "tester"); err != nil {
t.Fatalf("CreateIssue(root): %v", err) t.Fatalf("CreateIssue(root): %v", err)
} }
+6 -6
View File
@@ -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 // Find a swarm molecule with relates-to dependency to this epic
for _, dep := range dependents { for _, dep := range dependents {
// Only consider molecules (GetDependents doesn't populate mol_type, so we fetch full issue) // Only consider molecules (GetDependents doesn't populate mol_type, so we fetch full issue)
if dep.IssueType != types.TypeMolecule { if dep.IssueType != "molecule" {
continue continue
} }
@@ -187,7 +187,7 @@ Examples:
} }
// Verify it's an epic // 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) FatalErrorRespectJSON("'%s' is not an epic or molecule (type: %s)", epicID, epic.IssueType)
} }
@@ -655,7 +655,7 @@ Examples:
var epic *types.Issue var epic *types.Issue
// Check if it's a swarm molecule - if so, follow the link to the epic // 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 // Find linked epic via relates-to dependency
deps, err := store.GetDependencyRecords(ctx, issue.ID) deps, err := store.GetDependencyRecords(ctx, issue.ID)
if err != nil { if err != nil {
@@ -673,7 +673,7 @@ Examples:
if epic == nil { if epic == nil {
FatalErrorRespectJSON("swarm molecule '%s' has no linked epic", issueID) 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 epic = issue
} else { } else {
FatalErrorRespectJSON("'%s' is not an epic or swarm molecule (type: %s)", issueID, issue.IssueType) FatalErrorRespectJSON("'%s' is not an epic or swarm molecule (type: %s)", issueID, issue.IssueType)
@@ -950,7 +950,7 @@ Examples:
var epicTitle string var epicTitle string
// Check if it's an epic or single issue that needs wrapping // 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 epicID = issue.ID
epicTitle = issue.Title epicTitle = issue.Title
} else { } else {
@@ -1042,7 +1042,7 @@ Examples:
Description: fmt.Sprintf("Swarm molecule orchestrating epic %s.\n\nEpic: %s\nCoordinator: %s", epicID, epicID, coordinator), Description: fmt.Sprintf("Swarm molecule orchestrating epic %s.\n\nEpic: %s\nCoordinator: %s", epicID, epicID, coordinator),
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: epic.Priority, Priority: epic.Priority,
IssueType: types.TypeMolecule, IssueType: "molecule",
MolType: types.MolTypeSwarm, MolType: types.MolTypeSwarm,
Assignee: coordinator, Assignee: coordinator,
CreatedBy: actor, CreatedBy: actor,
+9 -9
View File
@@ -24,7 +24,7 @@ func TestThreadTraversal(t *testing.T) {
Description: "This is the original message", Description: "This is the original message",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Assignee: "worker", Assignee: "worker",
Sender: "manager", Sender: "manager",
Ephemeral: true, Ephemeral: true,
@@ -40,7 +40,7 @@ func TestThreadTraversal(t *testing.T) {
Description: "This is reply 1", Description: "This is reply 1",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Assignee: "manager", Assignee: "manager",
Sender: "worker", Sender: "worker",
Ephemeral: true, Ephemeral: true,
@@ -56,7 +56,7 @@ func TestThreadTraversal(t *testing.T) {
Description: "This is reply 2", Description: "This is reply 2",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Assignee: "worker", Assignee: "worker",
Sender: "manager", Sender: "manager",
Ephemeral: true, Ephemeral: true,
@@ -187,7 +187,7 @@ func TestThreadTraversalEmptyThread(t *testing.T) {
Description: "This message has no thread", Description: "This message has no thread",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Assignee: "user", Assignee: "user",
Sender: "sender", Sender: "sender",
Ephemeral: true, Ephemeral: true,
@@ -225,7 +225,7 @@ func TestThreadTraversalBranching(t *testing.T) {
Description: "This message will have multiple replies", Description: "This message will have multiple replies",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Assignee: "user", Assignee: "user",
Sender: "sender", Sender: "sender",
Ephemeral: true, Ephemeral: true,
@@ -242,7 +242,7 @@ func TestThreadTraversalBranching(t *testing.T) {
Description: "First branch reply", Description: "First branch reply",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Assignee: "sender", Assignee: "sender",
Sender: "user", Sender: "user",
Ephemeral: true, Ephemeral: true,
@@ -258,7 +258,7 @@ func TestThreadTraversalBranching(t *testing.T) {
Description: "Second branch reply", Description: "Second branch reply",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Assignee: "sender", Assignee: "sender",
Sender: "another-user", Sender: "another-user",
Ephemeral: true, Ephemeral: true,
@@ -361,7 +361,7 @@ func TestThreadTraversalOnlyRepliesTo(t *testing.T) {
Description: "First message (target of blocks dep)", Description: "First message (target of blocks dep)",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Assignee: "user", Assignee: "user",
Sender: "sender", Sender: "sender",
Ephemeral: true, Ephemeral: true,
@@ -377,7 +377,7 @@ func TestThreadTraversalOnlyRepliesTo(t *testing.T) {
Description: "Second message with blocks dependency to msg1", Description: "Second message with blocks dependency to msg1",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Assignee: "user", Assignee: "user",
Sender: "sender", Sender: "sender",
Ephemeral: true, Ephemeral: true,
-41
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
@@ -22,24 +21,6 @@ var coreWorkTypes = []struct {
{types.TypeEpic, "Large body of work spanning multiple issues"}, {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{ var typesCmd = &cobra.Command{
Use: "types", Use: "types",
GroupID: "views", GroupID: "views",
@@ -96,34 +77,12 @@ Examples:
if len(customTypes) > 0 { if len(customTypes) > 0 {
fmt.Println("\nConfigured custom types:") fmt.Println("\nConfigured custom types:")
for _, t := range customTypes { 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 { } else {
fmt.Println("\nNo custom types configured.") fmt.Println("\nNo custom types configured.")
fmt.Println("Configure with: bd config set types.custom \"type1,type2,...\"") 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, ", "))
}
}, },
} }
+1 -2
View File
@@ -342,14 +342,13 @@ const (
StatusClosed = types.StatusClosed StatusClosed = types.StatusClosed
) )
// IssueType constants // IssueType constants (core types only - Gas Town types removed)
const ( const (
TypeBug = types.TypeBug TypeBug = types.TypeBug
TypeFeature = types.TypeFeature TypeFeature = types.TypeFeature
TypeTask = types.TypeTask TypeTask = types.TypeTask
TypeEpic = types.TypeEpic TypeEpic = types.TypeEpic
TypeChore = types.TypeChore TypeChore = types.TypeChore
TypeMolecule = types.TypeMolecule
) )
// DependencyType constants // DependencyType constants
+1 -1
View File
@@ -164,7 +164,7 @@ func TestLoader_SkipExistingMolecules(t *testing.T) {
existingMol := &types.Issue{ existingMol := &types.Issue{
ID: "mol-existing", ID: "mol-existing",
Title: "Existing Molecule", Title: "Existing Molecule",
IssueType: types.TypeMolecule, IssueType: "molecule",
Status: types.StatusOpen, Status: types.StatusOpen,
IsTemplate: true, 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 { if err := json.Unmarshal(showResp.Data, &gate); err != nil {
t.Fatalf("unmarshal GateShow: %v", err) 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) t.Fatalf("unexpected gate: %+v", gate)
} }
+5 -5
View File
@@ -2053,7 +2053,7 @@ func (s *Server) handleGateCreate(req *Request) Response {
// Create gate issue // Create gate issue
gate := &types.Issue{ gate := &types.Issue{
Title: args.Title, Title: args.Title,
IssueType: types.TypeGate, IssueType: "gate",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, // Gates are typically high priority Priority: 1, // Gates are typically high priority
Assignee: "deacon/", Assignee: "deacon/",
@@ -2104,7 +2104,7 @@ func (s *Server) handleGateList(req *Request) Response {
ctx := s.reqCtx(req) ctx := s.reqCtx(req)
// Build filter for gates // Build filter for gates
gateType := types.TypeGate gateType := types.IssueType("gate")
filter := types.IssueFilter{ filter := types.IssueFilter{
IssueType: &gateType, IssueType: &gateType,
} }
@@ -2169,7 +2169,7 @@ func (s *Server) handleGateShow(req *Request) Response {
Error: fmt.Sprintf("gate %s not found", gateID), Error: fmt.Sprintf("gate %s not found", gateID),
} }
} }
if gate.IssueType != types.TypeGate { if gate.IssueType != "gate" {
return Response{ return Response{
Success: false, Success: false,
Error: fmt.Sprintf("%s is not a gate (type: %s)", gateID, gate.IssueType), 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), Error: fmt.Sprintf("gate %s not found", gateID),
} }
} }
if gate.IssueType != types.TypeGate { if gate.IssueType != "gate" {
return Response{ return Response{
Success: false, Success: false,
Error: fmt.Sprintf("%s is not a gate (type: %s)", gateID, gate.IssueType), 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), Error: fmt.Sprintf("gate %s not found", gateID),
} }
} }
if gate.IssueType != types.TypeGate { if gate.IssueType != "gate" {
return Response{ return Response{
Success: false, Success: false,
Error: fmt.Sprintf("%s is not a gate (type: %s)", gateID, gate.IssueType), 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 { } else {
// Exclude workflow types from ready work by default // Exclude workflow types from ready work by default
// These are internal workflow items, not work for polecats to claim // These are internal workflow items, not work for polecats to claim
// (Gas Town types - not built into beads core)
switch issue.IssueType { switch issue.IssueType {
case types.TypeMergeRequest, types.TypeGate, types.TypeMolecule, types.TypeMessage: case "merge-request", "gate", "molecule", "message":
continue 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} 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} 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} 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} { for _, iss := range []*types.Issue{parent, child, blocker, pinned, workflow} {
if err := store.CreateIssue(ctx, iss, "actor"); err != nil { if err := store.CreateIssue(ctx, iss, "actor"); err != nil {
t.Fatalf("CreateIssue %s: %v", iss.ID, err) 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. // 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 { if err != nil {
t.Fatalf("GetReadyWork type: %v", err) t.Fatalf("GetReadyWork type: %v", err)
} }
@@ -39,7 +39,7 @@ func TestGateFieldsPreservedAcrossConnections(t *testing.T) {
Title: "Test Gate", Title: "Test Gate",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeGate, IssueType: "gate",
Ephemeral: true, Ephemeral: true,
AwaitType: "timer", AwaitType: "timer",
AwaitID: "5s", AwaitID: "5s",
+10 -10
View File
@@ -292,7 +292,7 @@ func TestRepliesTo(t *testing.T) {
Description: "Original content", Description: "Original content",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Sender: "alice", Sender: "alice",
Assignee: "bob", Assignee: "bob",
Ephemeral: true, Ephemeral: true,
@@ -304,7 +304,7 @@ func TestRepliesTo(t *testing.T) {
Description: "Reply content", Description: "Reply content",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Sender: "bob", Sender: "bob",
Assignee: "alice", Assignee: "alice",
Ephemeral: true, Ephemeral: true,
@@ -360,7 +360,7 @@ func TestRepliesTo_Chain(t *testing.T) {
Title: "Message", Title: "Message",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Sender: "user", Sender: "user",
Assignee: "inbox", Assignee: "inbox",
Ephemeral: true, Ephemeral: true,
@@ -414,7 +414,7 @@ func TestWispField(t *testing.T) {
Title: "Wisp Issue", Title: "Wisp Issue",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Ephemeral: true, Ephemeral: true,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
@@ -467,7 +467,7 @@ func TestWispFilter(t *testing.T) {
Title: "Wisp", Title: "Wisp",
Status: types.StatusClosed, // Closed for cleanup test Status: types.StatusClosed, // Closed for cleanup test
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Ephemeral: true, Ephemeral: true,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
@@ -534,7 +534,7 @@ func TestSenderField(t *testing.T) {
Title: "Message", Title: "Message",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Sender: "alice@example.com", Sender: "alice@example.com",
Assignee: "bob@example.com", Assignee: "bob@example.com",
CreatedAt: time.Now(), CreatedAt: time.Now(),
@@ -565,7 +565,7 @@ func TestMessageType(t *testing.T) {
Title: "Test Message", Title: "Test Message",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
@@ -579,12 +579,12 @@ func TestMessageType(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("GetIssue failed: %v", err) t.Fatalf("GetIssue failed: %v", err)
} }
if saved.IssueType != types.TypeMessage { if saved.IssueType != "message" {
t.Errorf("IssueType = %q, want %q", saved.IssueType, types.TypeMessage) t.Errorf("IssueType = %q, want %q", saved.IssueType, "message")
} }
// Filter by message type // Filter by message type
messageType := types.TypeMessage messageType := types.IssueType("message")
filter := types.IssueFilter{ filter := types.IssueFilter{
IssueType: &messageType, IssueType: &messageType,
} }
+2 -2
View File
@@ -1128,7 +1128,7 @@ func TestUpsertPreservesGateFields(t *testing.T) {
Title: "Test Gate", Title: "Test Gate",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeGate, IssueType: "gate",
Ephemeral: true, Ephemeral: true,
AwaitType: "gh:run", AwaitType: "gh:run",
AwaitID: "123456789", AwaitID: "123456789",
@@ -1170,7 +1170,7 @@ func TestUpsertPreservesGateFields(t *testing.T) {
Title: "Test Gate Updated", // Different title to trigger update Title: "Test Gate Updated", // Different title to trigger update
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeGate, IssueType: "gate",
AwaitType: "", // Empty - simulating JSONL without await fields AwaitType: "", // Empty - simulating JSONL without await fields
AwaitID: "", // Empty AwaitID: "", // Empty
Timeout: 0, Timeout: 0,
+2 -2
View File
@@ -524,14 +524,14 @@ func TestTransactionAddDependency_RepliesTo(t *testing.T) {
Title: "Original Message", Title: "Original Message",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Sender: "alice", Sender: "alice",
} }
reply := &types.Issue{ reply := &types.Issue{
Title: "Re: Original Message", Title: "Re: Original Message",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: "message",
Sender: "bob", Sender: "bob",
} }
if err := store.CreateIssue(ctx, original, "test-actor"); err != nil { if err := store.CreateIssue(ctx, original, "test-actor"); err != nil {
+4 -20
View File
@@ -486,21 +486,9 @@ const (
TypeChore IssueType = "chore" TypeChore IssueType = "chore"
) )
// Well-known custom types - constants for code convenience. // Note: Gas Town types (molecule, gate, convoy, merge-request, slot, agent, role, rig, event, message)
// These are NOT built-in types and require types.custom configuration for validation. // were removed from beads core. They are now purely custom types with no built-in constants.
// Used by Gas Town and other infrastructure that extends beads. // Use string literals like types.IssueType("molecule") if needed, and configure types.custom.
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)
)
// IsValid checks if the issue type is a core work type. // IsValid checks if the issue type is a core work type.
// Only core work types (bug, feature, task, epic, chore) are built-in. // 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. // 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. // Case-insensitive to match util.NormalizeIssueType behavior.
func (t IssueType) Normalize() IssueType { func (t IssueType) Normalize() IssueType {
switch strings.ToLower(string(t)) { switch strings.ToLower(string(t)) {
case "enhancement", "feat": case "enhancement", "feat":
return TypeFeature return TypeFeature
case "mr":
return TypeMergeRequest
case "mol":
return TypeMolecule
default: default:
return t return t
} }
+19 -19
View File
@@ -442,12 +442,12 @@ func TestValidateForImport(t *testing.T) {
wantErr: false, // Should pass - federation trust model wantErr: false, // Should pass - federation trust model
}, },
{ {
name: "built-in type agent passes", name: "custom type passes (federation trust)",
issue: Issue{ issue: Issue{
Title: "Test Issue", Title: "Test Issue",
Status: StatusOpen, Status: StatusOpen,
Priority: 1, Priority: 1,
IssueType: TypeAgent, // Gas Town built-in type IssueType: IssueType("agent"), // Custom type (no longer built-in)
}, },
wantErr: false, wantErr: false,
}, },
@@ -545,17 +545,17 @@ func TestIssueTypeIsValid(t *testing.T) {
{TypeTask, true}, {TypeTask, true},
{TypeEpic, true}, {TypeEpic, true},
{TypeChore, true}, {TypeChore, true},
// Gas Town types require types.custom configuration // Gas Town types are now custom types (not built-in)
{TypeMessage, false}, {IssueType("message"), false},
{TypeMergeRequest, false}, {IssueType("merge-request"), false},
{TypeMolecule, false}, {IssueType("molecule"), false},
{TypeGate, false}, {IssueType("gate"), false},
{TypeAgent, false}, {IssueType("agent"), false},
{TypeRole, false}, {IssueType("role"), false},
{TypeConvoy, false}, {IssueType("convoy"), false},
{TypeEvent, false}, {IssueType("event"), false},
{TypeSlot, false}, {IssueType("slot"), false},
{TypeRig, false}, {IssueType("rig"), false},
// Invalid types // Invalid types
{IssueType("invalid"), false}, {IssueType("invalid"), false},
{IssueType(""), false}, {IssueType(""), false},
@@ -581,12 +581,12 @@ func TestIssueTypeRequiredSections(t *testing.T) {
{TypeTask, 1, "## Acceptance Criteria"}, {TypeTask, 1, "## Acceptance Criteria"},
{TypeEpic, 1, "## Success Criteria"}, {TypeEpic, 1, "## Success Criteria"},
{TypeChore, 0, ""}, {TypeChore, 0, ""},
{TypeMessage, 0, ""}, // Gas Town types are now custom and have no required sections
{TypeMolecule, 0, ""}, {IssueType("message"), 0, ""},
{TypeGate, 0, ""}, {IssueType("molecule"), 0, ""},
{TypeEvent, 0, ""}, {IssueType("gate"), 0, ""},
{TypeMergeRequest, 0, ""}, {IssueType("event"), 0, ""},
// Gas Town types (agent, role, rig, convoy, slot) have been removed {IssueType("merge-request"), 0, ""},
} }
for _, tt := range tests { for _, tt := range tests {
+2 -2
View File
@@ -123,13 +123,13 @@ Widget displays correctly`,
}, },
{ {
name: "message has no requirements", name: "message has no requirements",
issueType: types.TypeMessage, issueType: "message",
description: "Hello", description: "Hello",
wantErr: false, wantErr: false,
}, },
{ {
name: "molecule has no requirements", name: "molecule has no requirements",
issueType: types.TypeMolecule, issueType: "molecule",
description: "", description: "",
wantErr: false, wantErr: false,
}, },