refactor(cmd): split molecule.go into focused files
Split the 1929-line molecule.go into 5 focused files: - molecule.go (376): command definitions, init(), loadMoleculeCatalog - molecule_status.go (673): status, progress, current commands - molecule_list.go (432): list, show, export, parse, instances - molecule_lifecycle.go (359): instantiate, catalog, burn, squash - molecule_attach.go (128): attach, detach, attachment No functional changes - pure refactoring for maintainability. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
128
internal/cmd/molecule_attach.go
Normal file
128
internal/cmd/molecule_attach.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
)
|
||||
|
||||
func runMoleculeAttach(cmd *cobra.Command, args []string) error {
|
||||
pinnedBeadID := args[0]
|
||||
moleculeID := args[1]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Attach the molecule
|
||||
issue, err := b.AttachMolecule(pinnedBeadID, moleculeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("attaching molecule: %w", err)
|
||||
}
|
||||
|
||||
attachment := beads.ParseAttachmentFields(issue)
|
||||
fmt.Printf("%s Attached %s to %s\n", style.Bold.Render("✓"), moleculeID, pinnedBeadID)
|
||||
if attachment != nil && attachment.AttachedAt != "" {
|
||||
fmt.Printf(" attached_at: %s\n", attachment.AttachedAt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeDetach(cmd *cobra.Command, args []string) error {
|
||||
pinnedBeadID := args[0]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Check current attachment first
|
||||
attachment, err := b.GetAttachment(pinnedBeadID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking attachment: %w", err)
|
||||
}
|
||||
|
||||
if attachment == nil {
|
||||
fmt.Printf("%s No molecule attached to %s\n", style.Dim.Render("ℹ"), pinnedBeadID)
|
||||
return nil
|
||||
}
|
||||
|
||||
previousMolecule := attachment.AttachedMolecule
|
||||
|
||||
// Detach the molecule
|
||||
_, err = b.DetachMolecule(pinnedBeadID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("detaching molecule: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Detached %s from %s\n", style.Bold.Render("✓"), previousMolecule, pinnedBeadID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeAttachment(cmd *cobra.Command, args []string) error {
|
||||
pinnedBeadID := args[0]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Get the issue
|
||||
issue, err := b.Show(pinnedBeadID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting issue: %w", err)
|
||||
}
|
||||
|
||||
attachment := beads.ParseAttachmentFields(issue)
|
||||
|
||||
if moleculeJSON {
|
||||
type attachmentOutput struct {
|
||||
IssueID string `json:"issue_id"`
|
||||
IssueTitle string `json:"issue_title"`
|
||||
Status string `json:"status"`
|
||||
AttachedMolecule string `json:"attached_molecule,omitempty"`
|
||||
AttachedAt string `json:"attached_at,omitempty"`
|
||||
}
|
||||
out := attachmentOutput{
|
||||
IssueID: issue.ID,
|
||||
IssueTitle: issue.Title,
|
||||
Status: issue.Status,
|
||||
}
|
||||
if attachment != nil {
|
||||
out.AttachedMolecule = attachment.AttachedMolecule
|
||||
out.AttachedAt = attachment.AttachedAt
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(out)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("\n%s: %s\n", style.Bold.Render(issue.ID), issue.Title)
|
||||
fmt.Printf("Status: %s\n", issue.Status)
|
||||
|
||||
if attachment == nil || attachment.AttachedMolecule == "" {
|
||||
fmt.Printf("\n%s\n", style.Dim.Render("No molecule attached"))
|
||||
} else {
|
||||
fmt.Printf("\n%s\n", style.Bold.Render("Attached Molecule:"))
|
||||
fmt.Printf(" ID: %s\n", attachment.AttachedMolecule)
|
||||
if attachment.AttachedAt != "" {
|
||||
fmt.Printf(" Attached at: %s\n", attachment.AttachedAt)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
359
internal/cmd/molecule_lifecycle.go
Normal file
359
internal/cmd/molecule_lifecycle.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
func runMoleculeInstantiate(cmd *cobra.Command, args []string) error {
|
||||
molID := args[0]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Try catalog first
|
||||
catalog, err := loadMoleculeCatalog(workDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading catalog: %w", err)
|
||||
}
|
||||
|
||||
var mol *beads.Issue
|
||||
|
||||
if catalogMol := catalog.Get(molID); catalogMol != nil {
|
||||
mol = catalogMol.ToIssue()
|
||||
} else {
|
||||
// Fall back to database
|
||||
mol, err = b.Show(molID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting molecule: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if mol.Type != "molecule" {
|
||||
return fmt.Errorf("%s is not a molecule (type: %s)", molID, mol.Type)
|
||||
}
|
||||
|
||||
// Validate molecule
|
||||
if err := beads.ValidateMolecule(mol); err != nil {
|
||||
return fmt.Errorf("invalid molecule: %w", err)
|
||||
}
|
||||
|
||||
// Get the parent issue
|
||||
parent, err := b.Show(moleculeInstParent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting parent issue: %w", err)
|
||||
}
|
||||
|
||||
// Parse context variables
|
||||
ctx := make(map[string]string)
|
||||
for _, kv := range moleculeInstContext {
|
||||
parts := strings.SplitN(kv, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid context format %q (expected key=value)", kv)
|
||||
}
|
||||
ctx[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
// Instantiate the molecule
|
||||
opts := beads.InstantiateOptions{Context: ctx}
|
||||
steps, err := b.InstantiateMolecule(mol, parent, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("instantiating molecule: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Created %d steps from %s on %s\n\n",
|
||||
style.Bold.Render("✓"), len(steps), molID, moleculeInstParent)
|
||||
|
||||
for _, step := range steps {
|
||||
fmt.Printf(" %s: %s\n", style.Dim.Render(step.ID), step.Title)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runMoleculeCatalog lists available molecule protos.
|
||||
func runMoleculeCatalog(cmd *cobra.Command, args []string) error {
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
// Load catalog
|
||||
catalog, err := loadMoleculeCatalog(workDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading catalog: %w", err)
|
||||
}
|
||||
|
||||
molecules := catalog.List()
|
||||
|
||||
if moleculeJSON {
|
||||
type catalogEntry struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Source string `json:"source"`
|
||||
StepCount int `json:"step_count"`
|
||||
}
|
||||
|
||||
var entries []catalogEntry
|
||||
for _, mol := range molecules {
|
||||
steps, _ := beads.ParseMoleculeSteps(mol.Description)
|
||||
entries = append(entries, catalogEntry{
|
||||
ID: mol.ID,
|
||||
Title: mol.Title,
|
||||
Source: mol.Source,
|
||||
StepCount: len(steps),
|
||||
})
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(entries)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("%s Molecule Catalog (%d protos)\n\n", style.Bold.Render("🧬"), len(molecules))
|
||||
|
||||
if len(molecules) == 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render("(no protos available)"))
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, mol := range molecules {
|
||||
steps, _ := beads.ParseMoleculeSteps(mol.Description)
|
||||
stepCount := len(steps)
|
||||
|
||||
sourceMarker := style.Dim.Render(fmt.Sprintf("[%s]", mol.Source))
|
||||
fmt.Printf(" %s: %s (%d steps) %s\n",
|
||||
style.Bold.Render(mol.ID), mol.Title, stepCount, sourceMarker)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runMoleculeBurn burns (destroys) the current molecule attachment.
|
||||
func runMoleculeBurn(cmd *cobra.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
|
||||
// Find town root
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding workspace: %w", err)
|
||||
}
|
||||
if townRoot == "" {
|
||||
return fmt.Errorf("not in a Gas Town workspace")
|
||||
}
|
||||
|
||||
// Determine target agent
|
||||
var target string
|
||||
if len(args) > 0 {
|
||||
target = args[0]
|
||||
} else {
|
||||
// Auto-detect from current directory
|
||||
roleCtx := detectRole(cwd, townRoot)
|
||||
target = buildAgentIdentity(roleCtx)
|
||||
if target == "" {
|
||||
return fmt.Errorf("cannot determine agent identity from current directory")
|
||||
}
|
||||
}
|
||||
|
||||
// Find beads directory
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Find agent's pinned bead (handoff bead)
|
||||
parts := strings.Split(target, "/")
|
||||
role := parts[len(parts)-1]
|
||||
|
||||
handoff, err := b.FindHandoffBead(role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding handoff bead: %w", err)
|
||||
}
|
||||
if handoff == nil {
|
||||
return fmt.Errorf("no handoff bead found for %s", target)
|
||||
}
|
||||
|
||||
// Check for attached molecule
|
||||
attachment := beads.ParseAttachmentFields(handoff)
|
||||
if attachment == nil || attachment.AttachedMolecule == "" {
|
||||
fmt.Printf("%s No molecule attached to %s - nothing to burn\n",
|
||||
style.Dim.Render("ℹ"), target)
|
||||
return nil
|
||||
}
|
||||
|
||||
moleculeID := attachment.AttachedMolecule
|
||||
|
||||
// Detach the molecule (this "burns" it by removing the attachment)
|
||||
_, err = b.DetachMolecule(handoff.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("detaching molecule: %w", err)
|
||||
}
|
||||
|
||||
if moleculeJSON {
|
||||
result := map[string]interface{}{
|
||||
"burned": moleculeID,
|
||||
"from": target,
|
||||
"handoff_id": handoff.ID,
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(result)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Burned molecule %s from %s\n",
|
||||
style.Bold.Render("🔥"), moleculeID, target)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runMoleculeSquash squashes the current molecule into a digest.
|
||||
func runMoleculeSquash(cmd *cobra.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
|
||||
// Find town root
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding workspace: %w", err)
|
||||
}
|
||||
if townRoot == "" {
|
||||
return fmt.Errorf("not in a Gas Town workspace")
|
||||
}
|
||||
|
||||
// Determine target agent
|
||||
var target string
|
||||
if len(args) > 0 {
|
||||
target = args[0]
|
||||
} else {
|
||||
// Auto-detect from current directory
|
||||
roleCtx := detectRole(cwd, townRoot)
|
||||
target = buildAgentIdentity(roleCtx)
|
||||
if target == "" {
|
||||
return fmt.Errorf("cannot determine agent identity from current directory")
|
||||
}
|
||||
}
|
||||
|
||||
// Find beads directory
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Find agent's pinned bead (handoff bead)
|
||||
parts := strings.Split(target, "/")
|
||||
role := parts[len(parts)-1]
|
||||
|
||||
handoff, err := b.FindHandoffBead(role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding handoff bead: %w", err)
|
||||
}
|
||||
if handoff == nil {
|
||||
return fmt.Errorf("no handoff bead found for %s", target)
|
||||
}
|
||||
|
||||
// Check for attached molecule
|
||||
attachment := beads.ParseAttachmentFields(handoff)
|
||||
if attachment == nil || attachment.AttachedMolecule == "" {
|
||||
fmt.Printf("%s No molecule attached to %s - nothing to squash\n",
|
||||
style.Dim.Render("ℹ"), target)
|
||||
return nil
|
||||
}
|
||||
|
||||
moleculeID := attachment.AttachedMolecule
|
||||
|
||||
// Get progress info for the digest
|
||||
progress, _ := getMoleculeProgressInfo(b, moleculeID)
|
||||
|
||||
// Create a digest issue
|
||||
digestTitle := fmt.Sprintf("Digest: %s", moleculeID)
|
||||
digestDesc := fmt.Sprintf(`Squashed molecule execution.
|
||||
|
||||
molecule: %s
|
||||
agent: %s
|
||||
squashed_at: %s
|
||||
`, moleculeID, target, time.Now().UTC().Format(time.RFC3339))
|
||||
|
||||
if progress != nil {
|
||||
digestDesc += fmt.Sprintf(`
|
||||
## Execution Summary
|
||||
- Steps: %d/%d completed
|
||||
- Status: %s
|
||||
`, progress.DoneSteps, progress.TotalSteps, func() string {
|
||||
if progress.Complete {
|
||||
return "complete"
|
||||
}
|
||||
return "partial"
|
||||
}())
|
||||
}
|
||||
|
||||
// Create the digest bead
|
||||
digestIssue, err := b.Create(beads.CreateOptions{
|
||||
Title: digestTitle,
|
||||
Description: digestDesc,
|
||||
Type: "task",
|
||||
Priority: 4, // P4 - backlog priority for digests
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating digest: %w", err)
|
||||
}
|
||||
|
||||
// Add the digest label
|
||||
_ = b.Update(digestIssue.ID, beads.UpdateOptions{
|
||||
AddLabels: []string{"digest"},
|
||||
})
|
||||
|
||||
// Close the digest immediately
|
||||
closedStatus := "closed"
|
||||
err = b.Update(digestIssue.ID, beads.UpdateOptions{
|
||||
Status: &closedStatus,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("%s Created digest but couldn't close it: %v\n",
|
||||
style.Dim.Render("Warning:"), err)
|
||||
}
|
||||
|
||||
// Detach the molecule from the handoff bead
|
||||
_, err = b.DetachMolecule(handoff.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("detaching molecule: %w", err)
|
||||
}
|
||||
|
||||
if moleculeJSON {
|
||||
result := map[string]interface{}{
|
||||
"squashed": moleculeID,
|
||||
"digest_id": digestIssue.ID,
|
||||
"from": target,
|
||||
"handoff_id": handoff.ID,
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(result)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Squashed molecule %s → digest %s\n",
|
||||
style.Bold.Render("📦"), moleculeID, digestIssue.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
432
internal/cmd/molecule_list.go
Normal file
432
internal/cmd/molecule_list.go
Normal file
@@ -0,0 +1,432 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
)
|
||||
|
||||
func runMoleculeList(cmd *cobra.Command, args []string) error {
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
// Collect molecules from requested sources
|
||||
type moleculeEntry struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Source string `json:"source"`
|
||||
StepCount int `json:"step_count,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
var entries []moleculeEntry
|
||||
|
||||
// Load from catalog (unless --db only)
|
||||
if !moleculeDBOnly {
|
||||
catalog, err := loadMoleculeCatalog(workDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading catalog: %w", err)
|
||||
}
|
||||
|
||||
for _, mol := range catalog.List() {
|
||||
steps, _ := beads.ParseMoleculeSteps(mol.Description)
|
||||
entries = append(entries, moleculeEntry{
|
||||
ID: mol.ID,
|
||||
Title: mol.Title,
|
||||
Source: mol.Source,
|
||||
StepCount: len(steps),
|
||||
Description: mol.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Load from database (unless --catalog only)
|
||||
if !moleculeCatalogOnly {
|
||||
b := beads.New(workDir)
|
||||
issues, err := b.List(beads.ListOptions{
|
||||
Type: "molecule",
|
||||
Status: "all",
|
||||
Priority: -1,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing molecules: %w", err)
|
||||
}
|
||||
|
||||
// Track catalog IDs to avoid duplicates
|
||||
catalogIDs := make(map[string]bool)
|
||||
for _, e := range entries {
|
||||
catalogIDs[e.ID] = true
|
||||
}
|
||||
|
||||
for _, mol := range issues {
|
||||
// Skip if already in catalog (catalog takes precedence)
|
||||
if catalogIDs[mol.ID] {
|
||||
continue
|
||||
}
|
||||
|
||||
steps, _ := beads.ParseMoleculeSteps(mol.Description)
|
||||
entries = append(entries, moleculeEntry{
|
||||
ID: mol.ID,
|
||||
Title: mol.Title,
|
||||
Source: "database",
|
||||
StepCount: len(steps),
|
||||
Status: mol.Status,
|
||||
Description: mol.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if moleculeJSON {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(entries)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("%s Molecules (%d)\n\n", style.Bold.Render("🧬"), len(entries))
|
||||
|
||||
if len(entries) == 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render("(no molecules defined)"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create styled table
|
||||
table := style.NewTable(
|
||||
style.Column{Name: "ID", Width: 20},
|
||||
style.Column{Name: "TITLE", Width: 35},
|
||||
style.Column{Name: "STEPS", Width: 5, Align: style.AlignRight},
|
||||
style.Column{Name: "SOURCE", Width: 10},
|
||||
)
|
||||
|
||||
for _, mol := range entries {
|
||||
// Format steps count
|
||||
stepStr := ""
|
||||
if mol.StepCount > 0 {
|
||||
stepStr = fmt.Sprintf("%d", mol.StepCount)
|
||||
}
|
||||
|
||||
// Format title with status
|
||||
title := mol.Title
|
||||
if mol.Status == "closed" {
|
||||
title = style.Dim.Render(mol.Title + " [closed]")
|
||||
}
|
||||
|
||||
// Format source
|
||||
source := style.Dim.Render(mol.Source)
|
||||
|
||||
table.AddRow(mol.ID, title, stepStr, source)
|
||||
}
|
||||
|
||||
fmt.Print(table.Render())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeExport(cmd *cobra.Command, args []string) error {
|
||||
path := args[0]
|
||||
|
||||
if err := beads.ExportBuiltinMolecules(path); err != nil {
|
||||
return fmt.Errorf("exporting molecules: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Exported %d built-in molecules to %s\n",
|
||||
style.Bold.Render("✓"), len(beads.BuiltinMolecules()), path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeShow(cmd *cobra.Command, args []string) error {
|
||||
molID := args[0]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
// Try catalog first
|
||||
catalog, err := loadMoleculeCatalog(workDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading catalog: %w", err)
|
||||
}
|
||||
|
||||
var mol *beads.Issue
|
||||
var source string
|
||||
|
||||
if catalogMol := catalog.Get(molID); catalogMol != nil {
|
||||
mol = catalogMol.ToIssue()
|
||||
source = catalogMol.Source
|
||||
} else {
|
||||
// Fall back to database
|
||||
b := beads.New(workDir)
|
||||
mol, err = b.Show(molID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting molecule: %w", err)
|
||||
}
|
||||
source = "database"
|
||||
}
|
||||
|
||||
if mol.Type != "molecule" {
|
||||
return fmt.Errorf("%s is not a molecule (type: %s)", molID, mol.Type)
|
||||
}
|
||||
|
||||
// Parse steps
|
||||
steps, parseErr := beads.ParseMoleculeSteps(mol.Description)
|
||||
_ = source // Used below in output
|
||||
|
||||
// For JSON, include parsed steps
|
||||
if moleculeJSON {
|
||||
type moleculeOutput struct {
|
||||
*beads.Issue
|
||||
Source string `json:"source"`
|
||||
Steps []beads.MoleculeStep `json:"steps,omitempty"`
|
||||
ParseError string `json:"parse_error,omitempty"`
|
||||
}
|
||||
out := moleculeOutput{Issue: mol, Source: source, Steps: steps}
|
||||
if parseErr != nil {
|
||||
out.ParseError = parseErr.Error()
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(out)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("\n%s: %s %s\n", style.Bold.Render(mol.ID), mol.Title, style.Dim.Render(fmt.Sprintf("[%s]", source)))
|
||||
fmt.Printf("Type: %s\n", mol.Type)
|
||||
|
||||
if parseErr != nil {
|
||||
fmt.Printf("\n%s Parse error: %s\n", style.Bold.Render("⚠"), parseErr)
|
||||
}
|
||||
|
||||
// Show steps
|
||||
fmt.Printf("\nSteps (%d):\n", len(steps))
|
||||
if len(steps) == 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render("(no steps defined)"))
|
||||
} else {
|
||||
// Find which steps are ready (no dependencies)
|
||||
for _, step := range steps {
|
||||
needsStr := ""
|
||||
if len(step.Needs) == 0 {
|
||||
needsStr = style.Dim.Render("(ready first)")
|
||||
} else {
|
||||
needsStr = fmt.Sprintf("Needs: %s", strings.Join(step.Needs, ", "))
|
||||
}
|
||||
|
||||
tierStr := ""
|
||||
if step.Tier != "" {
|
||||
tierStr = fmt.Sprintf(" [%s]", step.Tier)
|
||||
}
|
||||
|
||||
fmt.Printf(" %-12s → %s%s\n", step.Ref, needsStr, tierStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Count instances (need beads client for this)
|
||||
b := beads.New(workDir)
|
||||
instances, _ := findMoleculeInstances(b, molID)
|
||||
fmt.Printf("\nInstances: %d\n", len(instances))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeParse(cmd *cobra.Command, args []string) error {
|
||||
molID := args[0]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
mol, err := b.Show(molID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting molecule: %w", err)
|
||||
}
|
||||
|
||||
// Validate the molecule
|
||||
validationErr := beads.ValidateMolecule(mol)
|
||||
|
||||
// Parse steps regardless of validation
|
||||
steps, parseErr := beads.ParseMoleculeSteps(mol.Description)
|
||||
|
||||
if moleculeJSON {
|
||||
type parseOutput struct {
|
||||
Valid bool `json:"valid"`
|
||||
ValidationError string `json:"validation_error,omitempty"`
|
||||
ParseError string `json:"parse_error,omitempty"`
|
||||
Steps []beads.MoleculeStep `json:"steps"`
|
||||
}
|
||||
out := parseOutput{
|
||||
Valid: validationErr == nil,
|
||||
Steps: steps,
|
||||
}
|
||||
if validationErr != nil {
|
||||
out.ValidationError = validationErr.Error()
|
||||
}
|
||||
if parseErr != nil {
|
||||
out.ParseError = parseErr.Error()
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(out)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("\n%s: %s\n\n", style.Bold.Render(mol.ID), mol.Title)
|
||||
|
||||
if validationErr != nil {
|
||||
fmt.Printf("%s Validation failed: %s\n\n", style.Bold.Render("✗"), validationErr)
|
||||
} else {
|
||||
fmt.Printf("%s Valid molecule\n\n", style.Bold.Render("✓"))
|
||||
}
|
||||
|
||||
if parseErr != nil {
|
||||
fmt.Printf("Parse error: %s\n\n", parseErr)
|
||||
}
|
||||
|
||||
fmt.Printf("Parsed Steps (%d):\n", len(steps))
|
||||
for i, step := range steps {
|
||||
fmt.Printf("\n [%d] %s\n", i+1, style.Bold.Render(step.Ref))
|
||||
if step.Title != step.Ref {
|
||||
fmt.Printf(" Title: %s\n", step.Title)
|
||||
}
|
||||
if len(step.Needs) > 0 {
|
||||
fmt.Printf(" Needs: %s\n", strings.Join(step.Needs, ", "))
|
||||
}
|
||||
if step.Tier != "" {
|
||||
fmt.Printf(" Tier: %s\n", step.Tier)
|
||||
}
|
||||
if step.Instructions != "" {
|
||||
// Show first line of instructions
|
||||
firstLine := strings.SplitN(step.Instructions, "\n", 2)[0]
|
||||
if len(firstLine) > 60 {
|
||||
firstLine = firstLine[:57] + "..."
|
||||
}
|
||||
fmt.Printf(" Instructions: %s\n", style.Dim.Render(firstLine))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeInstances(cmd *cobra.Command, args []string) error {
|
||||
molID := args[0]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Verify the molecule exists
|
||||
mol, err := b.Show(molID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting molecule: %w", err)
|
||||
}
|
||||
|
||||
if mol.Type != "molecule" {
|
||||
return fmt.Errorf("%s is not a molecule (type: %s)", molID, mol.Type)
|
||||
}
|
||||
|
||||
// Find all instances
|
||||
instances, err := findMoleculeInstances(b, molID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding instances: %w", err)
|
||||
}
|
||||
|
||||
if moleculeJSON {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(instances)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("\n%s Instances of %s (%d)\n\n",
|
||||
style.Bold.Render("📋"), molID, len(instances))
|
||||
|
||||
if len(instances) == 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render("(no instantiations found)"))
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-16s %-12s %s\n",
|
||||
style.Bold.Render("Parent"),
|
||||
style.Bold.Render("Status"),
|
||||
style.Bold.Render("Created"))
|
||||
fmt.Println(strings.Repeat("-", 50))
|
||||
|
||||
for _, inst := range instances {
|
||||
// Calculate progress from children
|
||||
progress := ""
|
||||
if len(inst.Children) > 0 {
|
||||
closed := 0
|
||||
for _, childID := range inst.Children {
|
||||
child, err := b.Show(childID)
|
||||
if err == nil && child.Status == "closed" {
|
||||
closed++
|
||||
}
|
||||
}
|
||||
progress = fmt.Sprintf(" (%d/%d complete)", closed, len(inst.Children))
|
||||
}
|
||||
|
||||
statusStr := inst.Status
|
||||
if inst.Status == "closed" {
|
||||
statusStr = style.Dim.Render("done")
|
||||
} else if inst.Status == "in_progress" {
|
||||
statusStr = "active"
|
||||
}
|
||||
|
||||
created := ""
|
||||
if inst.CreatedAt != "" {
|
||||
// Parse and format date
|
||||
created = inst.CreatedAt[:10] // Just the date portion
|
||||
}
|
||||
|
||||
fmt.Printf("%-16s %-12s %s%s\n", inst.ID, statusStr, created, progress)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findMoleculeInstances finds all parent issues that have steps instantiated from the given molecule.
|
||||
func findMoleculeInstances(b *beads.Beads, molID string) ([]*beads.Issue, error) {
|
||||
// Get all issues and look for ones with children that have instantiated_from metadata
|
||||
// This is a brute-force approach - could be optimized with better queries
|
||||
|
||||
// Strategy: search for issues whose descriptions contain "instantiated_from: <molID>"
|
||||
allIssues, err := b.List(beads.ListOptions{Status: "all", Priority: -1})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find issues that reference this molecule
|
||||
parentIDs := make(map[string]bool)
|
||||
for _, issue := range allIssues {
|
||||
if strings.Contains(issue.Description, fmt.Sprintf("instantiated_from: %s", molID)) {
|
||||
// This is a step - find its parent
|
||||
if issue.Parent != "" {
|
||||
parentIDs[issue.Parent] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the parent issues
|
||||
var parents []*beads.Issue
|
||||
for parentID := range parentIDs {
|
||||
parent, err := b.Show(parentID)
|
||||
if err == nil {
|
||||
parents = append(parents, parent)
|
||||
}
|
||||
}
|
||||
|
||||
return parents, nil
|
||||
}
|
||||
673
internal/cmd/molecule_status.go
Normal file
673
internal/cmd/molecule_status.go
Normal file
@@ -0,0 +1,673 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
// MoleculeProgressInfo contains progress information for a molecule instance.
|
||||
type MoleculeProgressInfo struct {
|
||||
RootID string `json:"root_id"`
|
||||
RootTitle string `json:"root_title"`
|
||||
MoleculeID string `json:"molecule_id,omitempty"`
|
||||
TotalSteps int `json:"total_steps"`
|
||||
DoneSteps int `json:"done_steps"`
|
||||
InProgress int `json:"in_progress_steps"`
|
||||
ReadySteps []string `json:"ready_steps"`
|
||||
BlockedSteps []string `json:"blocked_steps"`
|
||||
Percent int `json:"percent_complete"`
|
||||
Complete bool `json:"complete"`
|
||||
}
|
||||
|
||||
// MoleculeStatusInfo contains status information for an agent's hook.
|
||||
type MoleculeStatusInfo struct {
|
||||
Target string `json:"target"`
|
||||
Role string `json:"role"`
|
||||
HasWork bool `json:"has_work"`
|
||||
PinnedBead *beads.Issue `json:"pinned_bead,omitempty"`
|
||||
AttachedMolecule string `json:"attached_molecule,omitempty"`
|
||||
AttachedAt string `json:"attached_at,omitempty"`
|
||||
IsWisp bool `json:"is_wisp"`
|
||||
Progress *MoleculeProgressInfo `json:"progress,omitempty"`
|
||||
NextAction string `json:"next_action,omitempty"`
|
||||
}
|
||||
|
||||
// MoleculeCurrentInfo contains info about what an agent should be working on.
|
||||
type MoleculeCurrentInfo struct {
|
||||
Identity string `json:"identity"`
|
||||
HandoffID string `json:"handoff_id,omitempty"`
|
||||
HandoffTitle string `json:"handoff_title,omitempty"`
|
||||
MoleculeID string `json:"molecule_id,omitempty"`
|
||||
MoleculeTitle string `json:"molecule_title,omitempty"`
|
||||
StepsComplete int `json:"steps_complete"`
|
||||
StepsTotal int `json:"steps_total"`
|
||||
CurrentStepID string `json:"current_step_id,omitempty"`
|
||||
CurrentStep string `json:"current_step,omitempty"`
|
||||
Status string `json:"status"` // "working", "naked", "complete", "blocked"
|
||||
}
|
||||
|
||||
func runMoleculeProgress(cmd *cobra.Command, args []string) error {
|
||||
rootID := args[0]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Get the root issue
|
||||
root, err := b.Show(rootID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting root issue: %w", err)
|
||||
}
|
||||
|
||||
// Find all children of the root issue
|
||||
children, err := b.List(beads.ListOptions{
|
||||
Parent: rootID,
|
||||
Status: "all",
|
||||
Priority: -1,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing children: %w", err)
|
||||
}
|
||||
|
||||
if len(children) == 0 {
|
||||
return fmt.Errorf("no steps found for %s (not a molecule root?)", rootID)
|
||||
}
|
||||
|
||||
// Build progress info
|
||||
progress := MoleculeProgressInfo{
|
||||
RootID: rootID,
|
||||
RootTitle: root.Title,
|
||||
}
|
||||
|
||||
// Try to find molecule ID from first child's description
|
||||
for _, child := range children {
|
||||
if molID := extractMoleculeID(child.Description); molID != "" {
|
||||
progress.MoleculeID = molID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Build set of closed issue IDs for dependency checking
|
||||
closedIDs := make(map[string]bool)
|
||||
for _, child := range children {
|
||||
if child.Status == "closed" {
|
||||
closedIDs[child.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Categorize steps
|
||||
for _, child := range children {
|
||||
progress.TotalSteps++
|
||||
|
||||
switch child.Status {
|
||||
case "closed":
|
||||
progress.DoneSteps++
|
||||
case "in_progress":
|
||||
progress.InProgress++
|
||||
case "open":
|
||||
// Check if all dependencies are closed
|
||||
allDepsClosed := true
|
||||
for _, depID := range child.DependsOn {
|
||||
if !closedIDs[depID] {
|
||||
allDepsClosed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(child.DependsOn) == 0 || allDepsClosed {
|
||||
progress.ReadySteps = append(progress.ReadySteps, child.ID)
|
||||
} else {
|
||||
progress.BlockedSteps = append(progress.BlockedSteps, child.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate completion percentage
|
||||
if progress.TotalSteps > 0 {
|
||||
progress.Percent = (progress.DoneSteps * 100) / progress.TotalSteps
|
||||
}
|
||||
progress.Complete = progress.DoneSteps == progress.TotalSteps
|
||||
|
||||
// JSON output
|
||||
if moleculeJSON {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(progress)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("\n%s %s\n\n", style.Bold.Render("🧬 Molecule Progress:"), root.Title)
|
||||
fmt.Printf(" Root: %s\n", rootID)
|
||||
if progress.MoleculeID != "" {
|
||||
fmt.Printf(" Molecule: %s\n", progress.MoleculeID)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Progress bar
|
||||
barWidth := 20
|
||||
filled := (progress.Percent * barWidth) / 100
|
||||
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
|
||||
fmt.Printf(" [%s] %d%% (%d/%d)\n\n", bar, progress.Percent, progress.DoneSteps, progress.TotalSteps)
|
||||
|
||||
// Step status
|
||||
fmt.Printf(" Done: %d\n", progress.DoneSteps)
|
||||
fmt.Printf(" In Progress: %d\n", progress.InProgress)
|
||||
fmt.Printf(" Ready: %d", len(progress.ReadySteps))
|
||||
if len(progress.ReadySteps) > 0 {
|
||||
fmt.Printf(" (%s)", strings.Join(progress.ReadySteps, ", "))
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf(" Blocked: %d\n", len(progress.BlockedSteps))
|
||||
|
||||
if progress.Complete {
|
||||
fmt.Printf("\n %s\n", style.Bold.Render("✓ Molecule complete!"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractMoleculeID extracts the molecule ID from an issue's description.
|
||||
func extractMoleculeID(description string) string {
|
||||
lines := strings.Split(description, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "instantiated_from:") {
|
||||
return strings.TrimSpace(strings.TrimPrefix(line, "instantiated_from:"))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func runMoleculeStatus(cmd *cobra.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
|
||||
// Find town root
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding workspace: %w", err)
|
||||
}
|
||||
if townRoot == "" {
|
||||
return fmt.Errorf("not in a Gas Town workspace")
|
||||
}
|
||||
|
||||
// Determine target agent
|
||||
var target string
|
||||
var roleCtx RoleContext
|
||||
|
||||
if len(args) > 0 {
|
||||
// Explicit target provided
|
||||
target = args[0]
|
||||
} else {
|
||||
// Auto-detect from current directory
|
||||
roleCtx = detectRole(cwd, townRoot)
|
||||
target = buildAgentIdentity(roleCtx)
|
||||
if target == "" {
|
||||
return fmt.Errorf("cannot determine agent identity from current directory (role: %s)", roleCtx.Role)
|
||||
}
|
||||
}
|
||||
|
||||
// Find beads directory
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Find pinned beads for this agent
|
||||
pinnedBeads, err := b.List(beads.ListOptions{
|
||||
Status: beads.StatusPinned,
|
||||
Assignee: target,
|
||||
Priority: -1,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing pinned beads: %w", err)
|
||||
}
|
||||
|
||||
// Build status info
|
||||
status := MoleculeStatusInfo{
|
||||
Target: target,
|
||||
Role: string(roleCtx.Role),
|
||||
HasWork: len(pinnedBeads) > 0,
|
||||
}
|
||||
|
||||
if len(pinnedBeads) > 0 {
|
||||
// Take the first pinned bead (agents typically have one pinned bead)
|
||||
status.PinnedBead = pinnedBeads[0]
|
||||
|
||||
// Check for attached molecule
|
||||
attachment := beads.ParseAttachmentFields(pinnedBeads[0])
|
||||
if attachment != nil {
|
||||
status.AttachedMolecule = attachment.AttachedMolecule
|
||||
status.AttachedAt = attachment.AttachedAt
|
||||
|
||||
// Check if it's a wisp (look for wisp indicator in description)
|
||||
status.IsWisp = strings.Contains(pinnedBeads[0].Description, "wisp: true") ||
|
||||
strings.Contains(pinnedBeads[0].Description, "is_wisp: true")
|
||||
|
||||
// Get progress if there's an attached molecule
|
||||
if attachment.AttachedMolecule != "" {
|
||||
progress, _ := getMoleculeProgressInfo(b, attachment.AttachedMolecule)
|
||||
status.Progress = progress
|
||||
|
||||
// Determine next action
|
||||
status.NextAction = determineNextAction(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine next action if no work is slung
|
||||
if !status.HasWork {
|
||||
status.NextAction = "Check inbox for work assignments: gt mail inbox"
|
||||
} else if status.AttachedMolecule == "" {
|
||||
status.NextAction = "Attach a molecule to start work: gt mol attach <bead-id> <molecule-id>"
|
||||
}
|
||||
|
||||
// JSON output
|
||||
if moleculeJSON {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(status)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
return outputMoleculeStatus(status)
|
||||
}
|
||||
|
||||
// buildAgentIdentity constructs the agent identity string from role context.
|
||||
func buildAgentIdentity(ctx RoleContext) string {
|
||||
switch ctx.Role {
|
||||
case RoleMayor:
|
||||
return "mayor"
|
||||
case RoleDeacon:
|
||||
return "deacon"
|
||||
case RoleWitness:
|
||||
return ctx.Rig + "/witness"
|
||||
case RoleRefinery:
|
||||
return ctx.Rig + "/refinery"
|
||||
case RolePolecat:
|
||||
return ctx.Rig + "/" + ctx.Polecat
|
||||
case RoleCrew:
|
||||
return ctx.Rig + "/crew/" + ctx.Polecat
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// getMoleculeProgressInfo gets progress info for a molecule instance.
|
||||
func getMoleculeProgressInfo(b *beads.Beads, moleculeRootID string) (*MoleculeProgressInfo, error) {
|
||||
// Get the molecule root issue
|
||||
root, err := b.Show(moleculeRootID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting molecule root: %w", err)
|
||||
}
|
||||
|
||||
// Find all children of the root issue
|
||||
children, err := b.List(beads.ListOptions{
|
||||
Parent: moleculeRootID,
|
||||
Status: "all",
|
||||
Priority: -1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing children: %w", err)
|
||||
}
|
||||
|
||||
if len(children) == 0 {
|
||||
// No children - might be a simple issue, not a molecule
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Build progress info
|
||||
progress := &MoleculeProgressInfo{
|
||||
RootID: moleculeRootID,
|
||||
RootTitle: root.Title,
|
||||
}
|
||||
|
||||
// Try to find molecule ID from first child's description
|
||||
for _, child := range children {
|
||||
if molID := extractMoleculeID(child.Description); molID != "" {
|
||||
progress.MoleculeID = molID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Build set of closed issue IDs for dependency checking
|
||||
closedIDs := make(map[string]bool)
|
||||
for _, child := range children {
|
||||
if child.Status == "closed" {
|
||||
closedIDs[child.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Categorize steps
|
||||
for _, child := range children {
|
||||
progress.TotalSteps++
|
||||
|
||||
switch child.Status {
|
||||
case "closed":
|
||||
progress.DoneSteps++
|
||||
case "in_progress":
|
||||
progress.InProgress++
|
||||
case "open":
|
||||
// Check if all dependencies are closed
|
||||
allDepsClosed := true
|
||||
for _, depID := range child.DependsOn {
|
||||
if !closedIDs[depID] {
|
||||
allDepsClosed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(child.DependsOn) == 0 || allDepsClosed {
|
||||
progress.ReadySteps = append(progress.ReadySteps, child.ID)
|
||||
} else {
|
||||
progress.BlockedSteps = append(progress.BlockedSteps, child.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate completion percentage
|
||||
if progress.TotalSteps > 0 {
|
||||
progress.Percent = (progress.DoneSteps * 100) / progress.TotalSteps
|
||||
}
|
||||
progress.Complete = progress.DoneSteps == progress.TotalSteps
|
||||
|
||||
return progress, nil
|
||||
}
|
||||
|
||||
// determineNextAction suggests the next action based on status.
|
||||
func determineNextAction(status MoleculeStatusInfo) string {
|
||||
if status.Progress == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if status.Progress.Complete {
|
||||
return "Molecule complete! Close the bead: bd close " + status.PinnedBead.ID
|
||||
}
|
||||
|
||||
if status.Progress.InProgress > 0 {
|
||||
return "Continue working on in-progress steps"
|
||||
}
|
||||
|
||||
if len(status.Progress.ReadySteps) > 0 {
|
||||
return fmt.Sprintf("Start next ready step: bd update %s --status=in_progress", status.Progress.ReadySteps[0])
|
||||
}
|
||||
|
||||
if len(status.Progress.BlockedSteps) > 0 {
|
||||
return "All remaining steps are blocked - waiting on dependencies"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// outputMoleculeStatus outputs human-readable status.
|
||||
func outputMoleculeStatus(status MoleculeStatusInfo) error {
|
||||
// Header with hook icon
|
||||
fmt.Printf("\n%s Hook Status: %s\n", style.Bold.Render("🪝"), status.Target)
|
||||
if status.Role != "" && status.Role != "unknown" {
|
||||
fmt.Printf("Role: %s\n", status.Role)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if !status.HasWork {
|
||||
fmt.Printf("%s\n", style.Dim.Render("Nothing on hook - no work slung"))
|
||||
fmt.Printf("\n%s %s\n", style.Bold.Render("Next:"), status.NextAction)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Show pinned bead info
|
||||
fmt.Printf("%s %s: %s\n", style.Bold.Render("📌 Pinned:"), status.PinnedBead.ID, status.PinnedBead.Title)
|
||||
|
||||
// Show attached molecule
|
||||
if status.AttachedMolecule != "" {
|
||||
molType := "Molecule"
|
||||
if status.IsWisp {
|
||||
molType = "Wisp"
|
||||
}
|
||||
fmt.Printf("%s %s: %s\n", style.Bold.Render("🧬 "+molType+":"), status.AttachedMolecule, "")
|
||||
if status.AttachedAt != "" {
|
||||
fmt.Printf(" Attached: %s\n", status.AttachedAt)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("%s\n", style.Dim.Render("No molecule attached"))
|
||||
}
|
||||
|
||||
// Show progress if available
|
||||
if status.Progress != nil {
|
||||
fmt.Println()
|
||||
|
||||
// Progress bar
|
||||
barWidth := 20
|
||||
filled := (status.Progress.Percent * barWidth) / 100
|
||||
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
|
||||
fmt.Printf("Progress: [%s] %d%% (%d/%d steps)\n",
|
||||
bar, status.Progress.Percent, status.Progress.DoneSteps, status.Progress.TotalSteps)
|
||||
|
||||
// Step breakdown
|
||||
fmt.Printf(" Done: %d\n", status.Progress.DoneSteps)
|
||||
fmt.Printf(" In Progress: %d\n", status.Progress.InProgress)
|
||||
fmt.Printf(" Ready: %d", len(status.Progress.ReadySteps))
|
||||
if len(status.Progress.ReadySteps) > 0 && len(status.Progress.ReadySteps) <= 3 {
|
||||
fmt.Printf(" (%s)", strings.Join(status.Progress.ReadySteps, ", "))
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf(" Blocked: %d\n", len(status.Progress.BlockedSteps))
|
||||
|
||||
if status.Progress.Complete {
|
||||
fmt.Printf("\n%s\n", style.Bold.Render("✓ Molecule complete!"))
|
||||
}
|
||||
}
|
||||
|
||||
// Next action hint
|
||||
if status.NextAction != "" {
|
||||
fmt.Printf("\n%s %s\n", style.Bold.Render("Next:"), status.NextAction)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeCurrent(cmd *cobra.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
|
||||
// Find town root
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding workspace: %w", err)
|
||||
}
|
||||
if townRoot == "" {
|
||||
return fmt.Errorf("not in a Gas Town workspace")
|
||||
}
|
||||
|
||||
// Determine target agent identity
|
||||
var target string
|
||||
var roleCtx RoleContext
|
||||
|
||||
if len(args) > 0 {
|
||||
// Explicit target provided
|
||||
target = args[0]
|
||||
} else {
|
||||
// Auto-detect from current directory
|
||||
roleCtx = detectRole(cwd, townRoot)
|
||||
target = buildAgentIdentity(roleCtx)
|
||||
if target == "" {
|
||||
return fmt.Errorf("cannot determine agent identity from current directory (role: %s)", roleCtx.Role)
|
||||
}
|
||||
}
|
||||
|
||||
// Find beads directory
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Extract role from target for handoff bead lookup
|
||||
parts := strings.Split(target, "/")
|
||||
role := parts[len(parts)-1]
|
||||
|
||||
// Find handoff bead for this identity
|
||||
handoff, err := b.FindHandoffBead(role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding handoff bead: %w", err)
|
||||
}
|
||||
|
||||
// Build current info
|
||||
info := MoleculeCurrentInfo{
|
||||
Identity: target,
|
||||
}
|
||||
|
||||
if handoff == nil {
|
||||
info.Status = "naked"
|
||||
return outputMoleculeCurrent(info)
|
||||
}
|
||||
|
||||
info.HandoffID = handoff.ID
|
||||
info.HandoffTitle = handoff.Title
|
||||
|
||||
// Check for attached molecule
|
||||
attachment := beads.ParseAttachmentFields(handoff)
|
||||
if attachment == nil || attachment.AttachedMolecule == "" {
|
||||
info.Status = "naked"
|
||||
return outputMoleculeCurrent(info)
|
||||
}
|
||||
|
||||
info.MoleculeID = attachment.AttachedMolecule
|
||||
|
||||
// Get the molecule root to find its title and children
|
||||
molRoot, err := b.Show(attachment.AttachedMolecule)
|
||||
if err != nil {
|
||||
// Molecule not found - might be a template ID, still report what we have
|
||||
info.Status = "working"
|
||||
return outputMoleculeCurrent(info)
|
||||
}
|
||||
|
||||
info.MoleculeTitle = molRoot.Title
|
||||
|
||||
// Find all children (steps) of the molecule root
|
||||
children, err := b.List(beads.ListOptions{
|
||||
Parent: attachment.AttachedMolecule,
|
||||
Status: "all",
|
||||
Priority: -1,
|
||||
})
|
||||
if err != nil {
|
||||
// No steps - just an issue, not a molecule instance
|
||||
info.Status = "working"
|
||||
return outputMoleculeCurrent(info)
|
||||
}
|
||||
|
||||
info.StepsTotal = len(children)
|
||||
|
||||
// Build set of closed issue IDs for dependency checking
|
||||
closedIDs := make(map[string]bool)
|
||||
var inProgressSteps []*beads.Issue
|
||||
var readySteps []*beads.Issue
|
||||
|
||||
for _, child := range children {
|
||||
switch child.Status {
|
||||
case "closed":
|
||||
info.StepsComplete++
|
||||
closedIDs[child.ID] = true
|
||||
case "in_progress":
|
||||
inProgressSteps = append(inProgressSteps, child)
|
||||
}
|
||||
}
|
||||
|
||||
// Find ready steps (open with all deps closed)
|
||||
for _, child := range children {
|
||||
if child.Status == "open" {
|
||||
allDepsClosed := true
|
||||
for _, depID := range child.DependsOn {
|
||||
if !closedIDs[depID] {
|
||||
allDepsClosed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(child.DependsOn) == 0 || allDepsClosed {
|
||||
readySteps = append(readySteps, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine current step and status
|
||||
if info.StepsComplete == info.StepsTotal && info.StepsTotal > 0 {
|
||||
info.Status = "complete"
|
||||
} else if len(inProgressSteps) > 0 {
|
||||
// First in-progress step is the current one
|
||||
info.Status = "working"
|
||||
info.CurrentStepID = inProgressSteps[0].ID
|
||||
info.CurrentStep = inProgressSteps[0].Title
|
||||
} else if len(readySteps) > 0 {
|
||||
// First ready step is the next to work on
|
||||
info.Status = "working"
|
||||
info.CurrentStepID = readySteps[0].ID
|
||||
info.CurrentStep = readySteps[0].Title
|
||||
} else if info.StepsTotal > 0 {
|
||||
// Has steps but none ready or in-progress -> blocked
|
||||
info.Status = "blocked"
|
||||
} else {
|
||||
info.Status = "working"
|
||||
}
|
||||
|
||||
return outputMoleculeCurrent(info)
|
||||
}
|
||||
|
||||
// outputMoleculeCurrent outputs the current info in the appropriate format.
|
||||
func outputMoleculeCurrent(info MoleculeCurrentInfo) error {
|
||||
if moleculeJSON {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(info)
|
||||
}
|
||||
|
||||
// Human-readable output matching spec format
|
||||
fmt.Printf("Identity: %s\n", info.Identity)
|
||||
|
||||
if info.HandoffID != "" {
|
||||
fmt.Printf("Handoff: %s (%s)\n", info.HandoffID, info.HandoffTitle)
|
||||
} else {
|
||||
fmt.Printf("Handoff: %s\n", style.Dim.Render("(none)"))
|
||||
}
|
||||
|
||||
if info.MoleculeID != "" {
|
||||
if info.MoleculeTitle != "" {
|
||||
fmt.Printf("Molecule: %s (%s)\n", info.MoleculeID, info.MoleculeTitle)
|
||||
} else {
|
||||
fmt.Printf("Molecule: %s\n", info.MoleculeID)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Molecule: %s\n", style.Dim.Render("(none attached)"))
|
||||
}
|
||||
|
||||
if info.StepsTotal > 0 {
|
||||
fmt.Printf("Progress: %d/%d steps complete\n", info.StepsComplete, info.StepsTotal)
|
||||
}
|
||||
|
||||
if info.CurrentStepID != "" {
|
||||
fmt.Printf("Current: %s - %s\n", info.CurrentStepID, info.CurrentStep)
|
||||
} else if info.Status == "naked" {
|
||||
fmt.Printf("Status: %s\n", style.Dim.Render("naked - awaiting work assignment"))
|
||||
} else if info.Status == "complete" {
|
||||
fmt.Printf("Status: %s\n", style.Bold.Render("complete - molecule finished"))
|
||||
} else if info.Status == "blocked" {
|
||||
fmt.Printf("Status: %s\n", style.Dim.Render("blocked - waiting on dependencies"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user