Implement gt mol status command (gt-uym5)
Add `gt mol status [target]` command to show what's on an agent's hook: - Auto-detects current agent from working directory - Shows pinned bead and attached molecule info - Displays phase progress with progress bar - Indicates wisp vs durable molecules - Provides next action hints Also adds `mol` alias for the `molecule` command. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,8 +23,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var moleculeCmd = &cobra.Command{
|
var moleculeCmd = &cobra.Command{
|
||||||
Use: "molecule",
|
Use: "molecule",
|
||||||
Short: "Molecule workflow commands",
|
Aliases: []string{"mol"},
|
||||||
|
Short: "Molecule workflow commands",
|
||||||
Long: `Manage molecule workflow templates.
|
Long: `Manage molecule workflow templates.
|
||||||
|
|
||||||
Molecules are composable workflow patterns stored as beads issues.
|
Molecules are composable workflow patterns stored as beads issues.
|
||||||
@@ -169,6 +170,28 @@ Example:
|
|||||||
RunE: runMoleculeAttachment,
|
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() {
|
func init() {
|
||||||
// List flags
|
// List flags
|
||||||
moleculeListCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
moleculeListCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||||
@@ -195,7 +218,11 @@ func init() {
|
|||||||
// Attachment flags
|
// Attachment flags
|
||||||
moleculeAttachmentCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
moleculeAttachmentCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
|
// Status flags
|
||||||
|
moleculeStatusCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
|
moleculeCmd.AddCommand(moleculeStatusCmd)
|
||||||
moleculeCmd.AddCommand(moleculeListCmd)
|
moleculeCmd.AddCommand(moleculeListCmd)
|
||||||
moleculeCmd.AddCommand(moleculeShowCmd)
|
moleculeCmd.AddCommand(moleculeShowCmd)
|
||||||
moleculeCmd.AddCommand(moleculeParseCmd)
|
moleculeCmd.AddCommand(moleculeParseCmd)
|
||||||
@@ -979,3 +1006,307 @@ func runMoleculeAttachment(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
return nil
|
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