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:
@@ -30,7 +30,7 @@ var agentCmd = &cobra.Command{
|
||||
Short: "Manage agent bead state",
|
||||
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.
|
||||
|
||||
States:
|
||||
@@ -207,7 +207,7 @@ func runAgentState(cmd *cobra.Command, args []string) error {
|
||||
agent = &types.Issue{
|
||||
ID: 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,
|
||||
RoleType: roleType,
|
||||
Rig: rig,
|
||||
@@ -218,10 +218,11 @@ func runAgentState(cmd *cobra.Command, args []string) error {
|
||||
createArgs := &rpc.CreateArgs{
|
||||
ID: agentID,
|
||||
Title: agent.Title,
|
||||
IssueType: string(types.TypeAgent),
|
||||
IssueType: string(types.TypeTask), // Use task type; gt:agent label marks it as agent
|
||||
RoleType: roleType,
|
||||
Rig: rig,
|
||||
CreatedBy: actor,
|
||||
Labels: []string{"gt:agent"}, // Gas Town agent label
|
||||
}
|
||||
resp, err := daemonClient.Create(createArgs)
|
||||
if err != nil {
|
||||
@@ -234,6 +235,10 @@ func runAgentState(cmd *cobra.Command, args []string) error {
|
||||
if err := activeStore.CreateIssue(ctx, agent, actor); err != nil {
|
||||
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
|
||||
if roleType != "" {
|
||||
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 {
|
||||
// Get existing agent bead to verify it's an agent
|
||||
var labels []string
|
||||
if routedResult != nil && routedResult.Issue != nil {
|
||||
// Already have the issue from routed resolution
|
||||
agent = routedResult.Issue
|
||||
// Get labels from routed store
|
||||
labels, _ = routedResult.Store.GetLabels(ctx, agentID)
|
||||
} else if daemonClient != nil && !needsRouting(agentArg) {
|
||||
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID})
|
||||
if err != nil {
|
||||
@@ -259,17 +267,19 @@ func runAgentState(cmd *cobra.Command, args []string) error {
|
||||
if err := json.Unmarshal(resp.Data, &agent); err != nil {
|
||||
return fmt.Errorf("parsing response: %w", err)
|
||||
}
|
||||
labels = agent.Labels
|
||||
} else {
|
||||
var err error
|
||||
agent, err = activeStore.GetIssue(ctx, agentID)
|
||||
if err != nil || agent == nil {
|
||||
return fmt.Errorf("agent bead not found: %s", agentID)
|
||||
}
|
||||
labels, _ = activeStore.GetLabels(ctx, agentID)
|
||||
}
|
||||
|
||||
// Verify agent bead is actually an agent
|
||||
if agent.IssueType != "agent" {
|
||||
return fmt.Errorf("%s is not an agent bead (type=%s)", agentID, agent.IssueType)
|
||||
// Verify agent bead is actually an agent (check for gt:agent label)
|
||||
if !isAgentBead(labels) {
|
||||
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
|
||||
var agent *types.Issue
|
||||
var labels []string
|
||||
if routedResult != nil && routedResult.Issue != nil {
|
||||
// Already have the issue from routed resolution
|
||||
agent = routedResult.Issue
|
||||
labels, _ = routedResult.Store.GetLabels(ctx, agentID)
|
||||
} else if daemonClient != nil && !needsRouting(agentArg) {
|
||||
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID})
|
||||
if err != nil {
|
||||
@@ -373,17 +385,19 @@ func runAgentHeartbeat(cmd *cobra.Command, args []string) error {
|
||||
if err := json.Unmarshal(resp.Data, &agent); err != nil {
|
||||
return fmt.Errorf("parsing response: %w", err)
|
||||
}
|
||||
labels = agent.Labels
|
||||
} else {
|
||||
var err error
|
||||
agent, err = activeStore.GetIssue(ctx, agentID)
|
||||
if err != nil || agent == nil {
|
||||
return fmt.Errorf("agent bead not found: %s", agentID)
|
||||
}
|
||||
labels, _ = activeStore.GetLabels(ctx, agentID)
|
||||
}
|
||||
|
||||
// Verify agent bead is actually an agent
|
||||
if agent.IssueType != "agent" {
|
||||
return fmt.Errorf("%s is not an agent bead (type=%s)", agentID, agent.IssueType)
|
||||
// Verify agent bead is actually an agent (check for gt:agent label)
|
||||
if !isAgentBead(labels) {
|
||||
return fmt.Errorf("%s is not an agent bead (missing gt:agent label)", agentID)
|
||||
}
|
||||
|
||||
// Update only last_activity
|
||||
@@ -464,9 +478,11 @@ func runAgentShow(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Get agent bead
|
||||
var agent *types.Issue
|
||||
var labels []string
|
||||
if routedResult != nil && routedResult.Issue != nil {
|
||||
// Already have the issue from routed resolution
|
||||
agent = routedResult.Issue
|
||||
labels, _ = routedResult.Store.GetLabels(ctx, agentID)
|
||||
} else if daemonClient != nil && !needsRouting(agentArg) {
|
||||
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID})
|
||||
if err != nil {
|
||||
@@ -475,17 +491,19 @@ func runAgentShow(cmd *cobra.Command, args []string) error {
|
||||
if err := json.Unmarshal(resp.Data, &agent); err != nil {
|
||||
return fmt.Errorf("parsing response: %w", err)
|
||||
}
|
||||
labels = agent.Labels
|
||||
} else {
|
||||
var err error
|
||||
agent, err = store.GetIssue(ctx, agentID)
|
||||
if err != nil || agent == nil {
|
||||
return fmt.Errorf("agent bead not found: %s", agentID)
|
||||
}
|
||||
labels, _ = store.GetLabels(ctx, agentID)
|
||||
}
|
||||
|
||||
// Verify agent bead is actually an agent
|
||||
if agent.IssueType != "agent" {
|
||||
return fmt.Errorf("%s is not an agent bead (type=%s)", agentID, agent.IssueType)
|
||||
// Verify agent bead is actually an agent (check for gt:agent label)
|
||||
if !isAgentBead(labels) {
|
||||
return fmt.Errorf("%s is not an agent bead (missing gt:agent label)", agentID)
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
@@ -565,11 +583,11 @@ func runAgentBackfillLabels(cmd *cobra.Command, args []string) error {
|
||||
|
||||
ctx := rootCtx
|
||||
|
||||
// List all agent beads
|
||||
// List all agent beads (by gt:agent label)
|
||||
var agents []*types.Issue
|
||||
if daemonClient != nil {
|
||||
resp, err := daemonClient.List(&rpc.ListArgs{
|
||||
IssueType: "agent",
|
||||
Labels: []string{"gt:agent"},
|
||||
})
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
agentType := types.TypeAgent
|
||||
filter := types.IssueFilter{
|
||||
IssueType: &agentType,
|
||||
Labels: []string{"gt:agent"},
|
||||
}
|
||||
var err error
|
||||
agents, err = store.SearchIssues(ctx, "", filter)
|
||||
@@ -756,6 +773,17 @@ func containsLabel(labels []string, label string) bool {
|
||||
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.
|
||||
// Agent ID patterns:
|
||||
// - Town-level: <prefix>-<role> (e.g., gt-mayor) → role="mayor", rig=""
|
||||
|
||||
@@ -45,11 +45,11 @@ func TestAgentStateWithRouting(t *testing.T) {
|
||||
rigDBPath := filepath.Join(rigBeadsDir, "beads.db")
|
||||
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{
|
||||
ID: "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,
|
||||
RoleType: "polecat",
|
||||
Rig: "testrig",
|
||||
@@ -57,6 +57,9 @@ func TestAgentStateWithRouting(t *testing.T) {
|
||||
if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil {
|
||||
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
|
||||
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")
|
||||
}
|
||||
|
||||
if result.Issue.IssueType != types.TypeAgent {
|
||||
t.Errorf("Expected issue type %q, got %q", types.TypeAgent, result.Issue.IssueType)
|
||||
if result.Issue.IssueType != types.TypeTask {
|
||||
t.Errorf("Expected issue type %q, got %q", types.TypeTask, result.Issue.IssueType)
|
||||
}
|
||||
|
||||
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")
|
||||
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{
|
||||
ID: "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,
|
||||
RoleType: "witness",
|
||||
Rig: "test",
|
||||
@@ -148,6 +151,9 @@ func TestAgentHeartbeatWithRouting(t *testing.T) {
|
||||
if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil {
|
||||
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
|
||||
routesContent := `{"prefix":"gt-","path":"rig"}`
|
||||
@@ -207,11 +213,11 @@ func TestAgentShowWithRouting(t *testing.T) {
|
||||
rigDBPath := filepath.Join(rigBeadsDir, "beads.db")
|
||||
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{
|
||||
ID: "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,
|
||||
RoleType: "crew",
|
||||
Rig: "myrig",
|
||||
@@ -219,6 +225,9 @@ func TestAgentShowWithRouting(t *testing.T) {
|
||||
if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil {
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
if result.Issue.IssueType != types.TypeAgent {
|
||||
t.Errorf("Expected issue type %q, got %q", types.TypeAgent, result.Issue.IssueType)
|
||||
if result.Issue.IssueType != types.TypeTask {
|
||||
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)
|
||||
|
||||
@@ -474,7 +474,15 @@ var createCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// 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 != "" {
|
||||
agentLabel := "role_type:" + issue.RoleType
|
||||
if err := store.AddLabel(ctx, issue.ID, agentLabel, actor); err != nil {
|
||||
|
||||
@@ -33,6 +33,8 @@ var obsidianPriority = []string{
|
||||
}
|
||||
|
||||
// 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{
|
||||
types.TypeBug: "#Bug",
|
||||
types.TypeFeature: "#Feature",
|
||||
@@ -43,10 +45,6 @@ var obsidianTypeTag = map[types.IssueType]string{
|
||||
types.TypeMergeRequest: "#MergeRequest",
|
||||
types.TypeMolecule: "#Molecule",
|
||||
types.TypeGate: "#Gate",
|
||||
types.TypeAgent: "#Agent",
|
||||
types.TypeRole: "#Role",
|
||||
types.TypeRig: "#Rig",
|
||||
types.TypeConvoy: "#Convoy",
|
||||
types.TypeEvent: "#Event",
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
- status=open: slot is available
|
||||
- status=in_progress: slot is held
|
||||
@@ -157,15 +157,15 @@ func runMergeSlotCreate(cmd *cobra.Command, args []string) error {
|
||||
// Create the merge slot bead
|
||||
title := "Merge Slot"
|
||||
description := "Exclusive access slot for serialized conflict resolution in the merge queue."
|
||||
slotType := types.TypeSlot
|
||||
|
||||
if daemonClient != nil {
|
||||
createArgs := &rpc.CreateArgs{
|
||||
ID: slotID,
|
||||
Title: title,
|
||||
Description: description,
|
||||
IssueType: string(slotType),
|
||||
Priority: 0, // P0 - system infrastructure
|
||||
IssueType: string(types.TypeTask), // Use task type; gt:slot label marks it as slot
|
||||
Priority: 0, // P0 - system infrastructure
|
||||
Labels: []string{"gt:slot"}, // Gas Town slot label
|
||||
}
|
||||
resp, err := daemonClient.Create(createArgs)
|
||||
if err != nil {
|
||||
@@ -179,13 +179,18 @@ func runMergeSlotCreate(cmd *cobra.Command, args []string) error {
|
||||
ID: slotID,
|
||||
Title: title,
|
||||
Description: description,
|
||||
IssueType: slotType,
|
||||
IssueType: types.TypeTask, // Use task type; gt:slot label marks it as slot
|
||||
Status: types.StatusOpen,
|
||||
Priority: 0,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, actor); err != nil {
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user