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
+44 -16
View File
@@ -30,7 +30,7 @@ var agentCmd = &cobra.Command{
Short: "Manage agent bead state", Short: "Manage agent bead state",
Long: `Manage state on agent beads for ZFC-compliant state reporting. Long: `Manage state on agent beads for ZFC-compliant state reporting.
Agent beads (type=agent) can self-report their state using these commands. Agent beads (labeled gt:agent) can self-report their state using these commands.
This enables the Witness and other monitoring systems to track agent health. This enables the Witness and other monitoring systems to track agent health.
States: States:
@@ -207,7 +207,7 @@ func runAgentState(cmd *cobra.Command, args []string) error {
agent = &types.Issue{ agent = &types.Issue{
ID: agentID, ID: agentID,
Title: fmt.Sprintf("Agent: %s", agentID), Title: fmt.Sprintf("Agent: %s", agentID),
IssueType: types.TypeAgent, IssueType: types.TypeTask, // Use task type; gt:agent label marks it as agent
Status: types.StatusOpen, Status: types.StatusOpen,
RoleType: roleType, RoleType: roleType,
Rig: rig, Rig: rig,
@@ -218,10 +218,11 @@ func runAgentState(cmd *cobra.Command, args []string) error {
createArgs := &rpc.CreateArgs{ createArgs := &rpc.CreateArgs{
ID: agentID, ID: agentID,
Title: agent.Title, Title: agent.Title,
IssueType: string(types.TypeAgent), IssueType: string(types.TypeTask), // Use task type; gt:agent label marks it as agent
RoleType: roleType, RoleType: roleType,
Rig: rig, Rig: rig,
CreatedBy: actor, CreatedBy: actor,
Labels: []string{"gt:agent"}, // Gas Town agent label
} }
resp, err := daemonClient.Create(createArgs) resp, err := daemonClient.Create(createArgs)
if err != nil { if err != nil {
@@ -234,6 +235,10 @@ func runAgentState(cmd *cobra.Command, args []string) error {
if err := activeStore.CreateIssue(ctx, agent, actor); err != nil { if err := activeStore.CreateIssue(ctx, agent, actor); err != nil {
return fmt.Errorf("failed to auto-create agent bead %s: %w", agentID, err) return fmt.Errorf("failed to auto-create agent bead %s: %w", agentID, err)
} }
// Add gt:agent label to mark as agent bead
if err := activeStore.AddLabel(ctx, agent.ID, "gt:agent", actor); err != nil {
fmt.Fprintf(os.Stderr, "warning: failed to add gt:agent label: %v\n", err)
}
// Add role_type and rig labels for filtering // Add role_type and rig labels for filtering
if roleType != "" { if roleType != "" {
if err := activeStore.AddLabel(ctx, agent.ID, "role_type:"+roleType, actor); err != nil { if err := activeStore.AddLabel(ctx, agent.ID, "role_type:"+roleType, actor); err != nil {
@@ -248,9 +253,12 @@ func runAgentState(cmd *cobra.Command, args []string) error {
} }
} else { } else {
// Get existing agent bead to verify it's an agent // Get existing agent bead to verify it's an agent
var labels []string
if routedResult != nil && routedResult.Issue != nil { if routedResult != nil && routedResult.Issue != nil {
// Already have the issue from routed resolution // Already have the issue from routed resolution
agent = routedResult.Issue agent = routedResult.Issue
// Get labels from routed store
labels, _ = routedResult.Store.GetLabels(ctx, agentID)
} else if daemonClient != nil && !needsRouting(agentArg) { } else if daemonClient != nil && !needsRouting(agentArg) {
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID}) resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID})
if err != nil { if err != nil {
@@ -259,17 +267,19 @@ func runAgentState(cmd *cobra.Command, args []string) error {
if err := json.Unmarshal(resp.Data, &agent); err != nil { if err := json.Unmarshal(resp.Data, &agent); err != nil {
return fmt.Errorf("parsing response: %w", err) return fmt.Errorf("parsing response: %w", err)
} }
labels = agent.Labels
} else { } else {
var err error var err error
agent, err = activeStore.GetIssue(ctx, agentID) agent, err = activeStore.GetIssue(ctx, agentID)
if err != nil || agent == nil { if err != nil || agent == nil {
return fmt.Errorf("agent bead not found: %s", agentID) return fmt.Errorf("agent bead not found: %s", agentID)
} }
labels, _ = activeStore.GetLabels(ctx, agentID)
} }
// Verify agent bead is actually an agent // Verify agent bead is actually an agent (check for gt:agent label)
if agent.IssueType != "agent" { if !isAgentBead(labels) {
return fmt.Errorf("%s is not an agent bead (type=%s)", agentID, agent.IssueType) return fmt.Errorf("%s is not an agent bead (missing gt:agent label)", agentID)
} }
} }
@@ -362,9 +372,11 @@ func runAgentHeartbeat(cmd *cobra.Command, args []string) error {
// Get agent bead to verify it's an agent // Get agent bead to verify it's an agent
var agent *types.Issue var agent *types.Issue
var labels []string
if routedResult != nil && routedResult.Issue != nil { if routedResult != nil && routedResult.Issue != nil {
// Already have the issue from routed resolution // Already have the issue from routed resolution
agent = routedResult.Issue agent = routedResult.Issue
labels, _ = routedResult.Store.GetLabels(ctx, agentID)
} else if daemonClient != nil && !needsRouting(agentArg) { } else if daemonClient != nil && !needsRouting(agentArg) {
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID}) resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID})
if err != nil { if err != nil {
@@ -373,17 +385,19 @@ func runAgentHeartbeat(cmd *cobra.Command, args []string) error {
if err := json.Unmarshal(resp.Data, &agent); err != nil { if err := json.Unmarshal(resp.Data, &agent); err != nil {
return fmt.Errorf("parsing response: %w", err) return fmt.Errorf("parsing response: %w", err)
} }
labels = agent.Labels
} else { } else {
var err error var err error
agent, err = activeStore.GetIssue(ctx, agentID) agent, err = activeStore.GetIssue(ctx, agentID)
if err != nil || agent == nil { if err != nil || agent == nil {
return fmt.Errorf("agent bead not found: %s", agentID) return fmt.Errorf("agent bead not found: %s", agentID)
} }
labels, _ = activeStore.GetLabels(ctx, agentID)
} }
// Verify agent bead is actually an agent // Verify agent bead is actually an agent (check for gt:agent label)
if agent.IssueType != "agent" { if !isAgentBead(labels) {
return fmt.Errorf("%s is not an agent bead (type=%s)", agentID, agent.IssueType) return fmt.Errorf("%s is not an agent bead (missing gt:agent label)", agentID)
} }
// Update only last_activity // Update only last_activity
@@ -464,9 +478,11 @@ func runAgentShow(cmd *cobra.Command, args []string) error {
// Get agent bead // Get agent bead
var agent *types.Issue var agent *types.Issue
var labels []string
if routedResult != nil && routedResult.Issue != nil { if routedResult != nil && routedResult.Issue != nil {
// Already have the issue from routed resolution // Already have the issue from routed resolution
agent = routedResult.Issue agent = routedResult.Issue
labels, _ = routedResult.Store.GetLabels(ctx, agentID)
} else if daemonClient != nil && !needsRouting(agentArg) { } else if daemonClient != nil && !needsRouting(agentArg) {
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID}) resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID})
if err != nil { if err != nil {
@@ -475,17 +491,19 @@ func runAgentShow(cmd *cobra.Command, args []string) error {
if err := json.Unmarshal(resp.Data, &agent); err != nil { if err := json.Unmarshal(resp.Data, &agent); err != nil {
return fmt.Errorf("parsing response: %w", err) return fmt.Errorf("parsing response: %w", err)
} }
labels = agent.Labels
} else { } else {
var err error var err error
agent, err = store.GetIssue(ctx, agentID) agent, err = store.GetIssue(ctx, agentID)
if err != nil || agent == nil { if err != nil || agent == nil {
return fmt.Errorf("agent bead not found: %s", agentID) return fmt.Errorf("agent bead not found: %s", agentID)
} }
labels, _ = store.GetLabels(ctx, agentID)
} }
// Verify agent bead is actually an agent // Verify agent bead is actually an agent (check for gt:agent label)
if agent.IssueType != "agent" { if !isAgentBead(labels) {
return fmt.Errorf("%s is not an agent bead (type=%s)", agentID, agent.IssueType) return fmt.Errorf("%s is not an agent bead (missing gt:agent label)", agentID)
} }
if jsonOutput { if jsonOutput {
@@ -565,11 +583,11 @@ func runAgentBackfillLabels(cmd *cobra.Command, args []string) error {
ctx := rootCtx ctx := rootCtx
// List all agent beads // List all agent beads (by gt:agent label)
var agents []*types.Issue var agents []*types.Issue
if daemonClient != nil { if daemonClient != nil {
resp, err := daemonClient.List(&rpc.ListArgs{ resp, err := daemonClient.List(&rpc.ListArgs{
IssueType: "agent", Labels: []string{"gt:agent"},
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to list agents: %w", err) return fmt.Errorf("failed to list agents: %w", err)
@@ -578,9 +596,8 @@ func runAgentBackfillLabels(cmd *cobra.Command, args []string) error {
return fmt.Errorf("parsing response: %w", err) return fmt.Errorf("parsing response: %w", err)
} }
} else { } else {
agentType := types.TypeAgent
filter := types.IssueFilter{ filter := types.IssueFilter{
IssueType: &agentType, Labels: []string{"gt:agent"},
} }
var err error var err error
agents, err = store.SearchIssues(ctx, "", filter) agents, err = store.SearchIssues(ctx, "", filter)
@@ -756,6 +773,17 @@ func containsLabel(labels []string, label string) bool {
return false return false
} }
// isAgentBead checks if an issue is an agent bead by looking for the gt:agent label.
// This replaces the previous type-based check (issue_type='agent') for Gas Town separation.
func isAgentBead(labels []string) bool {
for _, l := range labels {
if l == "gt:agent" {
return true
}
}
return false
}
// parseAgentIDFields extracts role_type and rig from an agent bead ID. // parseAgentIDFields extracts role_type and rig from an agent bead ID.
// Agent ID patterns: // Agent ID patterns:
// - Town-level: <prefix>-<role> (e.g., gt-mayor) → role="mayor", rig="" // - Town-level: <prefix>-<role> (e.g., gt-mayor) → role="mayor", rig=""
+19 -10
View File
@@ -45,11 +45,11 @@ func TestAgentStateWithRouting(t *testing.T) {
rigDBPath := filepath.Join(rigBeadsDir, "beads.db") rigDBPath := filepath.Join(rigBeadsDir, "beads.db")
rigStore := newTestStoreWithPrefix(t, rigDBPath, "gt") rigStore := newTestStoreWithPrefix(t, rigDBPath, "gt")
// Create an agent bead in the rig database // Create an agent bead in the rig database (using task type with gt:agent label)
agentBead := &types.Issue{ agentBead := &types.Issue{
ID: "gt-testrig-polecat-test", ID: "gt-testrig-polecat-test",
Title: "Agent: gt-testrig-polecat-test", Title: "Agent: gt-testrig-polecat-test",
IssueType: types.TypeAgent, IssueType: types.TypeTask, // Use task type; gt:agent label marks it as agent
Status: types.StatusOpen, Status: types.StatusOpen,
RoleType: "polecat", RoleType: "polecat",
Rig: "testrig", Rig: "testrig",
@@ -57,6 +57,9 @@ func TestAgentStateWithRouting(t *testing.T) {
if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil { if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil {
t.Fatalf("Failed to create agent bead: %v", err) t.Fatalf("Failed to create agent bead: %v", err)
} }
if err := rigStore.AddLabel(ctx, agentBead.ID, "gt:agent", "test"); err != nil {
t.Fatalf("Failed to add gt:agent label: %v", err)
}
// Create routes.jsonl in town .beads directory // Create routes.jsonl in town .beads directory
routesContent := `{"prefix":"gt-","path":"rig"}` routesContent := `{"prefix":"gt-","path":"rig"}`
@@ -92,8 +95,8 @@ func TestAgentStateWithRouting(t *testing.T) {
t.Error("Expected result.Routed to be true for cross-repo lookup") t.Error("Expected result.Routed to be true for cross-repo lookup")
} }
if result.Issue.IssueType != types.TypeAgent { if result.Issue.IssueType != types.TypeTask {
t.Errorf("Expected issue type %q, got %q", types.TypeAgent, result.Issue.IssueType) t.Errorf("Expected issue type %q, got %q", types.TypeTask, result.Issue.IssueType)
} }
t.Logf("Successfully resolved agent %s via routing", result.Issue.ID) t.Logf("Successfully resolved agent %s via routing", result.Issue.ID)
@@ -136,11 +139,11 @@ func TestAgentHeartbeatWithRouting(t *testing.T) {
rigDBPath := filepath.Join(rigBeadsDir, "beads.db") rigDBPath := filepath.Join(rigBeadsDir, "beads.db")
rigStore := newTestStoreWithPrefix(t, rigDBPath, "gt") rigStore := newTestStoreWithPrefix(t, rigDBPath, "gt")
// Create an agent bead in the rig database // Create an agent bead in the rig database (using task type with gt:agent label)
agentBead := &types.Issue{ agentBead := &types.Issue{
ID: "gt-test-witness", ID: "gt-test-witness",
Title: "Agent: gt-test-witness", Title: "Agent: gt-test-witness",
IssueType: types.TypeAgent, IssueType: types.TypeTask, // Use task type; gt:agent label marks it as agent
Status: types.StatusOpen, Status: types.StatusOpen,
RoleType: "witness", RoleType: "witness",
Rig: "test", Rig: "test",
@@ -148,6 +151,9 @@ func TestAgentHeartbeatWithRouting(t *testing.T) {
if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil { if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil {
t.Fatalf("Failed to create agent bead: %v", err) t.Fatalf("Failed to create agent bead: %v", err)
} }
if err := rigStore.AddLabel(ctx, agentBead.ID, "gt:agent", "test"); err != nil {
t.Fatalf("Failed to add gt:agent label: %v", err)
}
// Create routes.jsonl // Create routes.jsonl
routesContent := `{"prefix":"gt-","path":"rig"}` routesContent := `{"prefix":"gt-","path":"rig"}`
@@ -207,11 +213,11 @@ func TestAgentShowWithRouting(t *testing.T) {
rigDBPath := filepath.Join(rigBeadsDir, "beads.db") rigDBPath := filepath.Join(rigBeadsDir, "beads.db")
rigStore := newTestStoreWithPrefix(t, rigDBPath, "gt") rigStore := newTestStoreWithPrefix(t, rigDBPath, "gt")
// Create an agent bead in the rig database // Create an agent bead in the rig database (using task type with gt:agent label)
agentBead := &types.Issue{ agentBead := &types.Issue{
ID: "gt-myrig-crew-alice", ID: "gt-myrig-crew-alice",
Title: "Agent: gt-myrig-crew-alice", Title: "Agent: gt-myrig-crew-alice",
IssueType: types.TypeAgent, IssueType: types.TypeTask, // Use task type; gt:agent label marks it as agent
Status: types.StatusOpen, Status: types.StatusOpen,
RoleType: "crew", RoleType: "crew",
Rig: "myrig", Rig: "myrig",
@@ -219,6 +225,9 @@ func TestAgentShowWithRouting(t *testing.T) {
if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil { if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil {
t.Fatalf("Failed to create agent bead: %v", err) t.Fatalf("Failed to create agent bead: %v", err)
} }
if err := rigStore.AddLabel(ctx, agentBead.ID, "gt:agent", "test"); err != nil {
t.Fatalf("Failed to add gt:agent label: %v", err)
}
// Create routes.jsonl // Create routes.jsonl
routesContent := `{"prefix":"gt-","path":"rig"}` routesContent := `{"prefix":"gt-","path":"rig"}`
@@ -246,8 +255,8 @@ func TestAgentShowWithRouting(t *testing.T) {
t.Errorf("Expected issue ID %q, got %q", "gt-myrig-crew-alice", result.Issue.ID) t.Errorf("Expected issue ID %q, got %q", "gt-myrig-crew-alice", result.Issue.ID)
} }
if result.Issue.IssueType != types.TypeAgent { if result.Issue.IssueType != types.TypeTask {
t.Errorf("Expected issue type %q, got %q", types.TypeAgent, result.Issue.IssueType) t.Errorf("Expected issue type %q, got %q", types.TypeTask, result.Issue.IssueType)
} }
t.Logf("Successfully resolved agent %s via routing for show test", result.Issue.ID) t.Logf("Successfully resolved agent %s via routing for show test", result.Issue.ID)
+9 -1
View File
@@ -474,7 +474,15 @@ var createCmd = &cobra.Command{
} }
// Auto-add role_type/rig labels for agent beads (enables filtering queries) // 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)
hasAgentLabel := false
for _, l := range labels {
if l == "gt:agent" {
hasAgentLabel = true
break
}
}
if hasAgentLabel {
if issue.RoleType != "" { if issue.RoleType != "" {
agentLabel := "role_type:" + issue.RoleType agentLabel := "role_type:" + issue.RoleType
if err := store.AddLabel(ctx, issue.ID, agentLabel, actor); err != nil { if err := store.AddLabel(ctx, issue.ID, agentLabel, actor); err != nil {
+2 -4
View File
@@ -33,6 +33,8 @@ var obsidianPriority = []string{
} }
// obsidianTypeTag maps bd issue type to Obsidian tag // 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.
var obsidianTypeTag = map[types.IssueType]string{ var obsidianTypeTag = map[types.IssueType]string{
types.TypeBug: "#Bug", types.TypeBug: "#Bug",
types.TypeFeature: "#Feature", types.TypeFeature: "#Feature",
@@ -43,10 +45,6 @@ var obsidianTypeTag = map[types.IssueType]string{
types.TypeMergeRequest: "#MergeRequest", types.TypeMergeRequest: "#MergeRequest",
types.TypeMolecule: "#Molecule", types.TypeMolecule: "#Molecule",
types.TypeGate: "#Gate", types.TypeGate: "#Gate",
types.TypeAgent: "#Agent",
types.TypeRole: "#Role",
types.TypeRig: "#Rig",
types.TypeConvoy: "#Convoy",
types.TypeEvent: "#Event", types.TypeEvent: "#Event",
} }
+10 -5
View File
@@ -25,7 +25,7 @@ A merge slot is an exclusive access primitive: only one agent can hold it at a t
This prevents "monkey knife fights" where multiple polecats race to resolve conflicts This prevents "monkey knife fights" where multiple polecats race to resolve conflicts
and create cascading conflicts. and create cascading conflicts.
Each rig has one merge slot bead: <prefix>-merge-slot (type=slot). Each rig has one merge slot bead: <prefix>-merge-slot (labeled gt:slot).
The slot uses: The slot uses:
- status=open: slot is available - status=open: slot is available
- status=in_progress: slot is held - status=in_progress: slot is held
@@ -157,15 +157,15 @@ func runMergeSlotCreate(cmd *cobra.Command, args []string) error {
// Create the merge slot bead // Create the merge slot bead
title := "Merge Slot" title := "Merge Slot"
description := "Exclusive access slot for serialized conflict resolution in the merge queue." description := "Exclusive access slot for serialized conflict resolution in the merge queue."
slotType := types.TypeSlot
if daemonClient != nil { if daemonClient != nil {
createArgs := &rpc.CreateArgs{ createArgs := &rpc.CreateArgs{
ID: slotID, ID: slotID,
Title: title, Title: title,
Description: description, Description: description,
IssueType: string(slotType), IssueType: string(types.TypeTask), // Use task type; gt:slot label marks it as slot
Priority: 0, // P0 - system infrastructure Priority: 0, // P0 - system infrastructure
Labels: []string{"gt:slot"}, // Gas Town slot label
} }
resp, err := daemonClient.Create(createArgs) resp, err := daemonClient.Create(createArgs)
if err != nil { if err != nil {
@@ -179,13 +179,18 @@ func runMergeSlotCreate(cmd *cobra.Command, args []string) error {
ID: slotID, ID: slotID,
Title: title, Title: title,
Description: description, Description: description,
IssueType: slotType, IssueType: types.TypeTask, // Use task type; gt:slot label marks it as slot
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 0, Priority: 0,
} }
if err := store.CreateIssue(ctx, issue, actor); err != nil { if err := store.CreateIssue(ctx, issue, actor); err != nil {
return fmt.Errorf("failed to create merge slot: %w", err) return fmt.Errorf("failed to create merge slot: %w", err)
} }
// Add gt:slot label to mark as slot bead
if err := store.AddLabel(ctx, slotID, "gt:slot", actor); err != nil {
// Non-fatal: log warning but don't fail creation
fmt.Fprintf(os.Stderr, "warning: failed to add gt:slot label: %v\n", err)
}
markDirtyAndScheduleFlush() markDirtyAndScheduleFlush()
} }
+18 -7
View File
@@ -14,6 +14,16 @@ import (
"github.com/steveyegge/beads/internal/utils" "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.) // parseTimeRPC parses time strings in multiple formats (RFC3339, YYYY-MM-DD, etc.)
// Matches the parseTimeFlag behavior in cmd/bd/list.go for CLI parity // Matches the parseTimeFlag behavior in cmd/bd/list.go for CLI parity
func parseTimeRPC(s string) (time.Time, error) { 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) // 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 != "" { if issue.RoleType != "" {
label := "role_type:" + issue.RoleType label := "role_type:" + issue.RoleType
if err := store.AddLabel(ctx, issue.ID, label, s.reqActor(req)); err != nil { 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 // 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 // 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 != "" { if updateArgs.RoleType != nil && *updateArgs.RoleType != "" {
// Remove any existing role_type:* labels first // Remove any existing role_type:* labels first
existingLabels, _ := store.GetLabels(ctx, updateArgs.ID) for _, l := range issueLabels {
for _, l := range existingLabels {
if strings.HasPrefix(l, "role_type:") { if strings.HasPrefix(l, "role_type:") {
_ = store.RemoveLabel(ctx, updateArgs.ID, l, actor) _ = store.RemoveLabel(ctx, updateArgs.ID, l, actor)
} }
@@ -612,8 +624,7 @@ func (s *Server) handleUpdate(req *Request) Response {
} }
if updateArgs.Rig != nil && *updateArgs.Rig != "" { if updateArgs.Rig != nil && *updateArgs.Rig != "" {
// Remove any existing rig:* labels first // Remove any existing rig:* labels first
existingLabels, _ := store.GetLabels(ctx, updateArgs.ID) for _, l := range issueLabels {
for _, l := range existingLabels {
if strings.HasPrefix(l, "rig:") { if strings.HasPrefix(l, "rig:") {
_ = store.RemoveLabel(ctx, updateArgs.ID, l, actor) _ = store.RemoveLabel(ctx, updateArgs.ID, l, actor)
} }
+3 -2
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 // 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) // 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, ` convoyRows, err := tx.QueryContext(ctx, `
SELECT DISTINCT d.issue_id SELECT DISTINCT d.issue_id
FROM dependencies d FROM dependencies d
JOIN issues i ON d.issue_id = i.id 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 = ? WHERE d.depends_on_id = ?
AND d.type = ? AND d.type = ?
AND i.issue_type = ?
AND i.status != ? AND i.status != ?
`, id, types.DepTracks, types.TypeConvoy, types.StatusClosed) `, id, types.DepTracks, types.StatusClosed)
if err != nil { if err != nil {
return fmt.Errorf("failed to find tracking convoys: %w", err) return fmt.Errorf("failed to find tracking convoys: %w", err)
} }
+5 -2
View File
@@ -1475,17 +1475,20 @@ func TestConvoyReactiveCompletion(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// Create a convoy // Create a convoy (using task type with gt:convoy label)
convoy := &types.Issue{ convoy := &types.Issue{
Title: "Test Convoy", Title: "Test Convoy",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, 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") err := store.CreateIssue(ctx, convoy, "test-user")
if err != nil { if err != nil {
t.Fatalf("CreateIssue convoy failed: %v", err) 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 // Create two issues to track
issue1 := &types.Issue{ issue1 := &types.Issue{
+3 -2
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 // 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) // 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, ` convoyRows, err := t.conn.QueryContext(ctx, `
SELECT DISTINCT d.issue_id SELECT DISTINCT d.issue_id
FROM dependencies d FROM dependencies d
JOIN issues i ON d.issue_id = i.id 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 = ? WHERE d.depends_on_id = ?
AND d.type = ? AND d.type = ?
AND i.issue_type = ?
AND i.status != ? AND i.status != ?
`, id, types.DepTracks, types.TypeConvoy, types.StatusClosed) `, id, types.DepTracks, types.StatusClosed)
if err != nil { if err != nil {
return fmt.Errorf("failed to find tracking convoys: %w", err) return fmt.Errorf("failed to find tracking convoys: %w", err)
} }
+5 -7
View File
@@ -412,6 +412,9 @@ func (s Status) IsValidWithCustom(customStatuses []string) bool {
type IssueType string type IssueType string
// Issue type constants // 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 ( const (
TypeBug IssueType = "bug" TypeBug IssueType = "bug"
TypeFeature IssueType = "feature" TypeFeature IssueType = "feature"
@@ -422,18 +425,13 @@ const (
TypeMergeRequest IssueType = "merge-request" // Merge queue entry for refinery processing TypeMergeRequest IssueType = "merge-request" // Merge queue entry for refinery processing
TypeMolecule IssueType = "molecule" // Template molecule for issue hierarchies TypeMolecule IssueType = "molecule" // Template molecule for issue hierarchies
TypeGate IssueType = "gate" // Async coordination gate 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 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) // IsValid checks if the issue type value is valid (built-in types only)
func (t IssueType) IsValid() bool { func (t IssueType) IsValid() bool {
switch t { 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 true
} }
return false return false
@@ -480,7 +478,7 @@ func (t IssueType) RequiredSections() []RequiredSection {
{Heading: "## Success Criteria", Hint: "Define high-level success criteria"}, {Heading: "## Success Criteria", Hint: "Define high-level success criteria"},
} }
default: default:
// Chore, message, molecule, gate, agent, role, convoy, event, merge-request // Chore, message, molecule, gate, event, merge-request
// have no required sections // have no required sections
return nil return nil
} }
+9 -9
View File
@@ -401,12 +401,15 @@ func TestIssueTypeIsValid(t *testing.T) {
{TypeTask, true}, {TypeTask, true},
{TypeEpic, true}, {TypeEpic, true},
{TypeChore, true}, {TypeChore, true},
{TypeAgent, true}, {TypeMessage, true},
{TypeRole, true}, {TypeMergeRequest, true},
{TypeRig, true}, {TypeMolecule, true},
{TypeConvoy, true}, {TypeGate, true},
{TypeEvent, 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("invalid"), false},
{IssueType(""), false}, {IssueType(""), false},
} }
@@ -434,12 +437,9 @@ func TestIssueTypeRequiredSections(t *testing.T) {
{TypeMessage, 0, ""}, {TypeMessage, 0, ""},
{TypeMolecule, 0, ""}, {TypeMolecule, 0, ""},
{TypeGate, 0, ""}, {TypeGate, 0, ""},
{TypeAgent, 0, ""},
{TypeRole, 0, ""},
{TypeRig, 0, ""},
{TypeConvoy, 0, ""},
{TypeEvent, 0, ""}, {TypeEvent, 0, ""},
{TypeMergeRequest, 0, ""}, {TypeMergeRequest, 0, ""},
// Gas Town types (agent, role, rig, convoy, slot) have been removed
} }
for _, tt := range tests { for _, tt := range tests {
+5 -22
View File
@@ -135,18 +135,8 @@ var (
Light: "", // standard text color Light: "", // standard text color
Dark: "", Dark: "",
} }
ColorTypeAgent = lipgloss.AdaptiveColor{ // Note: Gas Town-specific types (agent, role, rig) have been removed.
Light: "#59c2ff", // cyan - agent identity // Use labels (gt:agent, gt:role, gt:rig) with custom styling if needed.
Dark: "#59c2ff",
}
ColorTypeRole = lipgloss.AdaptiveColor{
Light: "#7fd962", // green - role definition
Dark: "#7fd962",
}
ColorTypeRig = lipgloss.AdaptiveColor{
Light: "#e6a756", // orange - rig identity (project container)
Dark: "#e6a756",
}
// === Issue ID Color === // === Issue ID Color ===
// IDs use standard text color - subtle, not attention-grabbing // IDs use standard text color - subtle, not attention-grabbing
@@ -194,9 +184,7 @@ var (
TypeTaskStyle = lipgloss.NewStyle().Foreground(ColorTypeTask) TypeTaskStyle = lipgloss.NewStyle().Foreground(ColorTypeTask)
TypeEpicStyle = lipgloss.NewStyle().Foreground(ColorTypeEpic) TypeEpicStyle = lipgloss.NewStyle().Foreground(ColorTypeEpic)
TypeChoreStyle = lipgloss.NewStyle().Foreground(ColorTypeChore) TypeChoreStyle = lipgloss.NewStyle().Foreground(ColorTypeChore)
TypeAgentStyle = lipgloss.NewStyle().Foreground(ColorTypeAgent) // Note: Gas Town-specific type styles (agent, role, rig) have been removed.
TypeRoleStyle = lipgloss.NewStyle().Foreground(ColorTypeRole)
TypeRigStyle = lipgloss.NewStyle().Foreground(ColorTypeRig)
) )
// CategoryStyle for section headers - bold with accent color // 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 // 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 { func RenderType(issueType string) string {
switch issueType { switch issueType {
case "bug": case "bug":
@@ -344,12 +333,6 @@ func RenderType(issueType string) string {
return TypeEpicStyle.Render(issueType) return TypeEpicStyle.Render(issueType)
case "chore": case "chore":
return TypeChoreStyle.Render(issueType) return TypeChoreStyle.Render(issueType)
case "agent":
return TypeAgentStyle.Render(issueType)
case "role":
return TypeRoleStyle.Render(issueType)
case "rig":
return TypeRigStyle.Render(issueType)
default: default:
return issueType return issueType
} }
+4 -3
View File
@@ -92,9 +92,10 @@ func TestRenderTypeVariants(t *testing.T) {
{"task", TypeTaskStyle.Render("task")}, {"task", TypeTaskStyle.Render("task")},
{"epic", TypeEpicStyle.Render("epic")}, {"epic", TypeEpicStyle.Render("epic")},
{"chore", TypeChoreStyle.Render("chore")}, {"chore", TypeChoreStyle.Render("chore")},
{"agent", TypeAgentStyle.Render("agent")}, // Gas Town types (agent, role, rig) have been removed - they now fall through to default
{"role", TypeRoleStyle.Render("role")}, {"agent", "agent"}, // Falls through to default (no styling)
{"rig", TypeRigStyle.Render("rig")}, {"role", "role"}, // Falls through to default (no styling)
{"rig", "rig"}, // Falls through to default (no styling)
{"custom", "custom"}, {"custom", "custom"},
} }
for _, tc := range cases { for _, tc := range cases {
+3 -3
View File
@@ -126,10 +126,10 @@ func TestParseIssueType(t *testing.T) {
{"merge-request type", "merge-request", types.TypeMergeRequest, false, ""}, {"merge-request type", "merge-request", types.TypeMergeRequest, false, ""},
{"molecule type", "molecule", types.TypeMolecule, false, ""}, {"molecule type", "molecule", types.TypeMolecule, false, ""},
{"gate type", "gate", types.TypeGate, false, ""}, {"gate type", "gate", types.TypeGate, false, ""},
{"agent type", "agent", types.TypeAgent, false, ""}, {"event type", "event", types.TypeEvent, false, ""},
{"role type", "role", types.TypeRole, false, ""},
{"rig type", "rig", types.TypeRig, false, ""},
{"message type", "message", types.TypeMessage, 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) // Case sensitivity (function is case-sensitive)
{"uppercase bug", "BUG", types.TypeTask, true, "invalid issue type"}, {"uppercase bug", "BUG", types.TypeTask, true, "invalid issue type"},