gt-975: Molecule execution support for polecats and crew
Added molecule workflow integration to Gas Town: 1. spawn.go: MoleculeContext in work assignment mail - Shows step N/M and molecule ID in subject - Includes molecule workflow instructions - Guides polecat through DAG execution 2. prime.go: outputMoleculeContext() - Detects if in-progress issue is a molecule step - Shows molecule progress and next steps - Displays molecule work loop instructions 3. molecule.go: 'gt molecule progress' command - Shows execution progress for molecule root - Displays done/in-progress/ready/blocked steps - Progress bar and completion percentage - JSON output for Witness automation This enables polecats to work through molecule DAGs: - Receive molecule-aware work assignments - See context in gt prime output - Follow DAG with 'bd ready --parent <root>' - Witness can monitor with 'gt molecule progress' 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,24 @@ Lists each instantiation with its status and progress.`,
|
||||
RunE: runMoleculeInstances,
|
||||
}
|
||||
|
||||
var moleculeProgressCmd = &cobra.Command{
|
||||
Use: "progress <root-issue-id>",
|
||||
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 ""
|
||||
}
|
||||
|
||||
@@ -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, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <issue-id>` 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")
|
||||
|
||||
Reference in New Issue
Block a user