diff --git a/internal/cmd/molecule.go b/internal/cmd/molecule.go index 93f10b02..e684dfa0 100644 --- a/internal/cmd/molecule.go +++ b/internal/cmd/molecule.go @@ -113,6 +113,24 @@ Lists each instantiation with its status and progress.`, RunE: runMoleculeInstances, } +var moleculeProgressCmd = &cobra.Command{ + Use: "progress ", + Short: "Show progress through a molecule's steps", + Long: `Show the execution progress of an instantiated molecule. + +Given a root issue (the parent of molecule steps), displays: +- Total steps and completion status +- Which steps are done, in-progress, ready, or blocked +- Overall progress percentage + +This is useful for the Witness to monitor molecule execution. + +Example: + gt molecule progress gt-abc`, + Args: cobra.ExactArgs(1), + RunE: runMoleculeProgress, +} + func init() { // List flags moleculeListCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON") @@ -133,6 +151,9 @@ func init() { // Instances flags moleculeInstancesCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON") + // Progress flags + moleculeProgressCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON") + // Add subcommands moleculeCmd.AddCommand(moleculeListCmd) moleculeCmd.AddCommand(moleculeShowCmd) @@ -140,6 +161,7 @@ func init() { moleculeCmd.AddCommand(moleculeInstantiateCmd) moleculeCmd.AddCommand(moleculeInstancesCmd) moleculeCmd.AddCommand(moleculeExportCmd) + moleculeCmd.AddCommand(moleculeProgressCmd) rootCmd.AddCommand(moleculeCmd) } @@ -647,3 +669,152 @@ func findMoleculeInstances(b *beads.Beads, molID string) ([]*beads.Issue, error) return parents, nil } + +// 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"` +} + +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 "" +} diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index 144f4163..7a3869fb 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -82,6 +82,9 @@ func runPrime(cmd *cobra.Command, args []string) error { // Output handoff content if present outputHandoffContent(ctx) + // Output molecule context if working on a molecule step + outputMoleculeContext(ctx) + // Run bd prime to output beads workflow context runBdPrime(cwd) @@ -464,3 +467,116 @@ func runMailCheckInject(workDir string) { fmt.Println(output) } } + +// outputMoleculeContext checks if the agent is working on a molecule step and shows progress. +func outputMoleculeContext(ctx RoleContext) { + // Only applies to polecats and crew workers + if ctx.Role != RolePolecat && ctx.Role != RoleCrew { + return + } + + // Check for in-progress issues + b := beads.New(ctx.WorkDir) + issues, err := b.List(beads.ListOptions{ + Status: "in_progress", + Assignee: ctx.Polecat, + Priority: -1, + }) + if err != nil || len(issues) == 0 { + return + } + + // Check if any in-progress issue is a molecule step + for _, issue := range issues { + moleculeID := parseMoleculeMetadata(issue.Description) + if moleculeID == "" { + continue + } + + // Get the parent (root) issue ID + rootID := issue.Parent + if rootID == "" { + continue + } + + // This is a molecule step - show context + fmt.Println() + fmt.Printf("%s\n\n", style.Bold.Render("## 🧬 Molecule Workflow")) + fmt.Printf("You are working on a molecule step.\n") + fmt.Printf(" Current step: %s\n", issue.ID) + fmt.Printf(" Molecule: %s\n", moleculeID) + fmt.Printf(" Root issue: %s\n\n", rootID) + + // Show molecule progress by finding sibling steps + showMoleculeProgress(b, rootID) + + fmt.Println() + fmt.Println("**Molecule Work Loop:**") + fmt.Println("1. Complete current step, then `bd close " + issue.ID + "`") + fmt.Println("2. Check for next steps: `bd ready --parent " + rootID + "`") + fmt.Println("3. Work on next ready step(s)") + fmt.Println("4. When all steps done, run `gt done`") + break // Only show context for first molecule step found + } +} + +// parseMoleculeMetadata extracts molecule info from a step's description. +// Looks for lines like: +// +// instantiated_from: mol-xyz +func parseMoleculeMetadata(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 "" +} + +// showMoleculeProgress displays the progress through a molecule's steps. +func showMoleculeProgress(b *beads.Beads, rootID string) { + if rootID == "" { + return + } + + // Find all children of the root issue + children, err := b.List(beads.ListOptions{ + Parent: rootID, + Status: "all", + Priority: -1, + }) + if err != nil || len(children) == 0 { + return + } + + total := len(children) + done := 0 + inProgress := 0 + var readySteps []string + + for _, child := range children { + switch child.Status { + case "closed": + done++ + case "in_progress": + inProgress++ + case "open": + // Check if ready (no open dependencies) + if len(child.DependsOn) == 0 { + readySteps = append(readySteps, child.ID) + } + } + } + + fmt.Printf("Progress: %d/%d steps complete", done, total) + if inProgress > 0 { + fmt.Printf(" (%d in progress)", inProgress) + } + fmt.Println() + + if len(readySteps) > 0 { + fmt.Printf("Ready steps: %s\n", strings.Join(readySteps, ", ")) + } +} diff --git a/internal/cmd/spawn.go b/internal/cmd/spawn.go index 2a901b75..37fd2d4a 100644 --- a/internal/cmd/spawn.go +++ b/internal/cmd/spawn.go @@ -242,6 +242,9 @@ func runSpawn(cmd *cobra.Command, args []string) error { fmt.Printf("%s beads sync: %v\n", style.Dim.Render("Warning:"), err) } + // Track molecule context for work assignment mail + var moleculeCtx *MoleculeContext + // Handle molecule instantiation if specified if spawnMolecule != "" { b := beads.New(beadsPath) @@ -281,9 +284,11 @@ func runSpawn(cmd *cobra.Command, args []string) error { // Find the first ready step (one with no dependencies) var firstReadyStep *beads.Issue - for _, step := range steps { + var stepNumber int + for i, step := range steps { if len(step.DependsOn) == 0 { firstReadyStep = step + stepNumber = i + 1 break } } @@ -292,6 +297,14 @@ func runSpawn(cmd *cobra.Command, args []string) error { return fmt.Errorf("no ready step found in molecule (all steps have dependencies)") } + // Build molecule context for work assignment + moleculeCtx = &MoleculeContext{ + MoleculeID: spawnMolecule, + RootIssueID: spawnIssue, // Original issue is the molecule root + TotalSteps: len(steps), + StepNumber: stepNumber, + } + // Switch to spawning on the first ready step fmt.Printf("\nSpawning on first ready step: %s\n", firstReadyStep.ID) spawnIssue = firstReadyStep.ID @@ -340,7 +353,7 @@ func runSpawn(cmd *cobra.Command, args []string) error { } // Send work assignment mail to polecat inbox (before starting session) - workMsg := buildWorkAssignmentMail(issue, spawnMessage, polecatAddress) + workMsg := buildWorkAssignmentMail(issue, spawnMessage, polecatAddress, moleculeCtx) fmt.Printf("Sending work assignment to %s inbox...\n", polecatAddress) if err := router.Send(workMsg); err != nil { @@ -579,14 +592,27 @@ func buildSpawnContext(issue *BeadsIssue, message string) string { return sb.String() } +// MoleculeContext contains information about a molecule workflow assignment. +type MoleculeContext struct { + MoleculeID string // The molecule template ID + RootIssueID string // The parent issue (molecule root) + TotalSteps int // Total number of steps in the molecule + StepNumber int // Which step this is (1-indexed) +} + // buildWorkAssignmentMail creates a work assignment mail message for a polecat. // This replaces tmux-based context injection with persistent mailbox delivery. -func buildWorkAssignmentMail(issue *BeadsIssue, message, polecatAddress string) *mail.Message { +// If moleculeCtx is non-nil, includes molecule workflow instructions. +func buildWorkAssignmentMail(issue *BeadsIssue, message, polecatAddress string, moleculeCtx *MoleculeContext) *mail.Message { var subject string var body strings.Builder if issue != nil { - subject = fmt.Sprintf("📋 Work Assignment: %s", issue.Title) + if moleculeCtx != nil { + subject = fmt.Sprintf("🧬 Molecule Step %d/%d: %s", moleculeCtx.StepNumber, moleculeCtx.TotalSteps, issue.Title) + } else { + subject = fmt.Sprintf("📋 Work Assignment: %s", issue.Title) + } body.WriteString(fmt.Sprintf("Issue: %s\n", issue.ID)) body.WriteString(fmt.Sprintf("Title: %s\n", issue.Title)) @@ -605,14 +631,32 @@ func buildWorkAssignmentMail(issue *BeadsIssue, message, polecatAddress string) body.WriteString(fmt.Sprintf("Task: %s\n", message)) } + // Add molecule context if present + if moleculeCtx != nil { + body.WriteString("\n## Molecule Workflow\n") + body.WriteString(fmt.Sprintf("You are working on step %d of %d in molecule %s.\n", moleculeCtx.StepNumber, moleculeCtx.TotalSteps, moleculeCtx.MoleculeID)) + body.WriteString(fmt.Sprintf("Root issue: %s\n\n", moleculeCtx.RootIssueID)) + body.WriteString("**IMPORTANT**: This is part of a molecule workflow. After completing this step:\n") + body.WriteString("1. Run `bd close " + issue.ID + "`\n") + body.WriteString("2. Run `bd ready --parent " + moleculeCtx.RootIssueID + "` to find next ready steps\n") + body.WriteString("3. If more steps are ready, continue working on them\n") + body.WriteString("4. When all steps are done, run `gt done` to signal completion\n\n") + } + body.WriteString("\n## Workflow\n") body.WriteString("1. Run `gt prime` to load polecat context\n") body.WriteString("2. Run `bd sync --from-main` to get fresh beads\n") body.WriteString("3. Work on your task, commit changes regularly\n") body.WriteString("4. Run `bd close ` when done\n") - body.WriteString("5. Run `bd sync` to push beads changes\n") - body.WriteString("6. Push code: `git push origin HEAD`\n") - body.WriteString("7. Run `gt done` to signal completion\n") + if moleculeCtx != nil { + body.WriteString("5. Check `bd ready --parent " + moleculeCtx.RootIssueID + "` for more steps\n") + body.WriteString("6. Repeat steps 3-5 for each ready step\n") + body.WriteString("7. When all steps done: run `bd sync`, push code, run `gt done`\n") + } else { + body.WriteString("5. Run `bd sync` to push beads changes\n") + body.WriteString("6. Push code: `git push origin HEAD`\n") + body.WriteString("7. Run `gt done` to signal completion\n") + } body.WriteString("\n## Handoff Protocol\n") body.WriteString("Before signaling done, ensure:\n") body.WriteString("- Git status is clean (no uncommitted changes)\n")