feat: extract Gas Town types from beads core (bd-i54l)

Remove Gas Town-specific issue types (agent, role, rig, convoy, slot)
from beads core. These types are now identified by labels instead:
- gt:agent, gt:role, gt:rig, gt:convoy, gt:slot

Changes:
- internal/types/types.go: Remove TypeAgent, TypeRole, TypeRig, TypeConvoy, TypeSlot constants
- cmd/bd/agent.go: Create agents with TypeTask + gt:agent label
- cmd/bd/merge_slot.go: Create slots with TypeTask + gt:slot label
- internal/storage/sqlite/queries.go, transaction.go: Query convoys by gt:convoy label
- internal/rpc/server_issues_epics.go: Check gt:agent label for role_type/rig label auto-add
- cmd/bd/create.go: Check gt:agent label for role_type/rig label auto-add
- internal/ui/styles.go: Remove agent/role/rig type colors
- cmd/bd/export_obsidian.go: Remove agent/role/rig/convoy type tag mappings
- Update all affected tests

This enables beads to be a generic issue tracker while Gas Town
uses labels for its specific type semantics.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
This commit is contained in:
dave
2026-01-06 22:18:37 -08:00
committed by Steve Yegge
parent b7358f17bf
commit a70c3a8cbe
14 changed files with 139 additions and 93 deletions

View File

