Merge polecat/nux: Implement gt mol status command (gt-uym5)
This commit is contained in:
@@ -23,8 +23,9 @@ var (
|
||||
)
|
||||
|
||||
var moleculeCmd = &cobra.Command{
|
||||
Use: "molecule",
|
||||
Short: "Molecule workflow commands",
|
||||
Use: "molecule",
|
||||
Aliases: []string{"mol"},
|
||||
Short: "Molecule workflow commands",
|
||||
Long: `Manage molecule workflow templates.
|
||||
|
||||
Molecules are composable workflow patterns stored as beads issues.
|
||||
@@ -169,6 +170,28 @@ Example:
|
||||
RunE: runMoleculeAttachment,
|
||||
}
|
||||
|
||||
var moleculeStatusCmd = &cobra.Command{
|
||||
Use: "status [target]",
|
||||
Short: "Show what's on an agent's hook",
|
||||
Long: `Show what's slung on an agent's hook.
|
||||
|
||||
If no target is specified, shows the current agent's status based on
|
||||
the working directory (polecat, crew member, witness, etc.).
|
||||
|
||||
Output includes:
|
||||
- What's slung (molecule name, associated issue)
|
||||
- Current phase and progress
|
||||
- Whether it's a wisp
|
||||
- Next action hint
|
||||
|
||||
Examples:
|
||||
gt mol status # Show current agent's hook
|
||||
gt mol status gastown/nux # Show specific polecat's hook
|
||||
gt mol status gastown/witness # Show witness's hook`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runMoleculeStatus,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// List flags
|
||||
moleculeListCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||
@@ -195,7 +218,11 @@ func init() {
|
||||
// Attachment flags
|
||||
moleculeAttachmentCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Status flags
|
||||
moleculeStatusCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Add subcommands
|
||||
moleculeCmd.AddCommand(moleculeStatusCmd)
|
||||
moleculeCmd.AddCommand(moleculeListCmd)
|
||||
moleculeCmd.AddCommand(moleculeShowCmd)
|
||||
moleculeCmd.AddCommand(moleculeParseCmd)
|
||||
@@ -979,3 +1006,307 @@ func runMoleculeAttachment(cmd *cobra.Command, args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user