Files
beads/cmd/bd/mol_current.go
Steve Yegge 15cfef0247 Update documentation for bd mol pour/wisp command structure
Updated all documentation and help text to reflect:
- bd pour → bd mol pour
- bd ephemeral → bd mol wisp
- bd ephemeral list → bd mol wisp list
- bd ephemeral gc → bd mol wisp gc

Files updated:
- docs/MOLECULES.md
- docs/CLI_REFERENCE.md
- docs/ARCHITECTURE.md
- docs/DELETIONS.md
- skills/beads/references/MOLECULES.md
- cmd/bd/mol_catalog.go
- cmd/bd/mol_current.go
- cmd/bd/mol_distill.go
- cmd/bd/cook.go
2025-12-26 23:52:59 -08:00

487 lines
13 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils"
)
// MoleculeProgress holds the progress information for a molecule
type MoleculeProgress struct {
MoleculeID string `json:"molecule_id"`
MoleculeTitle string `json:"molecule_title"`
Assignee string `json:"assignee,omitempty"`
CurrentStep *types.Issue `json:"current_step,omitempty"`
NextStep *types.Issue `json:"next_step,omitempty"`
Steps []*StepStatus `json:"steps"`
Completed int `json:"completed"`
Total int `json:"total"`
}
// StepStatus represents the status of a step in a molecule
type StepStatus struct {
Issue *types.Issue `json:"issue"`
Status string `json:"status"` // "done", "current", "ready", "blocked", "pending"
IsCurrent bool `json:"is_current"` // true if this is the in_progress step
}
var molCurrentCmd = &cobra.Command{
Use: "current [molecule-id]",
Short: "Show current position in molecule workflow",
Long: `Show where you are in a molecule workflow.
If molecule-id is given, show status for that molecule.
If not given, infer from in_progress issues assigned to current agent.
The output shows all steps with status indicators:
[done] - Step is complete (closed)
[current] - Step is in_progress (you are here)
[ready] - Step is ready to start (unblocked)
[blocked] - Step is blocked by dependencies
[pending] - Step is waiting`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ctx := rootCtx
forAgent, _ := cmd.Flags().GetString("for")
// Determine who we're looking for
agent := forAgent
if agent == "" {
agent = actor // Default to current user/agent
}
// mol current requires direct store access for subgraph loading
if store == nil {
if daemonClient != nil {
fmt.Fprintf(os.Stderr, "Error: mol current requires direct database access\n")
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol current\n")
} else {
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
}
os.Exit(1)
}
var molecules []*MoleculeProgress
if len(args) == 1 {
// Explicit molecule ID given
moleculeID, err := utils.ResolvePartialID(ctx, store, args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: molecule '%s' not found\n", args[0])
os.Exit(1)
}
progress, err := getMoleculeProgress(ctx, store, moleculeID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading molecule: %v\n", err)
os.Exit(1)
}
molecules = append(molecules, progress)
} else {
// Infer from in_progress issues
molecules = findInProgressMolecules(ctx, store, agent)
if len(molecules) == 0 {
if jsonOutput {
outputJSON([]interface{}{})
return
}
fmt.Printf("No molecules in progress")
if agent != "" {
fmt.Printf(" for %s", agent)
}
fmt.Println(".")
fmt.Println("\nTo start work on a molecule:")
fmt.Println(" bd mol pour <proto-id> # Instantiate a molecule from template")
fmt.Println(" bd update <step-id> --status in_progress # Claim a step")
return
}
}
if jsonOutput {
outputJSON(molecules)
return
}
// Human-readable output
for i, mol := range molecules {
if i > 0 {
fmt.Println()
}
printMoleculeProgress(mol)
}
},
}
// getMoleculeProgress loads a molecule and computes progress
func getMoleculeProgress(ctx context.Context, s storage.Storage, moleculeID string) (*MoleculeProgress, error) {
subgraph, err := loadTemplateSubgraph(ctx, s, moleculeID)
if err != nil {
return nil, err
}
progress := &MoleculeProgress{
MoleculeID: subgraph.Root.ID,
MoleculeTitle: subgraph.Root.Title,
Assignee: subgraph.Root.Assignee,
Total: len(subgraph.Issues) - 1, // Exclude root
}
// Get ready issues for this molecule
readyIDs := make(map[string]bool)
readyIssues, err := s.GetReadyWork(ctx, types.WorkFilter{})
if err == nil {
for _, issue := range readyIssues {
readyIDs[issue.ID] = true
}
}
// Build step status list (exclude root)
var steps []*StepStatus
for _, issue := range subgraph.Issues {
if issue.ID == subgraph.Root.ID {
continue // Skip root
}
step := &StepStatus{
Issue: issue,
}
switch issue.Status {
case types.StatusClosed:
step.Status = "done"
progress.Completed++
case types.StatusInProgress:
step.Status = "current"
step.IsCurrent = true
progress.CurrentStep = issue
case types.StatusBlocked:
step.Status = "blocked"
default:
// Check if ready (unblocked)
if readyIDs[issue.ID] {
step.Status = "ready"
if progress.NextStep == nil {
progress.NextStep = issue
}
} else {
step.Status = "pending"
}
}
steps = append(steps, step)
}
// Sort steps by dependency order
sortStepsByDependencyOrder(steps, subgraph)
progress.Steps = steps
// If no current step but there's a ready step, set it as next
if progress.CurrentStep == nil && progress.NextStep == nil {
for _, step := range steps {
if step.Status == "ready" {
progress.NextStep = step.Issue
break
}
}
}
return progress, nil
}
// findInProgressMolecules finds molecules with in_progress steps for an agent
func findInProgressMolecules(ctx context.Context, s storage.Storage, agent string) []*MoleculeProgress {
// Query for in_progress issues
var inProgressIssues []*types.Issue
if daemonClient != nil {
listArgs := &rpc.ListArgs{
Status: "in_progress",
Assignee: agent,
}
resp, err := daemonClient.List(listArgs)
if err == nil {
_ = json.Unmarshal(resp.Data, &inProgressIssues)
}
} else {
// Direct query - search for in_progress issues
status := types.StatusInProgress
filter := types.IssueFilter{Status: &status}
if agent != "" {
filter.Assignee = &agent
}
allIssues, err := s.SearchIssues(ctx, "", filter)
if err == nil {
inProgressIssues = allIssues
}
}
if len(inProgressIssues) == 0 {
return nil
}
// For each in_progress issue, find its parent molecule
moleculeMap := make(map[string]*MoleculeProgress)
for _, issue := range inProgressIssues {
moleculeID := findParentMolecule(ctx, s, issue.ID)
if moleculeID == "" {
// Not part of a molecule, skip
continue
}
if _, exists := moleculeMap[moleculeID]; !exists {
progress, err := getMoleculeProgress(ctx, s, moleculeID)
if err == nil {
moleculeMap[moleculeID] = progress
}
}
}
// Convert to slice
var molecules []*MoleculeProgress
for _, mol := range moleculeMap {
molecules = append(molecules, mol)
}
// Sort by molecule ID for consistent output
sort.Slice(molecules, func(i, j int) bool {
return molecules[i].MoleculeID < molecules[j].MoleculeID
})
return molecules
}
// findParentMolecule walks up parent-child chain to find the root molecule
func findParentMolecule(ctx context.Context, s storage.Storage, issueID string) string {
visited := make(map[string]bool)
currentID := issueID
for !visited[currentID] {
visited[currentID] = true
// Get dependencies for current issue
deps, err := s.GetDependencyRecords(ctx, currentID)
if err != nil {
return ""
}
// Find parent-child dependency where current is the child
var parentID string
for _, dep := range deps {
if dep.Type == types.DepParentChild && dep.IssueID == currentID {
parentID = dep.DependsOnID
break
}
}
if parentID == "" {
// No parent - check if current issue is a molecule root
issue, err := s.GetIssue(ctx, currentID)
if err != nil || issue == nil {
return ""
}
// Check if it has the template label (molecules are spawned from templates)
for _, label := range issue.Labels {
if label == BeadsTemplateLabel {
return currentID
}
}
// Also check if it's an epic with children (ad-hoc molecule)
if issue.IssueType == types.TypeEpic {
return currentID
}
return ""
}
currentID = parentID
}
return ""
}
// sortStepsByDependencyOrder sorts steps by their dependency order
func sortStepsByDependencyOrder(steps []*StepStatus, subgraph *TemplateSubgraph) {
// Build dependency graph
depCount := make(map[string]int) // issue ID -> number of deps
for _, step := range steps {
depCount[step.Issue.ID] = 0
}
// Count blocking dependencies within the step set
stepIDs := make(map[string]bool)
for _, step := range steps {
stepIDs[step.Issue.ID] = true
}
for _, dep := range subgraph.Dependencies {
if dep.Type == types.DepBlocks && stepIDs[dep.IssueID] && stepIDs[dep.DependsOnID] {
depCount[dep.IssueID]++
}
}
// Stable sort by dependency count (fewer deps first)
sort.SliceStable(steps, func(i, j int) bool {
return depCount[steps[i].Issue.ID] < depCount[steps[j].Issue.ID]
})
}
// printMoleculeProgress prints the progress in human-readable format
func printMoleculeProgress(mol *MoleculeProgress) {
fmt.Printf("You're working on molecule %s\n", ui.RenderAccent(mol.MoleculeID))
fmt.Printf(" %s\n", mol.MoleculeTitle)
if mol.Assignee != "" {
fmt.Printf(" Assigned to: %s\n", mol.Assignee)
}
fmt.Println()
for _, step := range mol.Steps {
statusIcon := getStatusIcon(step.Status)
marker := ""
if step.IsCurrent {
marker = " <- YOU ARE HERE"
}
fmt.Printf(" %s %s: %s%s\n", statusIcon, step.Issue.ID, step.Issue.Title, marker)
}
fmt.Println()
fmt.Printf("Progress: %d/%d steps complete\n", mol.Completed, mol.Total)
if mol.NextStep != nil && mol.CurrentStep == nil {
fmt.Printf("\nNext ready: %s - %s\n", mol.NextStep.ID, mol.NextStep.Title)
fmt.Printf(" Start with: bd update %s --status in_progress\n", mol.NextStep.ID)
}
}
// getStatusIcon returns the icon for a step status
func getStatusIcon(status string) string {
switch status {
case "done":
return ui.RenderPass("[done]")
case "current":
return ui.RenderWarn("[current]")
case "ready":
return ui.RenderAccent("[ready]")
case "blocked":
return ui.RenderFail("[blocked]")
default:
return "[pending]"
}
}
// ContinueResult holds the result of advancing to the next molecule step
type ContinueResult struct {
ClosedStep *types.Issue `json:"closed_step"`
NextStep *types.Issue `json:"next_step,omitempty"`
AutoAdvanced bool `json:"auto_advanced"`
MolComplete bool `json:"molecule_complete"`
MoleculeID string `json:"molecule_id,omitempty"`
}
// AdvanceToNextStep finds the next ready step in a molecule after closing a step
// If autoClaim is true, it marks the next step as in_progress
// Returns nil if the issue is not part of a molecule
func AdvanceToNextStep(ctx context.Context, s storage.Storage, closedStepID string, autoClaim bool, actorName string) (*ContinueResult, error) {
if s == nil {
return nil, fmt.Errorf("no database connection")
}
// Get the closed step
closedStep, err := s.GetIssue(ctx, closedStepID)
if err != nil || closedStep == nil {
return nil, fmt.Errorf("could not get closed step: %w", err)
}
result := &ContinueResult{
ClosedStep: closedStep,
}
// Find parent molecule
moleculeID := findParentMolecule(ctx, s, closedStepID)
if moleculeID == "" {
// Not part of a molecule - nothing to advance
return nil, nil
}
result.MoleculeID = moleculeID
// Load molecule progress
progress, err := getMoleculeProgress(ctx, s, moleculeID)
if err != nil {
return nil, fmt.Errorf("could not load molecule: %w", err)
}
// Check if molecule is complete
if progress.Completed >= progress.Total {
result.MolComplete = true
return result, nil
}
// Find next ready step
var nextStep *types.Issue
for _, step := range progress.Steps {
if step.Status == "ready" {
nextStep = step.Issue
break
}
}
if nextStep == nil {
// No ready steps - might be blocked
return result, nil
}
result.NextStep = nextStep
// Auto-claim if requested
if autoClaim {
updates := map[string]interface{}{
"status": types.StatusInProgress,
}
if err := s.UpdateIssue(ctx, nextStep.ID, updates, actorName); err != nil {
return result, fmt.Errorf("could not claim next step: %w", err)
}
result.AutoAdvanced = true
}
return result, nil
}
// PrintContinueResult prints the result of advancing to the next step
func PrintContinueResult(result *ContinueResult) {
if result == nil {
return
}
if result.MolComplete {
fmt.Printf("\n%s Molecule %s complete! All steps closed.\n", ui.RenderPass("✓"), result.MoleculeID)
fmt.Println("Consider: bd mol squash " + result.MoleculeID + " --summary '...'")
return
}
if result.NextStep == nil {
fmt.Println("\nNo ready steps in molecule (may be blocked).")
return
}
fmt.Printf("\nNext ready in molecule:\n")
fmt.Printf(" %s: %s\n", result.NextStep.ID, result.NextStep.Title)
if result.AutoAdvanced {
fmt.Printf("\n%s Marked in_progress (use --no-auto to skip)\n", ui.RenderWarn("→"))
} else {
fmt.Printf("\nStart with: bd update %s --status in_progress\n", result.NextStep.ID)
}
}
func init() {
molCurrentCmd.Flags().String("for", "", "Show molecules for a specific agent/assignee")
molCmd.AddCommand(molCurrentCmd)
}