package cmd import ( "bytes" "fmt" "os" "os/exec" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/templates" "github.com/steveyegge/gastown/internal/workspace" ) // Role represents a detected agent role. type Role string const ( RoleMayor Role = "mayor" RoleDeacon Role = "deacon" RoleWitness Role = "witness" RoleRefinery Role = "refinery" RolePolecat Role = "polecat" RoleCrew Role = "crew" RoleUnknown Role = "unknown" ) var primeCmd = &cobra.Command{ Use: "prime", Short: "Output role context for current directory", Long: `Detect the agent role from the current directory and output context. Role detection: - Town root or mayor/rig/ → Mayor context - /witness/rig/ → Witness context - /refinery/rig/ → Refinery context - /polecats// → Polecat context This command is typically used in shell prompts or agent initialization.`, RunE: runPrime, } func init() { rootCmd.AddCommand(primeCmd) } // RoleContext contains information about the detected role. type RoleContext struct { Role Role `json:"role"` Rig string `json:"rig,omitempty"` Polecat string `json:"polecat,omitempty"` TownRoot string `json:"town_root"` WorkDir string `json:"work_dir"` } func runPrime(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") } // Detect role ctx := detectRole(cwd, townRoot) // Output context if err := outputPrimeContext(ctx); err != nil { return err } // 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) // Run gt mail check --inject to inject any pending mail runMailCheckInject(cwd) // Output startup directive for roles that should announce themselves outputStartupDirective(ctx) return nil } func detectRole(cwd, townRoot string) RoleContext { ctx := RoleContext{ Role: RoleUnknown, TownRoot: townRoot, WorkDir: cwd, } // Get relative path from town root relPath, err := filepath.Rel(townRoot, cwd) if err != nil { return ctx } // Normalize and split path relPath = filepath.ToSlash(relPath) parts := strings.Split(relPath, "/") // Check for mayor role // At town root, or in mayor/ or mayor/rig/ if relPath == "." || relPath == "" { ctx.Role = RoleMayor return ctx } if len(parts) >= 1 && parts[0] == "mayor" { ctx.Role = RoleMayor return ctx } // Check for deacon role: deacon/ if len(parts) >= 1 && parts[0] == "deacon" { ctx.Role = RoleDeacon return ctx } // At this point, first part should be a rig name if len(parts) < 1 { return ctx } rigName := parts[0] ctx.Rig = rigName // Check for witness: /witness/rig/ if len(parts) >= 2 && parts[1] == "witness" { ctx.Role = RoleWitness return ctx } // Check for refinery: /refinery/rig/ if len(parts) >= 2 && parts[1] == "refinery" { ctx.Role = RoleRefinery return ctx } // Check for polecat: /polecats// if len(parts) >= 3 && parts[1] == "polecats" { ctx.Role = RolePolecat ctx.Polecat = parts[2] return ctx } // Check for crew: /crew// if len(parts) >= 3 && parts[1] == "crew" { ctx.Role = RoleCrew ctx.Polecat = parts[2] // Use Polecat field for crew member name return ctx } // Default: could be rig root - treat as unknown return ctx } func outputPrimeContext(ctx RoleContext) error { // Try to use templates first tmpl, err := templates.New() if err != nil { // Fall back to hardcoded output if templates fail return outputPrimeContextFallback(ctx) } // Map role to template name var roleName string switch ctx.Role { case RoleMayor: roleName = "mayor" case RoleDeacon: roleName = "deacon" case RoleWitness: roleName = "witness" case RoleRefinery: roleName = "refinery" case RolePolecat: roleName = "polecat" case RoleCrew: roleName = "crew" default: // Unknown role - use fallback return outputPrimeContextFallback(ctx) } // Build template data data := templates.RoleData{ Role: roleName, RigName: ctx.Rig, TownRoot: ctx.TownRoot, WorkDir: ctx.WorkDir, Polecat: ctx.Polecat, } // Render and output output, err := tmpl.RenderRole(roleName, data) if err != nil { return fmt.Errorf("rendering template: %w", err) } fmt.Print(output) return nil } func outputPrimeContextFallback(ctx RoleContext) error { switch ctx.Role { case RoleMayor: outputMayorContext(ctx) case RoleWitness: outputWitnessContext(ctx) case RoleRefinery: outputRefineryContext(ctx) case RolePolecat: outputPolecatContext(ctx) case RoleCrew: outputCrewContext(ctx) default: outputUnknownContext(ctx) } return nil } func outputMayorContext(ctx RoleContext) { fmt.Printf("%s\n\n", style.Bold.Render("# Mayor Context")) fmt.Println("You are the **Mayor** - the global coordinator of Gas Town.") fmt.Println() fmt.Println("## Responsibilities") fmt.Println("- Coordinate work across all rigs") fmt.Println("- Delegate to Refineries, not directly to polecats") fmt.Println("- Monitor overall system health") fmt.Println() fmt.Println("## Key Commands") fmt.Println("- `gt mail inbox` - Check your messages") fmt.Println("- `gt mail read ` - Read a specific message") fmt.Println("- `gt status` - Show overall town status") fmt.Println("- `gt rigs` - List all rigs") fmt.Println("- `bd ready` - Issues ready to work") fmt.Println() fmt.Println("## Startup") fmt.Println("Check for handoff messages with 🤝 HANDOFF in subject - continue predecessor's work.") fmt.Println() fmt.Printf("Town root: %s\n", style.Dim.Render(ctx.TownRoot)) } func outputWitnessContext(ctx RoleContext) { fmt.Printf("%s\n\n", style.Bold.Render("# Witness Context")) fmt.Printf("You are the **Witness** for rig: %s\n\n", style.Bold.Render(ctx.Rig)) fmt.Println("## Responsibilities") fmt.Println("- Monitor polecat health via heartbeat") fmt.Println("- Spawn ephemeral agents for stuck polecats") fmt.Println("- Report rig status to Mayor") fmt.Println() fmt.Println("## Key Commands") fmt.Println("- `gt witness status` - Show witness status") fmt.Println("- `gt polecats` - List polecats in this rig") fmt.Println() fmt.Printf("Rig: %s\n", style.Dim.Render(ctx.Rig)) } func outputRefineryContext(ctx RoleContext) { fmt.Printf("%s\n\n", style.Bold.Render("# Refinery Context")) fmt.Printf("You are the **Refinery** for rig: %s\n\n", style.Bold.Render(ctx.Rig)) fmt.Println("## Responsibilities") fmt.Println("- Process the merge queue for this rig") fmt.Println("- Merge polecat work to integration branch") fmt.Println("- Resolve merge conflicts") fmt.Println("- Land completed swarms to main") fmt.Println() fmt.Println("## Key Commands") fmt.Println("- `gt merge queue` - Show pending merges") fmt.Println("- `gt merge next` - Process next merge") fmt.Println() fmt.Printf("Rig: %s\n", style.Dim.Render(ctx.Rig)) } func outputPolecatContext(ctx RoleContext) { fmt.Printf("%s\n\n", style.Bold.Render("# Polecat Context")) fmt.Printf("You are polecat **%s** in rig: %s\n\n", style.Bold.Render(ctx.Polecat), style.Bold.Render(ctx.Rig)) fmt.Println("## Startup Protocol") fmt.Println("1. Run `gt prime` - loads context and checks mail automatically") fmt.Println("2. Check inbox - if mail shown, read with `gt mail read `") fmt.Println("3. Look for '📋 Work Assignment' messages for your task") fmt.Println("4. If no mail, check `bd list --status=in_progress` for existing work") fmt.Println() fmt.Println("## Key Commands") fmt.Println("- `gt mail inbox` - Check your inbox for work assignments") fmt.Println("- `bd show ` - View your assigned issue") fmt.Println("- `bd close ` - Mark issue complete") fmt.Println("- `gt done` - Signal work ready for merge") fmt.Println() fmt.Printf("Polecat: %s | Rig: %s\n", style.Dim.Render(ctx.Polecat), style.Dim.Render(ctx.Rig)) } func outputCrewContext(ctx RoleContext) { fmt.Printf("%s\n\n", style.Bold.Render("# Crew Worker Context")) fmt.Printf("You are crew worker **%s** in rig: %s\n\n", style.Bold.Render(ctx.Polecat), style.Bold.Render(ctx.Rig)) fmt.Println("## About Crew Workers") fmt.Println("- Persistent workspace (not auto-garbage-collected)") fmt.Println("- User-managed (not Witness-monitored)") fmt.Println("- Long-lived identity across sessions") fmt.Println() fmt.Println("## Key Commands") fmt.Println("- `gt mail inbox` - Check your inbox") fmt.Println("- `bd ready` - Available issues") fmt.Println("- `bd show ` - View issue details") fmt.Println("- `bd close ` - Mark issue complete") fmt.Println() fmt.Printf("Crew: %s | Rig: %s\n", style.Dim.Render(ctx.Polecat), style.Dim.Render(ctx.Rig)) } func outputUnknownContext(ctx RoleContext) { fmt.Printf("%s\n\n", style.Bold.Render("# Gas Town Context")) fmt.Println("Could not determine specific role from current directory.") fmt.Println() if ctx.Rig != "" { fmt.Printf("You appear to be in rig: %s\n\n", style.Bold.Render(ctx.Rig)) } fmt.Println("Navigate to a specific agent directory:") fmt.Println("- `/polecats//` - Polecat role") fmt.Println("- `/witness/rig/` - Witness role") fmt.Println("- `/refinery/rig/` - Refinery role") fmt.Println("- Town root or `mayor/` - Mayor role") fmt.Println() fmt.Printf("Town root: %s\n", style.Dim.Render(ctx.TownRoot)) } // outputHandoffContent reads and displays the pinned handoff bead for the role. func outputHandoffContent(ctx RoleContext) { if ctx.Role == RoleUnknown { return } // Get role key for handoff bead lookup roleKey := string(ctx.Role) bd := beads.New(ctx.TownRoot) issue, err := bd.FindHandoffBead(roleKey) if err != nil { // Silently skip if beads lookup fails (might not be a beads repo) return } if issue == nil || issue.Description == "" { // No handoff content return } // Display handoff content fmt.Println() fmt.Printf("%s\n\n", style.Bold.Render("## 🤝 Handoff from Previous Session")) fmt.Println(issue.Description) fmt.Println() fmt.Println(style.Dim.Render("(Clear with: gt rig reset --handoff)")) } // runBdPrime runs `bd prime` and outputs the result. // This provides beads workflow context to the agent. func runBdPrime(workDir string) { cmd := exec.Command("bd", "prime") cmd.Dir = workDir var stdout bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = nil // Ignore stderr if err := cmd.Run(); err != nil { // Silently skip if bd prime fails (beads might not be available) return } output := strings.TrimSpace(stdout.String()) if output != "" { fmt.Println() fmt.Println(output) } } // outputStartupDirective outputs role-specific instructions for the agent. // This tells agents like Mayor to announce themselves on startup. func outputStartupDirective(ctx RoleContext) { switch ctx.Role { case RoleMayor: fmt.Println() fmt.Println("---") fmt.Println() fmt.Println("**STARTUP PROTOCOL**: You are the Mayor. Please:") fmt.Println("1. Announce: \"Mayor, checking in.\"") fmt.Println("2. Check mail: `gt mail inbox`") fmt.Println("3. If there's a 🤝 HANDOFF message, read it and summarize") fmt.Println("4. If no mail, await user instruction") case RoleWitness: fmt.Println() fmt.Println("---") fmt.Println() fmt.Println("**STARTUP PROTOCOL**: You are the Witness. Please:") fmt.Println("1. Announce: \"Witness, checking in.\"") fmt.Println("2. Check for handoff: `gt mail inbox` - look for 🤝 HANDOFF messages") fmt.Println("3. Check polecat status: `gt polecat list " + ctx.Rig + " --json`") fmt.Println("4. Process any lifecycle requests from inbox") fmt.Println("5. If polecats stuck/idle, nudge them") fmt.Println("6. If all quiet, wait for activity") case RolePolecat: fmt.Println() fmt.Println("---") fmt.Println() fmt.Println("**STARTUP PROTOCOL**: You are a polecat. Please:") fmt.Printf("1. Announce: \"%s Polecat %s, checking in.\"\n", ctx.Rig, ctx.Polecat) fmt.Println("2. Check mail: `gt mail inbox`") fmt.Println("3. If assigned work, begin immediately") fmt.Println("4. If no work, announce ready and await assignment") case RoleRefinery: fmt.Println() fmt.Println("---") fmt.Println() fmt.Println("**STARTUP PROTOCOL**: You are the Refinery. Please:") fmt.Println("1. Announce: \"Refinery, checking in.\"") fmt.Println("2. Check mail: `gt mail inbox`") fmt.Printf("3. Check merge queue: `gt refinery queue %s`\n", ctx.Rig) fmt.Println("4. If MRs pending, process them one at a time") fmt.Println("5. If no work, monitor for new MRs periodically") case RoleCrew: fmt.Println() fmt.Println("---") fmt.Println() fmt.Println("**STARTUP PROTOCOL**: You are a crew worker. Please:") fmt.Printf("1. Announce: \"%s Crew %s, checking in.\"\n", ctx.Rig, ctx.Polecat) fmt.Println("2. Check mail: `gt mail inbox`") fmt.Println("3. If there's a 🤝 HANDOFF message, read it and continue the work") fmt.Println("4. If no mail, await user instruction") } } // runMailCheckInject runs `gt mail check --inject` and outputs the result. // This injects any pending mail into the agent's context. func runMailCheckInject(workDir string) { cmd := exec.Command("gt", "mail", "check", "--inject") cmd.Dir = workDir var stdout bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = nil // Ignore stderr if err := cmd.Run(); err != nil { // Silently skip if mail check fails return } output := strings.TrimSpace(stdout.String()) if output != "" { fmt.Println() 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, ", ")) } }