@@ -14,6 +14,16 @@ import (
"github.com/steveyegge/beads/internal/utils"
)
// containsLabel checks if a label exists in the list
func containsLabel(labels []string, label string) bool {
for _, l := range labels {
if l == label {
return true
}
}
return false
}
// parseTimeRPC parses time strings in multiple formats (RFC3339, YYYY-MM-DD, etc.)
// Matches the parseTimeFlag behavior in cmd/bd/list.go for CLI parity
func parseTimeRPC(s string) (time.Time, error) {
@@ -346,7 +356,8 @@ func (s *Server) handleCreate(req *Request) Response {
}
// Auto-add role_type/rig labels for agent beads (enables filtering queries)
if issue.IssueType == types.TypeAgent {
// Check for gt:agent label to identify agent beads (Gas Town separation)
if containsLabel(createArgs.Labels, "gt:agent") {
if issue.RoleType != "" {
label := "role_type:" + issue.RoleType
if err := store.AddLabel(ctx, issue.ID, label, s.reqActor(req)); err != nil {
@@ -590,13 +601,14 @@ func (s *Server) handleUpdate(req *Request) Response {
}
// Auto-add role_type/rig labels for agent beads when these fields are set
// This enables filtering queries like: bd list --type=agent --label=role_type:witness
// This enables filtering queries like: bd list --label=gt:agent --label=role_type:witness
// Note: We remove old role_type/rig labels first to prevent accumulation
if issue.IssueType == types.TypeAgent {
// Check for gt:agent label to identify agent beads (Gas Town separation)
issueLabels, _ := store.GetLabels(ctx, updateArgs.ID)
if containsLabel(issueLabels, "gt:agent") {
if updateArgs.RoleType != nil && *updateArgs.RoleType != "" {
// Remove any existing role_type:* labels first
existingLabels, _ := store.GetLabels(ctx, updateArgs.ID)
for _, l := range existingLabels {
for _, l := range issueLabels {
if strings.HasPrefix(l, "role_type:") {
_ = store.RemoveLabel(ctx, updateArgs.ID, l, actor)
}
@@ -612,8 +624,7 @@ func (s *Server) handleUpdate(req *Request) Response {
}
if updateArgs.Rig != nil && *updateArgs.Rig != "" {
// Remove any existing rig:* labels first
existingLabels, _ := store.GetLabels(ctx, updateArgs.ID)
for _, l := range existingLabels {
for _, l := range issueLabels {
if strings.HasPrefix(l, "rig:") {
_ = store.RemoveLabel(ctx, updateArgs.ID, l, actor)
}

View File

@@ -1151,15 +1151,16 @@ func (s *SQLiteStorage) CloseIssue(ctx context.Context, id string, reason string
// Reactive convoy completion: check if any convoys tracking this issue should auto-close
// Find convoys that track this issue (convoy.issue_id tracks closed_issue.depends_on_id)
// Uses gt:convoy label instead of issue_type for Gas Town separation
convoyRows, err := tx.QueryContext(ctx, `
SELECT DISTINCT d.issue_id
FROM dependencies d
JOIN issues i ON d.issue_id = i.id
JOIN labels l ON i.id = l.issue_id AND l.label = 'gt:convoy'
WHERE d.depends_on_id = ?
AND d.type = ?
AND i.issue_type = ?
AND i.status != ?
`, id, types.DepTracks, types.TypeConvoy, types.StatusClosed)
`, id, types.DepTracks, types.StatusClosed)
if err != nil {
return fmt.Errorf("failed to find tracking convoys: %w", err)
}

View File

@@ -1475,17 +1475,20 @@ func TestConvoyReactiveCompletion(t *testing.T) {
ctx := context.Background()
// Create a convoy
// Create a convoy (using task type with gt:convoy label)
convoy := &types.Issue{
Title: "Test Convoy",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeConvoy,
IssueType: types.TypeTask, // Use task type; gt:convoy label marks it as convoy
}
err := store.CreateIssue(ctx, convoy, "test-user")
if err != nil {
t.Fatalf("CreateIssue convoy failed: %v", err)
}
if err := store.AddLabel(ctx, convoy.ID, "gt:convoy", "test-user"); err != nil {
t.Fatalf("Failed to add gt:convoy label: %v", err)
}
// Create two issues to track
issue1 := &types.Issue{

View File

@@ -572,15 +572,16 @@ func (t *sqliteTxStorage) CloseIssue(ctx context.Context, id string, reason stri
// Reactive convoy completion: check if any convoys tracking this issue should auto-close
// Find convoys that track this issue (convoy.issue_id tracks closed_issue.depends_on_id)
// Uses gt:convoy label instead of issue_type for Gas Town separation
convoyRows, err := t.conn.QueryContext(ctx, `
SELECT DISTINCT d.issue_id
FROM dependencies d
JOIN issues i ON d.issue_id = i.id
JOIN labels l ON i.id = l.issue_id AND l.label = 'gt:convoy'
WHERE d.depends_on_id = ?
AND d.type = ?
AND i.issue_type = ?
AND i.status != ?
`, id, types.DepTracks, types.TypeConvoy, types.StatusClosed)
`, id, types.DepTracks, types.StatusClosed)
if err != nil {
return fmt.Errorf("failed to find tracking convoys: %w", err)
}

View File

@@ -412,6 +412,9 @@ func (s Status) IsValidWithCustom(customStatuses []string) bool {
type IssueType string
// Issue type constants
// Note: Gas Town-specific types (agent, role, rig, convoy, slot) have been removed.
// Use custom types via `bd config set types.custom "agent,role,..."` if needed.
// These types are now identified by labels (gt:agent, gt:role, etc.) instead.
const (
TypeBug IssueType = "bug"
TypeFeature IssueType = "feature"
@@ -422,18 +425,13 @@ const (
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 (project container)
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 value is valid (built-in types only)
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, TypeMessage, TypeMergeRequest, TypeMolecule, TypeGate, TypeEvent:
return true
}
return false
@@ -480,7 +478,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
// Chore, message, molecule, gate, event, merge-request
// have no required sections
return nil
}

View File

@@ -401,12 +401,15 @@ func TestIssueTypeIsValid(t *testing.T) {
{TypeTask, true},
{TypeEpic, true},
{TypeChore, true},
{TypeAgent, true},
{TypeRole, true},
{TypeRig, true},
{TypeConvoy, true},
{TypeMessage, true},
{TypeMergeRequest, true},
{TypeMolecule, true},
{TypeGate, true},
{TypeEvent, true},
{TypeSlot, true},
// Gas Town types (agent, role, rig, convoy, slot) have been removed
// They are now identified by labels (gt:agent, etc.) instead
{IssueType("agent"), false}, // Now requires custom type config
{IssueType("convoy"), false}, // Now requires custom type config
{IssueType("invalid"), false},
{IssueType(""), false},
}
@@ -434,12 +437,9 @@ func TestIssueTypeRequiredSections(t *testing.T) {
{TypeMessage, 0, ""},
{TypeMolecule, 0, ""},
{TypeGate, 0, ""},
{TypeAgent, 0, ""},
{TypeRole, 0, ""},
{TypeRig, 0, ""},
{TypeConvoy, 0, ""},
{TypeEvent, 0, ""},
{TypeMergeRequest, 0, ""},
// Gas Town types (agent, role, rig, convoy, slot) have been removed
}
for _, tt := range tests {

View File

@@ -135,18 +135,8 @@ var (
Light: "", // standard text color
Dark: "",
}
ColorTypeAgent = lipgloss.AdaptiveColor{
Light: "#59c2ff", // cyan - agent identity
Dark: "#59c2ff",
}
ColorTypeRole = lipgloss.AdaptiveColor{
Light: "#7fd962", // green - role definition
Dark: "#7fd962",
}
ColorTypeRig = lipgloss.AdaptiveColor{
Light: "#e6a756", // orange - rig identity (project container)
Dark: "#e6a756",
}
// Note: Gas Town-specific types (agent, role, rig) have been removed.
// Use labels (gt:agent, gt:role, gt:rig) with custom styling if needed.
// === Issue ID Color ===
// IDs use standard text color - subtle, not attention-grabbing
@@ -194,9 +184,7 @@ var (
TypeTaskStyle = lipgloss.NewStyle().Foreground(ColorTypeTask)
TypeEpicStyle = lipgloss.NewStyle().Foreground(ColorTypeEpic)
TypeChoreStyle = lipgloss.NewStyle().Foreground(ColorTypeChore)
TypeAgentStyle = lipgloss.NewStyle().Foreground(ColorTypeAgent)
TypeRoleStyle = lipgloss.NewStyle().Foreground(ColorTypeRole)
TypeRigStyle = lipgloss.NewStyle().Foreground(ColorTypeRig)
// Note: Gas Town-specific type styles (agent, role, rig) have been removed.
)
// CategoryStyle for section headers - bold with accent color
@@ -331,7 +319,8 @@ func RenderPriority(priority int) string {
}
// RenderType renders an issue type with semantic styling
// bugs get color; all other types use standard text
// bugs and epics get color; all other types use standard text
// Note: Gas Town-specific types (agent, role, rig) now fall through to default
func RenderType(issueType string) string {
switch issueType {
case "bug":
@@ -344,12 +333,6 @@ func RenderType(issueType string) string {
return TypeEpicStyle.Render(issueType)
case "chore":
return TypeChoreStyle.Render(issueType)
case "agent":
return TypeAgentStyle.Render(issueType)
case "role":
return TypeRoleStyle.Render(issueType)
case "rig":
return TypeRigStyle.Render(issueType)
default:
return issueType
}

View File

@@ -92,9 +92,10 @@ func TestRenderTypeVariants(t *testing.T) {
{"task", TypeTaskStyle.Render("task")},
{"epic", TypeEpicStyle.Render("epic")},
{"chore", TypeChoreStyle.Render("chore")},
{"agent", TypeAgentStyle.Render("agent")},
{"role", TypeRoleStyle.Render("role")},
{"rig", TypeRigStyle.Render("rig")},
// Gas Town types (agent, role, rig) have been removed - they now fall through to default
{"agent", "agent"}, // Falls through to default (no styling)
{"role", "role"}, // Falls through to default (no styling)
{"rig", "rig"}, // Falls through to default (no styling)
{"custom", "custom"},
}
for _, tc := range cases {

View File

@@ -126,10 +126,10 @@ func TestParseIssueType(t *testing.T) {
{"merge-request type", "merge-request", types.TypeMergeRequest, false, ""},
{"molecule type", "molecule", types.TypeMolecule, false, ""},
{"gate type", "gate", types.TypeGate, false, ""},
{"agent type", "agent", types.TypeAgent, false, ""},
{"role type", "role", types.TypeRole, false, ""},
{"rig type", "rig", types.TypeRig, 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,
// Case sensitivity (function is case-sensitive)
{"uppercase bug", "BUG", types.TypeTask, true, "invalid issue type"},