feat(types): remove Gas Town types from core built-in types

Core beads built-in types now only include work types:
- bug, feature, task, epic, chore

Gas Town types (molecule, gate, convoy, merge-request, slot, agent,
role, rig, event, message) are now "well-known custom types":
- Constants still exist for code convenience
- Require types.custom configuration for validation
- bd types command shows core types and configured custom types

Changes:
- types.go: Separate core work types from well-known custom types
- IsValid(): Only accepts core work types
- bd types: Updated to show core types and custom types from config
- memory.go: Use ValidateWithCustom for custom type support
- multirepo.go: Only check core types as built-in
- Updated all tests to configure custom types

This allows Gas Town (and other projects) to define their own types
via config while keeping beads core focused on work tracking.

Closes: bd-find4

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/emma
2026-01-17 05:07:11 -08:00
committed by Steve Yegge
parent 88a6438c80
commit 4f0f5744a6
14 changed files with 189 additions and 67 deletions

View File

@@ -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",

View File

@@ -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())

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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},
}

View File

@@ -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"},