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:
Steve Yegge
2025-12-21 12:02:58 -08:00
parent beb4efc4f9
commit 308a7bc190
3 changed files with 338 additions and 7 deletions

View File

@@ -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 ""
}

View File

@@ -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, ", "))
}
}

View File

@@ -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")