diff --git a/internal/cmd/mail.go b/internal/cmd/mail.go index e8e9b487..bd6fef09 100644 --- a/internal/cmd/mail.go +++ b/internal/cmd/mail.go @@ -26,8 +26,10 @@ var ( mailInboxJSON bool mailReadJSON bool mailInboxUnread bool + mailInboxIdentity string mailCheckInject bool mailCheckJSON bool + mailCheckIdentity string mailThreadJSON bool mailReplySubject string mailReplyMessage string @@ -78,11 +80,13 @@ var mailInboxCmd = &cobra.Command{ Long: `Check messages in an inbox. If no address is specified, shows the current context's inbox. +Use --identity for polecats to explicitly specify their identity. Examples: - gt mail inbox # Current context - gt mail inbox mayor/ # Mayor's inbox - gt mail inbox gastown/Toast # Polecat's inbox`, + gt mail inbox # Current context (auto-detected) + gt mail inbox mayor/ # Mayor's inbox + gt mail inbox gastown/Toast # Polecat's inbox + gt mail inbox --identity gastown/Toast # Explicit polecat identity`, Args: cobra.MaximumNArgs(1), RunE: runMailInbox, } @@ -120,9 +124,12 @@ Exit codes (--inject mode): 0 - Always (hooks should never block) Output: system-reminder if mail exists, silent if no mail +Use --identity for polecats to explicitly specify their identity. + Examples: - gt mail check # Simple check - gt mail check --inject # For hooks`, + gt mail check # Simple check (auto-detect identity) + gt mail check --inject # For hooks + gt mail check --identity gastown/Toast # Explicit polecat identity`, RunE: runMailCheck, } @@ -169,6 +176,7 @@ func init() { // Inbox flags mailInboxCmd.Flags().BoolVar(&mailInboxJSON, "json", false, "Output as JSON") mailInboxCmd.Flags().BoolVarP(&mailInboxUnread, "unread", "u", false, "Show only unread messages") + mailInboxCmd.Flags().StringVar(&mailInboxIdentity, "identity", "", "Explicit identity for inbox (e.g., gastown/Toast)") // Read flags mailReadCmd.Flags().BoolVar(&mailReadJSON, "json", false, "Output as JSON") @@ -176,6 +184,7 @@ func init() { // Check flags mailCheckCmd.Flags().BoolVar(&mailCheckInject, "inject", false, "Output format for Claude Code hooks") mailCheckCmd.Flags().BoolVar(&mailCheckJSON, "json", false, "Output as JSON") + mailCheckCmd.Flags().StringVar(&mailCheckIdentity, "identity", "", "Explicit identity for inbox (e.g., gastown/Toast)") // Thread flags mailThreadCmd.Flags().BoolVar(&mailThreadJSON, "json", false, "Output as JSON") @@ -270,9 +279,11 @@ func runMailInbox(cmd *cobra.Command, args []string) error { return fmt.Errorf("not in a Gas Town workspace: %w", err) } - // Determine which inbox to check + // Determine which inbox to check (priority: --identity flag, positional arg, auto-detect) address := "" - if len(args) > 0 { + if mailInboxIdentity != "" { + address = mailInboxIdentity + } else if len(args) > 0 { address = args[0] } else { address = detectSender() @@ -519,8 +530,13 @@ func runMailCheck(cmd *cobra.Command, args []string) error { return fmt.Errorf("not in a Gas Town workspace: %w", err) } - // Determine which inbox - address := detectSender() + // Determine which inbox (priority: --identity flag, auto-detect) + address := "" + if mailCheckIdentity != "" { + address = mailCheckIdentity + } else { + address = detectSender() + } // Get mailbox router := mail.NewRouter(workDir) diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index c1f21e7e..f3b2bfae 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -284,12 +284,14 @@ 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("## Responsibilities") - fmt.Println("- Work on assigned issues") - fmt.Println("- Commit work to your branch") - fmt.Println("- Signal completion for merge queue") + 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") diff --git a/internal/cmd/spawn.go b/internal/cmd/spawn.go index 96395900..00b33b56 100644 --- a/internal/cmd/spawn.go +++ b/internal/cmd/spawn.go @@ -13,6 +13,7 @@ import ( "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/git" + "github.com/steveyegge/gastown/internal/mail" "github.com/steveyegge/gastown/internal/polecat" "github.com/steveyegge/gastown/internal/rig" "github.com/steveyegge/gastown/internal/session" @@ -30,6 +31,7 @@ var ( spawnPolecat string spawnRig string spawnMolecule string + spawnForce bool ) var spawnCmd = &cobra.Command{ @@ -68,6 +70,7 @@ func init() { spawnCmd.Flags().StringVar(&spawnPolecat, "polecat", "", "Polecat name (alternative to positional arg)") spawnCmd.Flags().StringVar(&spawnRig, "rig", "", "Rig name (defaults to current directory's rig)") spawnCmd.Flags().StringVar(&spawnMolecule, "molecule", "", "Molecule ID to instantiate on the issue") + spawnCmd.Flags().BoolVar(&spawnForce, "force", false, "Force spawn even if polecat has unread mail") rootCmd.AddCommand(spawnCmd) } @@ -179,6 +182,21 @@ func runSpawn(cmd *cobra.Command, args []string) error { return fmt.Errorf("polecat '%s' is already working on %s", polecatName, pc.Issue) } + // Check for unread mail in polecat's inbox (indicates existing unstarted work) + polecatAddress := fmt.Sprintf("%s/%s", rigName, polecatName) + router := mail.NewRouter(r.Path) + mailbox, err := router.GetMailbox(polecatAddress) + if err == nil { + _, unread, _ := mailbox.Count() + if unread > 0 && !spawnForce { + return fmt.Errorf("polecat '%s' has %d unread message(s) in inbox (possible existing work assignment)\nUse --force to override, or let the polecat process its inbox first", + polecatName, unread) + } else if unread > 0 { + fmt.Printf("%s Polecat has %d unread message(s), proceeding with --force\n", + style.Dim.Render("Warning:"), unread) + } + } + // Beads operations use mayor/rig directory (rig-level beads) beadsPath := filepath.Join(r.Path, "mayor", "rig") @@ -285,6 +303,16 @@ func runSpawn(cmd *cobra.Command, args []string) error { return nil } + // Send work assignment mail to polecat inbox (before starting session) + // polecatAddress and router already defined above when checking for unread mail + workMsg := buildWorkAssignmentMail(issue, spawnMessage, polecatAddress) + + fmt.Printf("Sending work assignment to %s inbox...\n", polecatAddress) + if err := router.Send(workMsg); err != nil { + return fmt.Errorf("sending work assignment: %w", err) + } + fmt.Printf("%s Work assignment sent\n", style.Bold.Render("✓")) + // Start session t := tmux.NewTmux() sessMgr := session.NewManager(t, r) @@ -292,29 +320,23 @@ func runSpawn(cmd *cobra.Command, args []string) error { // Check if already running running, _ := sessMgr.IsRunning(polecatName) if running { - // Just inject the context - fmt.Printf("Session already running, injecting context...\n") + // Session already running - send notification to check inbox + fmt.Printf("Session already running, notifying to check inbox...\n") + time.Sleep(500 * time.Millisecond) // Brief pause for notification } else { - // Start new session + // Start new session - polecat will check inbox via gt prime startup hook fmt.Printf("Starting session for %s/%s...\n", rigName, polecatName) if err := sessMgr.Start(polecatName, session.StartOptions{}); err != nil { return fmt.Errorf("starting session: %w", err) } - // Wait for Claude to fully initialize (needs 4-5s for prompt) - fmt.Printf("Waiting for Claude to initialize...\n") - time.Sleep(5 * time.Second) - } - - // Inject initial context - context := buildSpawnContext(issue, spawnMessage) - fmt.Printf("Injecting work assignment...\n") - if err := sessMgr.Inject(polecatName, context); err != nil { - return fmt.Errorf("injecting context: %w", err) + // Wait briefly for session to stabilize + time.Sleep(1 * time.Second) } fmt.Printf("%s Session started. Attach with: %s\n", style.Bold.Render("✓"), style.Dim.Render(fmt.Sprintf("gt session at %s/%s", rigName, polecatName))) + fmt.Printf(" %s\n", style.Dim.Render("Polecat will read work assignment from inbox on startup")) return nil } @@ -450,6 +472,7 @@ func syncBeads(workDir string, fromMain bool) error { } // buildSpawnContext creates the initial context message for the polecat. +// Deprecated: Use buildWorkAssignmentMail instead for mail-based work assignment. func buildSpawnContext(issue *BeadsIssue, message string) string { var sb strings.Builder @@ -478,3 +501,48 @@ func buildSpawnContext(issue *BeadsIssue, message string) string { return sb.String() } + +// 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 { + var subject string + var body strings.Builder + + if issue != nil { + 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)) + body.WriteString(fmt.Sprintf("Priority: P%d\n", issue.Priority)) + body.WriteString(fmt.Sprintf("Type: %s\n", issue.Type)) + if issue.Description != "" { + body.WriteString(fmt.Sprintf("\nDescription:\n%s\n", issue.Description)) + } + } else if message != "" { + // Truncate for subject if too long + titleText := message + if len(titleText) > 50 { + titleText = titleText[:47] + "..." + } + subject = fmt.Sprintf("📋 Work Assignment: %s", titleText) + body.WriteString(fmt.Sprintf("Task: %s\n", message)) + } + + 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\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. Signal DONE with summary\n") + + return &mail.Message{ + From: "mayor/", + To: polecatAddress, + Subject: subject, + Body: body.String(), + Priority: mail.PriorityHigh, + Type: mail.TypeTask, + } +} diff --git a/internal/cmd/start.go b/internal/cmd/start.go index 17d8878b..baf905af 100644 --- a/internal/cmd/start.go +++ b/internal/cmd/start.go @@ -203,7 +203,7 @@ func runGracefulShutdown(t *tmux.Tmux) error { } func runImmediateShutdown(t *tmux.Tmux) error { - fmt.Println("Shutting down Gas Town...\n") + fmt.Println("Shutting down Gas Town...") stopped := 0 diff --git a/internal/templates/roles/polecat.md.tmpl b/internal/templates/roles/polecat.md.tmpl index a4c8ebda..4e56a26b 100644 --- a/internal/templates/roles/polecat.md.tmpl +++ b/internal/templates/roles/polecat.md.tmpl @@ -61,9 +61,22 @@ Agent-friendly UX is critical. Your guesses reveal what's intuitive. - `gt done` - Signal work ready for merge queue - `bd sync` - Sync beads changes +## Startup Protocol + +When your session starts, follow this protocol: + +1. **Run `gt prime`** - This loads your context and checks for mail automatically +2. **Check your inbox** - If `gt prime` shows mail, read it with `gt mail read ` +3. **Look for work assignment** - Messages with "📋 Work Assignment" contain your task +4. **If no mail** - Check `bd list --status=in_progress` for existing assignments +5. **Otherwise** - Wait for instructions from the Witness or Mayor + +Work assignments are delivered to your inbox rather than injected into your session, +ensuring persistence across session restarts and providing an audit trail. + ## Work Protocol -1. **Start**: Check mail for assignment, or `bd show ` +1. **Start**: Read your work assignment from mail, or `bd show ` 2. **Work**: Implement the solution in your clone 3. **Commit**: Regular commits with clear messages 4. **Test**: Verify your changes work