Files
beads/cmd/bd/mol_ready_gated.go
collins 7cf67153de 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>
2026-01-21 10:36:59 -08:00

226 lines
6.3 KiB
Go

package main
import (
"context"
"fmt"
"os"
"sort"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
)
// GatedMolecule represents a molecule ready for gate-resume dispatch
type GatedMolecule struct {
MoleculeID string `json:"molecule_id"`
MoleculeTitle string `json:"molecule_title"`
ClosedGate *types.Issue `json:"closed_gate"`
ReadyStep *types.Issue `json:"ready_step"`
}
// GatedReadyOutput is the JSON output for bd mol ready --gated
type GatedReadyOutput struct {
Molecules []*GatedMolecule `json:"molecules"`
Count int `json:"count"`
}
var molReadyGatedCmd = &cobra.Command{
Use: "ready --gated",
Short: "Find molecules ready for gate-resume dispatch",
Long: `Find molecules where a gate has closed and the workflow is ready to resume.
This command discovers molecules waiting at a gate step where:
1. The molecule has a gate bead that blocks a step
2. The gate bead is now closed (condition satisfied)
3. The blocked step is now ready to proceed
4. No agent currently has this molecule hooked
This enables discovery-based resume without explicit waiter tracking.
The Deacon patrol uses this to find and dispatch gate-ready molecules.
Examples:
bd mol ready --gated # Find all gate-ready molecules
bd mol ready --gated --json # JSON output for automation`,
Run: runMolReadyGated,
}
func runMolReadyGated(cmd *cobra.Command, args []string) {
ctx := rootCtx
// mol ready --gated requires direct store access
if store == nil {
if daemonClient != nil {
fmt.Fprintf(os.Stderr, "Error: mol ready --gated requires direct database access\n")
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol ready --gated\n")
} else {
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
}
os.Exit(1)
}
// Find gate-ready molecules
molecules, err := findGateReadyMolecules(ctx, store)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if jsonOutput {
output := GatedReadyOutput{
Molecules: molecules,
Count: len(molecules),
}
if output.Molecules == nil {
output.Molecules = []*GatedMolecule{}
}
outputJSON(output)
return
}
// Human-readable output
if len(molecules) == 0 {
fmt.Printf("\n%s No molecules ready for gate-resume dispatch\n\n", ui.RenderWarn(""))
return
}
fmt.Printf("\n%s Molecules ready for gate-resume dispatch (%d):\n\n",
ui.RenderAccent(""), len(molecules))
for i, mol := range molecules {
fmt.Printf("%d. %s: %s\n", i+1, ui.RenderID(mol.MoleculeID), mol.MoleculeTitle)
if mol.ClosedGate != nil {
fmt.Printf(" Gate closed: %s (%s)\n", mol.ClosedGate.ID, mol.ClosedGate.AwaitType)
}
if mol.ReadyStep != nil {
fmt.Printf(" Ready step: %s - %s\n", mol.ReadyStep.ID, mol.ReadyStep.Title)
}
fmt.Println()
}
fmt.Println("To dispatch a molecule:")
fmt.Println(" gt sling <agent> --mol <molecule-id>")
}
// findGateReadyMolecules finds molecules where a gate has closed and work can resume.
//
// Logic:
// 1. Find all closed gate beads
// 2. For each closed gate, find what step it was blocking
// 3. Check if that step is now ready (unblocked)
// 4. Find the parent molecule
// 5. Filter out molecules that are already hooked by someone
func findGateReadyMolecules(ctx context.Context, s storage.Storage) ([]*GatedMolecule, error) {
// Step 1: Find all closed gate beads
gateType := types.IssueType("gate")
closedStatus := types.StatusClosed
gateFilter := types.IssueFilter{
IssueType: &gateType,
Status: &closedStatus,
Limit: 100,
}
closedGates, err := s.SearchIssues(ctx, "", gateFilter)
if err != nil {
return nil, fmt.Errorf("searching closed gates: %w", err)
}
if len(closedGates) == 0 {
return nil, nil
}
// Step 2: Get ready work to check which steps are ready
readyIssues, err := s.GetReadyWork(ctx, types.WorkFilter{Limit: 500})
if err != nil {
return nil, fmt.Errorf("getting ready work: %w", err)
}
readyIDs := make(map[string]bool)
for _, issue := range readyIssues {
readyIDs[issue.ID] = true
}
// Step 3: Get hooked molecules to filter out
hookedStatus := types.StatusHooked
hookedFilter := types.IssueFilter{
Status: &hookedStatus,
Limit: 100,
}
hookedIssues, err := s.SearchIssues(ctx, "", hookedFilter)
if err != nil {
// Non-fatal: just continue without filtering
hookedIssues = nil
}
hookedMolecules := make(map[string]bool)
for _, issue := range hookedIssues {
// If the hooked issue is a molecule root, mark it
hookedMolecules[issue.ID] = true
// Also find parent molecule for hooked steps
if parentMol := findParentMolecule(ctx, s, issue.ID); parentMol != "" {
hookedMolecules[parentMol] = true
}
}
// Step 4: For each closed gate, find issues that depend on it (were blocked)
moleculeMap := make(map[string]*GatedMolecule)
for _, gate := range closedGates {
// Find issues that depend on this gate (GetDependents returns issues where depends_on_id = gate.ID)
dependents, err := s.GetDependents(ctx, gate.ID)
if err != nil {
continue
}
for _, dependent := range dependents {
// Check if the previously blocked step is now ready
if !readyIDs[dependent.ID] {
continue
}
// Find the parent molecule
moleculeID := findParentMolecule(ctx, s, dependent.ID)
if moleculeID == "" {
continue
}
// Skip if already hooked
if hookedMolecules[moleculeID] {
continue
}
// Get molecule details
moleculeIssue, err := s.GetIssue(ctx, moleculeID)
if err != nil || moleculeIssue == nil {
continue
}
// Add to results (dedupe by molecule ID)
if _, exists := moleculeMap[moleculeID]; !exists {
moleculeMap[moleculeID] = &GatedMolecule{
MoleculeID: moleculeID,
MoleculeTitle: moleculeIssue.Title,
ClosedGate: gate,
ReadyStep: dependent,
}
}
}
}
// Convert to slice and sort
var molecules []*GatedMolecule
for _, mol := range moleculeMap {
molecules = append(molecules, mol)
}
sort.Slice(molecules, func(i, j int) bool {
return molecules[i].MoleculeID < molecules[j].MoleculeID
})
return molecules, nil
}
func init() {
// Note: --gated flag is registered in ready.go
// Also add as a subcommand under mol for discoverability
molCmd.AddCommand(molReadyGatedCmd)
}