* fix(ready): exclude molecule steps from bd ready by default (GH#1239) Add ID prefix constants (IDPrefixMol, IDPrefixWisp) to types.go as single source of truth. Update pour.go and wisp.go to use these constants. GetReadyWork now excludes issues with -mol- in their ID when no explicit type filter is specified. Users can still see mol steps with --type=task. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(ready): config-driven ID pattern exclusion (GH#1239) Add ready.exclude_id_patterns config for excluding IDs from bd ready. Default patterns: -mol-, -wisp- (molecule steps and wisps). Changes: - Add IncludeMolSteps to WorkFilter for internal callers - Update findGateReadyMolecules and getMoleculeCurrentStep to use it - Make exclusion patterns config-driven via ready.exclude_id_patterns - Remove hardcoded MolStepIDPattern() in favor of config - Add test for custom patterns (e.g., gastown's -role-) Usage: bd config set ready.exclude_id_patterns "-mol-,-wisp-,-role-" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: remove -role- example from ready.go comments Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: remove GH#1239 references from code comments Issue references belong in commit messages, not code. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
227 lines
6.4 KiB
Go
227 lines
6.4 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
|
|
// IncludeMolSteps: true because we specifically need to see molecule steps here
|
|
readyIssues, err := s.GetReadyWork(ctx, types.WorkFilter{Limit: 500, IncludeMolSteps: true})
|
|
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)
|
|
}
|