Files
gastown/internal/cmd/prime_molecule.go
scout/crew/picard 0be6a39bb7 fix: enforce fresh context between molecule steps
Change molecule step completion instructions to use `gt mol step done`
instead of `bd close`. This ensures polecats get fresh context between
each step, which is critical for multi-step review workflows like
shiny-enterprise where each refinement pass should have unbiased attention.

The `gt mol step done` command already:
1. Closes the step
2. Finds the next ready step
3. Respawns the pane for fresh context

But polecats were being instructed to use `bd close` directly, which
skipped the respawn and let them run through entire workflows in a
single session with accumulated context.

Updated:
- prime_molecule.go: step completion instructions
- mol-polecat-work.formula.toml
- mol-polecat-code-review.formula.toml
- mol-polecat-review-pr.formula.toml

Fixes: hq-0kx7ra
2026-01-25 11:15:29 -08:00

309 lines
10 KiB
Go

package cmd
import (
"bytes"
"encoding/json"
"fmt"
"os/exec"
"strings"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/constants"
"github.com/steveyegge/gastown/internal/deacon"
"github.com/steveyegge/gastown/internal/style"
)
// MoleculeCurrentOutput represents the JSON output of bd mol current.
type MoleculeCurrentOutput struct {
MoleculeID string `json:"molecule_id"`
MoleculeTitle string `json:"molecule_title"`
NextStep *struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
} `json:"next_step"`
Completed int `json:"completed"`
Total int `json:"total"`
}
// showMoleculeExecutionPrompt calls bd mol current and shows the current step
// with execution instructions. This is the core of the Propulsion Principle.
func showMoleculeExecutionPrompt(workDir, moleculeID string) {
// Call bd mol current with JSON output
cmd := exec.Command("bd", "--no-daemon", "mol", "current", moleculeID, "--json")
cmd.Dir = workDir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
// Fall back to simple message if bd mol current fails
fmt.Println(style.Bold.Render("→ PROPULSION PRINCIPLE: Work is on your hook. RUN IT."))
fmt.Println(" Begin working on this molecule immediately.")
fmt.Printf(" Check status with: bd mol current %s\n", moleculeID)
return
}
// Handle bd --no-daemon exit 0 bug: empty stdout means not found
if stdout.Len() == 0 {
fmt.Println(style.Bold.Render("→ PROPULSION PRINCIPLE: Work is on your hook. RUN IT."))
fmt.Println(" Begin working on this molecule immediately.")
return
}
// Parse JSON output - it's an array with one element
var outputs []MoleculeCurrentOutput
if err := json.Unmarshal(stdout.Bytes(), &outputs); err != nil || len(outputs) == 0 {
// Fall back to simple message
fmt.Println(style.Bold.Render("→ PROPULSION PRINCIPLE: Work is on your hook. RUN IT."))
fmt.Println(" Begin working on this molecule immediately.")
return
}
output := outputs[0]
// Show molecule progress
fmt.Printf("**Progress:** %d/%d steps complete\n\n",
output.Completed, output.Total)
// Show current step if available
if output.NextStep != nil {
step := output.NextStep
fmt.Printf("%s\n\n", style.Bold.Render("## 🎬 CURRENT STEP: "+step.Title))
fmt.Printf("**Step ID:** %s\n", step.ID)
fmt.Printf("**Status:** %s (ready to execute)\n\n", step.Status)
// Show step description if available
if step.Description != "" {
fmt.Println("### Instructions")
fmt.Println()
// Indent the description for readability
lines := strings.Split(step.Description, "\n")
for _, line := range lines {
fmt.Printf("%s\n", line)
}
fmt.Println()
}
// The propulsion directive
fmt.Println(style.Bold.Render("→ EXECUTE THIS STEP NOW."))
fmt.Println()
fmt.Println("When complete:")
fmt.Printf(" gt mol step done %s\n", step.ID)
fmt.Println()
fmt.Println("This closes the step and respawns your session with fresh context for the next step.")
} else {
// No next step - molecule may be complete
fmt.Println(style.Bold.Render("✓ MOLECULE COMPLETE"))
fmt.Println()
fmt.Println("All steps are done. You may:")
fmt.Println(" - Report completion to supervisor")
fmt.Println(" - Check for new work: bd ready")
}
}
// outputMoleculeContext checks if the agent is working on a molecule step and shows progress.
func outputMoleculeContext(ctx RoleContext) {
// Applies to polecats, crew workers, deacon, witness, and refinery
if ctx.Role != RolePolecat && ctx.Role != RoleCrew && ctx.Role != RoleDeacon && ctx.Role != RoleWitness && ctx.Role != RoleRefinery {
return
}
// For Deacon, use special patrol molecule handling
if ctx.Role == RoleDeacon {
outputDeaconPatrolContext(ctx)
return
}
// For Witness, use special patrol molecule handling (auto-bonds on startup)
if ctx.Role == RoleWitness {
outputWitnessPatrolContext(ctx)
return
}
// For Refinery, use special patrol molecule handling (auto-bonds on startup)
if ctx.Role == RoleRefinery {
outputRefineryPatrolContext(ctx)
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("**When step complete:**")
fmt.Println(" `gt mol step done " + issue.ID + "`")
fmt.Println()
fmt.Println("This closes the step and respawns with fresh context for the next step.")
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, ", "))
}
}
// outputDeaconPatrolContext shows patrol molecule status for the Deacon.
// Deacon uses wisps (Wisp:true issues in main .beads/) for patrol cycles.
// Deacon is a town-level role, so it uses town root beads (not rig beads).
func outputDeaconPatrolContext(ctx RoleContext) {
// Check if Deacon is paused - if so, output PAUSED message and skip patrol context
paused, state, err := deacon.IsPaused(ctx.TownRoot)
if err == nil && paused {
outputDeaconPausedMessage(state)
return
}
cfg := PatrolConfig{
RoleName: "deacon",
PatrolMolName: "mol-deacon-patrol",
BeadsDir: ctx.TownRoot, // Town-level role uses town root beads
Assignee: "deacon",
HeaderEmoji: "🔄",
HeaderTitle: "Patrol Status (Wisp-based)",
CheckInProgress: false,
WorkLoopSteps: []string{
"Check next step: `bd ready`",
"Execute the step (heartbeat, mail, health checks, etc.)",
"Close step: `bd close <step-id>`",
"Check next: `bd ready`",
"At cycle end (loop-or-exit step):\n - If context LOW:\n * Squash: `bd mol squash <mol-id> --summary \"<summary>\"`\n * Create new patrol: `bd mol wisp mol-deacon-patrol`\n * Continue executing from inbox-check step\n - If context HIGH:\n * Send handoff: `gt handoff -s \"Deacon patrol\" -m \"<observations>\"`\n * Exit cleanly (daemon respawns fresh session)",
},
}
outputPatrolContext(cfg)
}
// outputWitnessPatrolContext shows patrol molecule status for the Witness.
// Witness AUTO-BONDS its patrol molecule on startup if one isn't already running.
func outputWitnessPatrolContext(ctx RoleContext) {
cfg := PatrolConfig{
RoleName: "witness",
PatrolMolName: "mol-witness-patrol",
BeadsDir: ctx.WorkDir,
Assignee: ctx.Rig + "/witness",
HeaderEmoji: constants.EmojiWitness,
HeaderTitle: "Witness Patrol Status",
CheckInProgress: true,
WorkLoopSteps: []string{
"Check inbox: `gt mail inbox`",
"Check next step: `bd ready`",
"Execute the step (survey polecats, inspect, nudge, etc.)",
"Close step: `bd close <step-id>`",
"Check next: `bd ready`",
"At cycle end (loop-or-exit step):\n - If context LOW:\n * Squash: `bd mol squash <mol-id> --summary \"<summary>\"`\n * Create new patrol: `bd mol wisp mol-witness-patrol`\n * Continue executing from inbox-check step\n - If context HIGH:\n * Send handoff: `gt handoff -s \"Witness patrol\" -m \"<observations>\"`\n * Exit cleanly (daemon respawns fresh session)",
},
}
outputPatrolContext(cfg)
}
// outputRefineryPatrolContext shows patrol molecule status for the Refinery.
// Refinery AUTO-BONDS its patrol molecule on startup if one isn't already running.
func outputRefineryPatrolContext(ctx RoleContext) {
cfg := PatrolConfig{
RoleName: "refinery",
PatrolMolName: "mol-refinery-patrol",
BeadsDir: ctx.WorkDir,
Assignee: ctx.Rig + "/refinery",
HeaderEmoji: "🔧",
HeaderTitle: "Refinery Patrol Status",
CheckInProgress: true,
WorkLoopSteps: []string{
"Check inbox: `gt mail inbox`",
"Check next step: `bd ready`",
"Execute the step (queue scan, process branch, tests, merge)",
"Close step: `bd close <step-id>`",
"Check next: `bd ready`",
"At cycle end (loop-or-exit step):\n - If context LOW:\n * Squash: `bd mol squash <mol-id> --summary \"<summary>\"`\n * Create new patrol: `bd mol wisp mol-refinery-patrol`\n * Continue executing from inbox-check step\n - If context HIGH:\n * Send handoff: `gt handoff -s \"Refinery patrol\" -m \"<observations>\"`\n * Exit cleanly (daemon respawns fresh session)",
},
}
outputPatrolContext(cfg)
}