From fa09ac480bd96897a6d88ca62be652a7b6119202 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 22 Dec 2025 12:33:35 -0800 Subject: [PATCH 1/2] Implement gt sling command (gt-4ev4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unified work dispatch command that implements spawn + assign + pin in one operation, based on the Universal Gas Town Propulsion Principle: "If you find something on your hook, YOU RUN IT." Supports: - Proto names (e.g., gt sling feature polecat/alpha) - Issue IDs with optional molecule (e.g., gt sling gt-xyz polecat/beta -m bugfix) - Target addressing (polecat/name, deacon/, witness/, refinery/) - --wisp flag for ephemeral molecules - --force flag for hook collision override - Hook collision detection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/sling.go | 725 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 725 insertions(+) create mode 100644 internal/cmd/sling.go diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go new file mode 100644 index 00000000..73ffdf89 --- /dev/null +++ b/internal/cmd/sling.go @@ -0,0 +1,725 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/spf13/cobra" + "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" + "github.com/steveyegge/gastown/internal/style" + "github.com/steveyegge/gastown/internal/tmux" + "github.com/steveyegge/gastown/internal/workspace" +) + +// Sling command flags +var ( + slingWisp bool // Create ephemeral molecule (wisp) + slingMolecule string // Molecule proto when slinging an issue + slingPriority int // Override priority (P0-P4) + slingForce bool // Re-sling even if hook has work + slingNoStart bool // Assign work but don't start session + slingCreate bool // Create polecat if it doesn't exist +) + +var slingCmd = &cobra.Command{ + Use: "sling ", + Short: "Unified work dispatch command", + Long: `Sling work at an agent - the universal Gas Town work dispatch. + +This command implements spawn + assign + pin in one operation. +Based on the Universal Gas Town Propulsion Principle: + + "If you find something on your hook, YOU RUN IT." + +Arguments: + thing What to sling: proto name, issue ID, or epic ID + target Who to sling at: agent address (polecat/name, deacon/, etc.) + +Examples: + gt sling feature polecat/alpha # Spawn feature mol, sling to alpha + gt sling gt-xyz polecat/beta -m bugfix # Sling issue with bugfix workflow + gt sling patrol deacon/ --wisp # Ephemeral patrol wisp + gt sling gt-epic-batch refinery/ # Batch work to refinery + +What Happens When You Sling: + 1. SPAWN (if proto) - Create molecule from template + 2. ASSIGN - Assign molecule/issue to target agent + 3. PIN - Put work on agent's hook (pinned bead) + 4. IGNITION - Agent wakes and runs the work`, + Args: cobra.ExactArgs(2), + RunE: runSling, +} + +func init() { + slingCmd.Flags().BoolVar(&slingWisp, "wisp", false, "Create ephemeral molecule (burned on complete)") + slingCmd.Flags().StringVarP(&slingMolecule, "molecule", "m", "", "Molecule proto when slinging an issue") + slingCmd.Flags().IntVarP(&slingPriority, "priority", "p", -1, "Override priority (0-4)") + slingCmd.Flags().BoolVar(&slingForce, "force", false, "Re-sling even if hook has work") + slingCmd.Flags().BoolVar(&slingNoStart, "no-start", false, "Assign work but don't start session") + slingCmd.Flags().BoolVar(&slingCreate, "create", false, "Create polecat if it doesn't exist") + + rootCmd.AddCommand(slingCmd) +} + +// SlingThing represents what's being slung. +type SlingThing struct { + Kind string // "proto", "issue", or "epic" + ID string // The identifier (proto name or issue ID) + Proto string // If Kind=="issue" and --molecule set, the proto name + IsWisp bool // If --wisp flag set +} + +// SlingTarget represents who's being slung at. +type SlingTarget struct { + Kind string // "polecat", "deacon", "witness", "refinery" + Rig string // Rig name (empty for town-level agents) + Name string // Agent name (for polecats) +} + +func runSling(cmd *cobra.Command, args []string) error { + thingArg := args[0] + targetArg := args[1] + + // Find workspace + townRoot, err := workspace.FindFromCwdOrError() + if err != nil { + return fmt.Errorf("not in a Gas Town workspace: %w", err) + } + + // Parse target first (needed to determine rig context) + target, err := parseSlingTarget(targetArg, townRoot) + if err != nil { + return fmt.Errorf("invalid target: %w", err) + } + + // Get rig context + rigPath := filepath.Join(townRoot, target.Rig) + beadsPath := rigPath + + // Parse thing (needs beads context for proto lookup) + thing, err := parseSlingThing(thingArg, beadsPath) + if err != nil { + return fmt.Errorf("invalid thing: %w", err) + } + + // Apply flags to thing + thing.Proto = slingMolecule + thing.IsWisp = slingWisp + + fmt.Printf("Slinging %s %s at %s\n", + thing.Kind, style.Bold.Render(thing.ID), + style.Bold.Render(targetArg)) + + // Route based on target kind + switch target.Kind { + case "polecat": + return slingToPolecat(townRoot, target, thing) + case "deacon": + return slingToDeacon(townRoot, target, thing) + case "witness": + return slingToWitness(townRoot, target, thing) + case "refinery": + return slingToRefinery(townRoot, target, thing) + default: + return fmt.Errorf("unknown target kind: %s", target.Kind) + } +} + +// parseSlingThing parses the argument. +// Returns the kind (proto, issue, epic) and ID. +func parseSlingThing(arg, beadsPath string) (*SlingThing, error) { + // Check if it looks like an issue ID (has a prefix like gt-, bd-, hq-) + if looksLikeIssueID(arg) { + // Fetch the issue to check its type + b := beads.New(beadsPath) + issue, err := b.Show(arg) + if err != nil { + return nil, fmt.Errorf("issue not found: %s", arg) + } + + kind := "issue" + if issue.Type == "epic" { + kind = "epic" + } + + return &SlingThing{ + Kind: kind, + ID: arg, + }, nil + } + + // Otherwise, assume it's a proto name + // Validate that the proto exists in the catalog + catalog, err := loadMoleculeCatalog(beadsPath) + if err != nil { + return nil, fmt.Errorf("loading catalog: %w", err) + } + + // Try both the exact name and with "mol-" prefix + protoID := arg + if catalog.Get(protoID) == nil { + protoID = "mol-" + arg + if catalog.Get(protoID) == nil { + return nil, fmt.Errorf("proto not found: %s (tried %s and mol-%s)", arg, arg, arg) + } + } + + return &SlingThing{ + Kind: "proto", + ID: protoID, + }, nil +} + +// parseSlingTarget parses the argument. +// Format: polecat/name, deacon/, witness/, refinery/ +// Or with rig: gastown/polecat/name, gastown/witness +func parseSlingTarget(arg, townRoot string) (*SlingTarget, error) { + parts := strings.Split(arg, "/") + + // Handle various formats + switch len(parts) { + case 1: + // Single word like "deacon" - need rig context + rigName, err := inferRigFromCwd(townRoot) + if err != nil { + return nil, fmt.Errorf("cannot infer rig: %w", err) + } + return parseAgentKind(parts[0], "", rigName) + + case 2: + // Could be: polecat/name, rig/role, or role/ (trailing slash) + first, second := parts[0], parts[1] + + // Check for trailing slash (e.g., "deacon/") + if second == "" { + rigName, err := inferRigFromCwd(townRoot) + if err != nil { + return nil, fmt.Errorf("cannot infer rig: %w", err) + } + return parseAgentKind(first, "", rigName) + } + + // Check if first is a known role + if isAgentRole(first) { + // It's role/name (e.g., polecat/alpha) + rigName, err := inferRigFromCwd(townRoot) + if err != nil { + return nil, fmt.Errorf("cannot infer rig: %w", err) + } + return parseAgentKind(first, second, rigName) + } + + // Otherwise it's rig/role (e.g., gastown/deacon) + return parseAgentKind(second, "", first) + + case 3: + // rig/role/name (e.g., gastown/polecat/alpha) + rigName, role, name := parts[0], parts[1], parts[2] + return parseAgentKind(role, name, rigName) + + default: + return nil, fmt.Errorf("invalid target format: %s", arg) + } +} + +// parseAgentKind creates a SlingTarget from parsed components. +func parseAgentKind(role, name, rigName string) (*SlingTarget, error) { + role = strings.ToLower(role) + + switch role { + case "polecat", "polecats": + if name == "" { + return nil, fmt.Errorf("polecat target requires a name (e.g., polecat/alpha)") + } + return &SlingTarget{Kind: "polecat", Rig: rigName, Name: name}, nil + + case "deacon": + return &SlingTarget{Kind: "deacon", Rig: rigName}, nil + + case "witness": + return &SlingTarget{Kind: "witness", Rig: rigName}, nil + + case "refinery": + return &SlingTarget{Kind: "refinery", Rig: rigName}, nil + + default: + // Might be a polecat name without "polecat/" prefix + // Try to detect by checking if it's a valid rig name + return &SlingTarget{Kind: "polecat", Rig: rigName, Name: role}, nil + } +} + +// isAgentRole returns true if the string is a known agent role. +func isAgentRole(s string) bool { + switch strings.ToLower(s) { + case "polecat", "polecats", "deacon", "witness", "refinery": + return true + } + return false +} + +// looksLikeIssueID returns true if the string looks like a beads issue ID. +func looksLikeIssueID(s string) bool { + // Issue IDs have a prefix followed by a dash + // Common prefixes: gt-, bd-, hq- + prefixes := []string{"gt-", "bd-", "hq-", "beads-"} + for _, prefix := range prefixes { + if strings.HasPrefix(s, prefix) { + return true + } + } + return false +} + +// slingToPolecat handles slinging work to a polecat. +func slingToPolecat(townRoot string, target *SlingTarget, thing *SlingThing) error { + rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json") + rigsConfig, err := config.LoadRigsConfig(rigsConfigPath) + if err != nil { + rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)} + } + + g := git.NewGit(townRoot) + rigMgr := rig.NewManager(townRoot, rigsConfig, g) + r, err := rigMgr.GetRig(target.Rig) + if err != nil { + return fmt.Errorf("rig '%s' not found", target.Rig) + } + + // Get polecat manager + polecatGit := git.NewGit(r.Path) + polecatMgr := polecat.NewManager(r, polecatGit) + + polecatName := target.Name + polecatAddress := fmt.Sprintf("%s/%s", target.Rig, polecatName) + + // Router for mail operations + router := mail.NewRouter(r.Path) + + // Check if polecat exists + existingPolecat, err := polecatMgr.Get(polecatName) + polecatExists := err == nil + + if polecatExists { + // Check for existing work on hook (unless --force) + if !slingForce { + if err := checkHookCollision(polecatAddress, r.Path); err != nil { + return err + } + } + + // Check for uncommitted work + pGit := git.NewGit(existingPolecat.ClonePath) + workStatus, checkErr := pGit.CheckUncommittedWork() + if checkErr == nil && !workStatus.Clean() { + fmt.Printf("\n%s Polecat has uncommitted work:\n", style.Warning.Render("⚠")) + if workStatus.HasUncommittedChanges { + fmt.Printf(" • %d uncommitted change(s)\n", len(workStatus.ModifiedFiles)+len(workStatus.UntrackedFiles)) + } + if workStatus.StashCount > 0 { + fmt.Printf(" • %d stash(es)\n", workStatus.StashCount) + } + if workStatus.UnpushedCommits > 0 { + fmt.Printf(" • %d unpushed commit(s)\n", workStatus.UnpushedCommits) + } + fmt.Println() + if !slingForce { + return fmt.Errorf("polecat '%s' has uncommitted work\nUse --force to proceed anyway", polecatName) + } + fmt.Printf("%s Proceeding with --force\n", style.Dim.Render("Warning:")) + } + + // Check for unread mail + mailbox, mailErr := router.GetMailbox(polecatAddress) + if mailErr == nil { + _, unread, _ := mailbox.Count() + if unread > 0 && !slingForce { + return fmt.Errorf("polecat '%s' has %d unread message(s)\nUse --force to override", polecatName, unread) + } else if unread > 0 { + fmt.Printf("%s Polecat has %d unread message(s), proceeding with --force\n", + style.Dim.Render("Warning:"), unread) + } + } + + // Recreate polecat with fresh worktree + fmt.Printf("Recreating polecat %s with fresh worktree...\n", polecatName) + if _, err = polecatMgr.Recreate(polecatName, slingForce); err != nil { + return fmt.Errorf("recreating polecat: %w", err) + } + fmt.Printf("%s Fresh worktree created\n", style.Bold.Render("✓")) + } else if err == polecat.ErrPolecatNotFound { + if !slingCreate { + return fmt.Errorf("polecat '%s' not found (use --create to create)", polecatName) + } + fmt.Printf("Creating polecat %s...\n", polecatName) + if _, err = polecatMgr.Add(polecatName); err != nil { + return fmt.Errorf("creating polecat: %w", err) + } + } else { + return fmt.Errorf("getting polecat: %w", err) + } + + beadsPath := r.Path + + // Sync beads + if err := syncBeads(beadsPath, true); err != nil { + fmt.Printf("%s beads sync: %v\n", style.Dim.Render("Warning:"), err) + } + + // Process the thing based on its kind + var issueID string + var moleculeCtx *MoleculeContext + + switch thing.Kind { + case "proto": + // Spawn molecule from proto + issueID, moleculeCtx, err = spawnMoleculeFromProto(beadsPath, thing, polecatAddress) + if err != nil { + return err + } + + case "issue": + issueID = thing.ID + if thing.Proto != "" { + // Sling issue with molecule workflow + issueID, moleculeCtx, err = spawnMoleculeOnIssue(beadsPath, thing, polecatAddress) + if err != nil { + return err + } + } + + case "epic": + // Epics go to refinery, not polecats + return fmt.Errorf("epics should be slung at refinery/, not polecat/") + } + + // Assign issue to polecat + if err := polecatMgr.AssignIssue(polecatName, issueID); err != nil { + return fmt.Errorf("assigning issue: %w", err) + } + fmt.Printf("%s Assigned %s to %s\n", style.Bold.Render("✓"), issueID, polecatAddress) + + // Pin to hook (update handoff bead with attachment) + if err := pinToHook(beadsPath, polecatAddress, issueID, moleculeCtx); err != nil { + fmt.Printf("%s Could not pin to hook: %v\n", style.Dim.Render("Warning:"), err) + } else { + fmt.Printf("%s Pinned to hook\n", style.Bold.Render("✓")) + } + + // Sync beads + if err := syncBeads(beadsPath, false); err != nil { + fmt.Printf("%s beads push: %v\n", style.Dim.Render("Warning:"), err) + } + + if slingNoStart { + fmt.Printf("\n %s\n", style.Dim.Render("Use 'gt session start' to start the session")) + return nil + } + + // Fetch the issue for mail content + b := beads.New(beadsPath) + issue, _ := b.Show(issueID) + var beadsIssue *BeadsIssue + if issue != nil { + beadsIssue = &BeadsIssue{ + ID: issue.ID, + Title: issue.Title, + Description: issue.Description, + Priority: issue.Priority, + Type: issue.Type, + Status: issue.Status, + } + } + + // Send work assignment mail + workMsg := buildWorkAssignmentMail(beadsIssue, "", polecatAddress, moleculeCtx) + 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) + + running, _ := sessMgr.IsRunning(polecatName) + if running { + fmt.Printf("Session already running, notifying to check inbox...\n") + time.Sleep(500 * time.Millisecond) + } else { + fmt.Printf("Starting session for %s...\n", polecatAddress) + if err := sessMgr.Start(polecatName, session.StartOptions{}); err != nil { + return fmt.Errorf("starting session: %w", err) + } + time.Sleep(3 * time.Second) + } + + fmt.Printf("%s Session started. Attach with: %s\n", + style.Bold.Render("✓"), + style.Dim.Render(fmt.Sprintf("gt session at %s", polecatAddress))) + + // Nudge polecat + sessionName := sessMgr.SessionName(polecatName) + nudgeMsg := fmt.Sprintf("You have a work assignment. Run 'gt mail inbox' to see it, then start working on issue %s.", issueID) + if err := t.NudgeSession(sessionName, nudgeMsg); err != nil { + fmt.Printf(" %s\n", style.Dim.Render(fmt.Sprintf("Warning: could not nudge: %v", err))) + } else { + fmt.Printf(" %s\n", style.Dim.Render("Polecat nudged to start working")) + } + + // Notify Witness + townRouter := mail.NewRouter(townRoot) + witnessAddr := fmt.Sprintf("%s/witness", target.Rig) + sender := detectSender() + spawnNotification := &mail.Message{ + To: witnessAddr, + From: sender, + Subject: fmt.Sprintf("SLING: %s starting on %s", polecatName, issueID), + Body: fmt.Sprintf("Polecat slung.\n\nPolecat: %s\nIssue: %s\nSession: %s\nSlung by: %s", polecatName, issueID, sessionName, sender), + } + if err := townRouter.Send(spawnNotification); err != nil { + fmt.Printf(" %s\n", style.Dim.Render(fmt.Sprintf("Warning: could not notify witness: %v", err))) + } else { + fmt.Printf(" %s\n", style.Dim.Render("Witness notified")) + } + + return nil +} + +// slingToDeacon handles slinging work to the deacon. +func slingToDeacon(townRoot string, target *SlingTarget, thing *SlingThing) error { + if thing.Kind != "proto" { + return fmt.Errorf("deacon only accepts protos (like 'patrol'), not issues") + } + + if !thing.IsWisp { + fmt.Printf("%s Deacon work should be ephemeral. Consider using --wisp\n", + style.Dim.Render("Note:")) + } + + // For deacon, we just need to update its hook and send mail + beadsPath := filepath.Join(townRoot, target.Rig) + + // Sync beads + if err := syncBeads(beadsPath, true); err != nil { + fmt.Printf("%s beads sync: %v\n", style.Dim.Render("Warning:"), err) + } + + // Spawn the molecule from proto + deaconAddress := fmt.Sprintf("%s/deacon", target.Rig) + issueID, moleculeCtx, err := spawnMoleculeFromProto(beadsPath, thing, deaconAddress) + if err != nil { + return err + } + + // Pin to deacon's hook + if err := pinToHook(beadsPath, deaconAddress, issueID, moleculeCtx); err != nil { + fmt.Printf("%s Could not pin to hook: %v\n", style.Dim.Render("Warning:"), err) + } else { + fmt.Printf("%s Pinned to deacon hook\n", style.Bold.Render("✓")) + } + + // Sync beads + if err := syncBeads(beadsPath, false); err != nil { + fmt.Printf("%s beads push: %v\n", style.Dim.Render("Warning:"), err) + } + + fmt.Printf("%s Deacon will run %s on next patrol\n", + style.Bold.Render("✓"), thing.ID) + + return nil +} + +// slingToWitness handles slinging work to the witness. +func slingToWitness(townRoot string, target *SlingTarget, thing *SlingThing) error { + // Similar to deacon - update hook and optionally signal + return fmt.Errorf("slinging to witness not yet implemented") +} + +// slingToRefinery handles slinging work to the refinery. +func slingToRefinery(townRoot string, target *SlingTarget, thing *SlingThing) error { + if thing.Kind != "epic" { + return fmt.Errorf("refinery accepts epics for batch processing, not %s", thing.Kind) + } + + // Refinery batch processing not yet implemented + return fmt.Errorf("slinging epics to refinery not yet implemented") +} + +// spawnMoleculeFromProto spawns a molecule from a proto template. +func spawnMoleculeFromProto(beadsPath string, thing *SlingThing, assignee string) (string, *MoleculeContext, error) { + fmt.Printf("Spawning molecule from proto %s...\n", thing.ID) + + // Use bd mol run to spawn the molecule + args := []string{"--no-daemon", "mol", "run", thing.ID, "--json"} + if assignee != "" { + args = append(args, "--var", "assignee="+assignee) + } + + cmd := exec.Command("bd", args...) + cmd.Dir = beadsPath + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + errMsg := strings.TrimSpace(stderr.String()) + if errMsg != "" { + return "", nil, fmt.Errorf("running molecule: %s", errMsg) + } + return "", nil, fmt.Errorf("running molecule: %w", err) + } + + // Parse result + var molResult struct { + RootID string `json:"root_id"` + IDMapping map[string]string `json:"id_mapping"` + Created int `json:"created"` + Assignee string `json:"assignee"` + Pinned bool `json:"pinned"` + } + if err := json.Unmarshal(stdout.Bytes(), &molResult); err != nil { + return "", nil, fmt.Errorf("parsing molecule result: %w", err) + } + + fmt.Printf("%s Molecule spawned: %s (%d steps)\n", + style.Bold.Render("✓"), molResult.RootID, molResult.Created-1) + + moleculeCtx := &MoleculeContext{ + MoleculeID: thing.ID, + RootIssueID: molResult.RootID, + TotalSteps: molResult.Created - 1, + StepNumber: 1, + } + + return molResult.RootID, moleculeCtx, nil +} + +// spawnMoleculeOnIssue spawns a molecule workflow on an existing issue. +func spawnMoleculeOnIssue(beadsPath string, thing *SlingThing, assignee string) (string, *MoleculeContext, error) { + fmt.Printf("Running molecule %s on issue %s...\n", thing.Proto, thing.ID) + + args := []string{"--no-daemon", "mol", "run", thing.Proto, + "--var", "issue=" + thing.ID, "--json"} + if assignee != "" { + args = append(args, "--var", "assignee="+assignee) + } + + cmd := exec.Command("bd", args...) + cmd.Dir = beadsPath + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + errMsg := strings.TrimSpace(stderr.String()) + if errMsg != "" { + return "", nil, fmt.Errorf("running molecule: %s", errMsg) + } + return "", nil, fmt.Errorf("running molecule: %w", err) + } + + var molResult struct { + RootID string `json:"root_id"` + IDMapping map[string]string `json:"id_mapping"` + Created int `json:"created"` + Assignee string `json:"assignee"` + Pinned bool `json:"pinned"` + } + if err := json.Unmarshal(stdout.Bytes(), &molResult); err != nil { + return "", nil, fmt.Errorf("parsing molecule result: %w", err) + } + + fmt.Printf("%s Molecule %s applied to %s (%d steps)\n", + style.Bold.Render("✓"), thing.Proto, thing.ID, molResult.Created-1) + + moleculeCtx := &MoleculeContext{ + MoleculeID: thing.Proto, + RootIssueID: molResult.RootID, + TotalSteps: molResult.Created - 1, + StepNumber: 1, + } + + return molResult.RootID, moleculeCtx, nil +} + +// checkHookCollision checks if the agent's hook already has work. +func checkHookCollision(agentAddress, beadsPath string) error { + // Parse agent address to get the role for handoff bead lookup + parts := strings.Split(agentAddress, "/") + var role string + if len(parts) >= 2 { + role = parts[len(parts)-1] // Last part is the name/role + } else { + role = parts[0] + } + + b := beads.New(beadsPath) + handoff, err := b.FindHandoffBead(role) + if err != nil { + // Can't check, assume OK + return nil + } + + if handoff == nil { + // No handoff bead exists, no collision + return nil + } + + // Check if there's an attached molecule + attachment := beads.ParseAttachmentFields(handoff) + if attachment != nil && attachment.AttachedMolecule != "" { + return fmt.Errorf("hook already occupied by %s\nUse --force to re-sling", + attachment.AttachedMolecule) + } + + return nil +} + +// pinToHook pins work to an agent's hook by updating their handoff bead. +func pinToHook(beadsPath, agentAddress, issueID string, moleculeCtx *MoleculeContext) error { + // Parse agent address to get the role + parts := strings.Split(agentAddress, "/") + var role string + if len(parts) >= 2 { + role = parts[len(parts)-1] + } else { + role = parts[0] + } + + b := beads.New(beadsPath) + + // Get or create handoff bead + handoff, err := b.GetOrCreateHandoffBead(role) + if err != nil { + return fmt.Errorf("getting handoff bead: %w", err) + } + + // Determine what to attach + attachedMolecule := issueID + if moleculeCtx != nil && moleculeCtx.RootIssueID != "" { + attachedMolecule = moleculeCtx.RootIssueID + } + + // Attach molecule to handoff bead + _, err = b.AttachMolecule(handoff.ID, attachedMolecule) + if err != nil { + return fmt.Errorf("attaching molecule: %w", err) + } + + return nil +} From cbf4497498b563c7e6ecc1ab58069a1f48738293 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 22 Dec 2025 12:36:40 -0800 Subject: [PATCH 2/2] bd sync: close gt-4ev4 --- .beads/issues.jsonl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index bae808b9..400e4e86 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -83,7 +83,7 @@ {"id":"gt-480b","title":"Improve test coverage in low-coverage packages","description":"Several packages have low test coverage:\n- internal/cmd: 6.8%\n- internal/mail: 3.6%\n- internal/daemon: 12.1%\n- internal/doctor: 14.5%\n- internal/refinery: 20.6%\n- internal/session: 27.8%\n- internal/git: 28.8%\n\nPriority should be given to mail, cmd, and daemon packages which handle critical functionality.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-21T21:34:47.807929-08:00","updated_at":"2025-12-21T22:19:59.416507-08:00","closed_at":"2025-12-21T22:19:59.416507-08:00","close_reason":"Deferred to post-launch. Test coverage improvement is ongoing work, not blocking for launch. Current tests cover critical paths; additional coverage can be added incrementally."} {"id":"gt-48bs","title":"gt rig reset: clear stale mail on reset/land","description":"## Problem\n\nWhen resetting or landing a rig/town, stale mail messages can confuse agents on startup. Old handoff messages, daemon notifications, and inter-agent mail should be cleaned up as part of reset.\n\n## Current State\n\n- `gt rig reset` exists but doesn't clear mail\n- Stale messages accumulate (e.g., daemon SHUTDOWN messages)\n- Agents may read outdated context on startup\n\n## Proposed Behavior\n\n`gt rig reset` and `gt town reset` should:\n1. Close all open messages (`--type=message`) in the relevant beads\n2. Optionally preserve pinned handoff beads (clear content, keep bead)\n3. Log what was cleaned up\n\n```bash\ngt rig reset gastown # Clears gastown mail\ngt rig reset gastown --mail # Only clear mail, keep other state\ngt town reset # Clears all town-level mail\ngt town reset --all # Clears mail in all rigs too\n```\n\n## Implementation\n\n1. Query `bd list --type=message --status=open`\n2. Close each with reason 'Cleared during reset'\n3. For pinned handoffs: `bd update \u003cid\u003e --description=''` instead of close","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T11:42:17.769674-08:00","updated_at":"2025-12-21T11:27:49.532203-08:00","closed_at":"2025-12-21T11:27:49.532203-08:00","close_reason":"Added --mail flag to gt rig reset"} {"id":"gt-4eim","title":"gt nudge should accept flexible session identifiers","description":"Currently `gt nudge` requires the exact tmux session name (e.g., `gt-gastown-furiosa`).\n\nIt should also accept:\n- `gastown/furiosa` (rig/polecat format)\n- `furiosa` (polecat name, infer rig from cwd or require if ambiguous)\n\nThe session list command shows `gastown/furiosa` format, but nudge rejects it:\n```\ngt session list → shows 'gastown/furiosa'\ngt nudge gastown/furiosa 'msg' → 'session not found'\ngt nudge gt-gastown-furiosa 'msg' → works\n```\n\nShould normalize all these formats to the tmux session name internally.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"gastown/angharad","created_at":"2025-12-21T15:36:45.013475-08:00","updated_at":"2025-12-21T15:37:59.042119-08:00"} -{"id":"gt-4ev4","title":"Implement gt sling command","description":"The unified work dispatch command.\n\n```bash\ngt sling \u003cthing\u003e \u003ctarget\u003e [options]\n```\n\nImplements spawn + assign + pin in one operation. See sling-design.md.\n\nAcceptance:\n- [ ] Parse thing (proto name, issue ID, epic ID)\n- [ ] Parse target (agent address) \n- [ ] Spawn molecule if proto\n- [ ] Assign to target agent\n- [ ] Pin to agent's hook (pinned bead)\n- [ ] Support --wisp flag for ephemeral\n- [ ] Support --molecule flag for issue+workflow\n- [ ] Error if hook already occupied (unless --force)","status":"in_progress","priority":1,"issue_type":"task","assignee":"gastown/furiosa","created_at":"2025-12-22T03:17:27.273013-08:00","updated_at":"2025-12-22T12:12:32.617698-08:00"} +{"id":"gt-4ev4","title":"Implement gt sling command","description":"The unified work dispatch command.\n\n```bash\ngt sling \u003cthing\u003e \u003ctarget\u003e [options]\n```\n\nImplements spawn + assign + pin in one operation. See sling-design.md.\n\nAcceptance:\n- [ ] Parse thing (proto name, issue ID, epic ID)\n- [ ] Parse target (agent address) \n- [ ] Spawn molecule if proto\n- [ ] Assign to target agent\n- [ ] Pin to agent's hook (pinned bead)\n- [ ] Support --wisp flag for ephemeral\n- [ ] Support --molecule flag for issue+workflow\n- [ ] Error if hook already occupied (unless --force)","status":"in_progress","priority":1,"issue_type":"task","assignee":"gastown/furiosa","created_at":"2025-12-22T03:17:27.273013-08:00","updated_at":"2025-12-22T12:19:35.961386-08:00"} {"id":"gt-4my","title":"Doctor check: Worker health and stuck detection","description":"Detect and report stuck workers via gt doctor.\n\n## Checks\n\n### WorkerHealthCheck\n- List all active workers (polecats with state=working)\n- Check last activity timestamp for each\n- Flag as potentially stuck if no progress for configurable threshold (default: 30 min)\n- Check if Witness is running for the rig\n- Verify Witness last heartbeat time\n\n### Stuck Detection Criteria\n- Polecat state=working but session not running\n- Polecat state=working but output unchanged for threshold\n- Witness not responding to health checks\n- Multiple polecats in same rig all stuck\n\n## Output\n\n```\n[WARN] Workers in rig 'wyvern' may be stuck:\n - Toast: working for 45m, no recent output\n - Capable: working for 52m, session not found\n - Witness: last heartbeat 20m ago\n \n Suggestions:\n - gt witness status wyvern\n - gt capture wyvern/Toast 50\n - gt stop --rig wyvern (kill all)\n```\n\n## Auto-Fix\n\nCannot auto-fix stuck workers (risk of data loss), but can:\n- Restart Witness daemon if crashed\n- Send warning mail to Mayor","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T23:17:59.265062-08:00","updated_at":"2025-12-16T17:24:34.882466-08:00","dependencies":[{"issue_id":"gt-4my","depends_on_id":"gt-f9x.4","type":"blocks","created_at":"2025-12-15T23:19:05.565606-08:00","created_by":"daemon"},{"issue_id":"gt-4my","depends_on_id":"gt-7ik","type":"blocks","created_at":"2025-12-17T15:44:42.068149-08:00","created_by":"daemon"}]} {"id":"gt-4nn","title":"Molecules: Composable Workflow Beads","description":"## Summary\n\nMolecules are crystallized workflow patterns stored as Beads issues.\nWhen instantiated, the molecule creates child beads forming a DAG.\n\n## Key Insight: Molecules ARE Beads\n\nPer HOP Decision 001: Beads IS the ledger. Molecules don't get a separate YAML format - they're issues with `type: molecule` containing prose-based step definitions.\n\nAgents don't need rigid schemas. They parse natural language natively. A molecule is just instructions with enough structure for tooling.\n\n## Example: Engineer in a Box\n\n```markdown\nid: mol-xyz\ntype: molecule\ntitle: Engineer in a Box\n\nThis workflow takes a task from design to merge.\n\n## Step: design\nThink carefully about architecture. Consider existing patterns, \ntrade-offs, testability.\n\n## Step: implement\nWrite clean code. Follow codebase conventions.\nNeeds: design\n\n## Step: review \nReview for bugs, edge cases, style issues.\nNeeds: implement\n\n## Step: test\nWrite and run tests. Cover happy path and edge cases.\nNeeds: implement\n\n## Step: submit\nSubmit for merge via refinery.\nNeeds: review, test\n```\n\n## Instantiation\n\n```bash\n# Attach molecule when spawning\ngt spawn --issue gt-abc --molecule mol-xyz\n\n# Creates child beads atomically:\ngt-abc.design ← ready first\ngt-abc.implement ← blocked by design \ngt-abc.review ← blocked by implement\ngt-abc.test ← blocked by implement\ngt-abc.submit ← blocked by review, test\n```\n\nEach step issue gets an `instantiated-from` edge to the molecule (with step metadata).\n\n## Why This Matters\n\n1. **Unified data plane**: Everything in Beads, no parallel YAML channel\n2. **AI-native**: Prose instructions, not rigid schemas\n3. **Error isolation**: Each step is a checkpoint - failure doesn't lose progress\n4. **Scales with AI**: As agents get smarter, they handle more complex molecules\n\n## Implementation Primitives\n\n- `ParseMoleculeSteps()`: Extract steps from prose (convention-based)\n- `InstantiateMolecule()`: Atomic transaction creating all steps + edges \n- `instantiated-from` edge type: Track provenance\n- Parameterization: `{{variable}}` substitution from context map","status":"closed","priority":0,"issue_type":"epic","created_at":"2025-12-18T18:06:24.573068-08:00","updated_at":"2025-12-19T14:44:59.705427-08:00","closed_at":"2025-12-19T14:44:59.705427-08:00"} {"id":"gt-4nn.1","title":"Molecule schema: YAML format for workflow definitions","description":"Define the YAML schema for molecule definitions:\n\n```yaml\nmolecule: \u003cname\u003e\nversion: 1\ndescription: \"Human description\"\nsteps:\n - id: \u003cstep-id\u003e\n title: \"Step title\"\n prompt: \"Instructions for agent\"\n depends: [\u003cother-step-ids\u003e] # optional\n tier: haiku|sonnet|opus # optional, default from config\n timeout: 30m # optional\n```\n\nStore molecules in:\n- `\u003crig\u003e/molecules/\u003cname\u003e.yaml` for rig-specific\n- `\u003ctown\u003e/molecules/\u003cname\u003e.yaml` for town-wide\n\nBuilt-in molecules to ship:\n- engineer-in-box: design→code→review→test→submit\n- quick-fix: implement→test→submit\n- research: investigate→document","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-18T18:06:49.441267-08:00","updated_at":"2025-12-18T20:14:32.629327-08:00","closed_at":"2025-12-18T20:14:32.629327-08:00","dependencies":[{"issue_id":"gt-4nn.1","depends_on_id":"gt-4nn","type":"parent-child","created_at":"2025-12-18T18:06:49.442723-08:00","created_by":"daemon"}]} @@ -139,7 +139,7 @@ {"id":"gt-71i","title":"Update architecture.md: Engineer role and Beads merge queue","description":"Update docs/architecture.md with recent design decisions:\n\n1. Agent table: Change \"Refinery\" role to \"Engineer\"\n - Refinery = place/module/directory\n - Engineer = role (agent that works in the Refinery)\n\n2. Merge Queue section: Document Beads-native model\n - MRs are beads issues with --type=merge-request\n - gt mq commands (submit, list, next, process, reorder)\n - Ordering via depends-on links\n\n3. CLI section: Add gt mq commands\n\n4. Key Design Decisions: Add decisions for:\n - #15: Merge Queue in Beads\n - #16: Engineer role (distinct from Refinery place)\n - #17: Session restart protocol for Engineer","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-16T23:12:03.616159-08:00","updated_at":"2025-12-16T23:12:03.616159-08:00","dependencies":[{"issue_id":"gt-71i","depends_on_id":"gt-h5n","type":"blocks","created_at":"2025-12-16T23:12:14.92163-08:00","created_by":"daemon"}]} {"id":"gt-72so","title":"gt mq list: doesn't show submitted MRs","description":"After submitting MRs with gt mq submit, gt mq list gastown shows empty queue.\n\n## Reproduction\n1. gt mq submit --issue gt-h5n.5 --branch polecat/Scabrous\n2. gt mq list gastown → (empty)\n3. bd list --type=merge-request → shows the MR\n\n## Expected\ngt mq list should show submitted MRs\n\n## MR example\ngt-ts4u has rig: gastown in description, type=merge-request, status=open","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-19T14:54:26.731813-08:00","updated_at":"2025-12-19T17:22:52.552662-08:00","closed_at":"2025-12-19T16:19:53.82455-08:00"} {"id":"gt-7918","title":"Patrols: Cyclic molecules for autonomous maintenance","description":"## Vision\n\nPatrols are **cyclic molecules** - workflow loops that give Gas Town its autonomous nervous system. While regular molecules are DAGs that terminate, patrols loop forever, performing maintenance tasks at varying cadences.\n\nThis is the \"steam engine\" of Gas Town: converting episodic Claude sessions into continuous autonomous operation.\n\n## Core Concepts\n\n### 1. Cyclic Molecules\n\nRegular molecule: A -\u003e B -\u003e C -\u003e done\nPatrol molecule: A -\u003e B -\u003e C --+\n ^ |\n +-------------+\n\nPatrols have a loop_to field specifying where to restart.\n\n### 2. Cooldown-Aware Steps (Atoms)\n\nSteps encode their own cadence. When patrol reaches a step:\n1. Check: now - last_run \u003e cooldown?\n2. If yes: execute, update last_run\n3. If no: skip (immediately close)\n\nThe patrol runner is simple - steps self-skip. Complexity distributed into atoms.\n\n### 3. The Beacon\n\nThe heartbeat that fires patrol triggers:\n- Internal ticker in Deacon (goroutine)\n- Or external cron firing gt deacon tick\n- Or mail-based triggers\n\nWithout beacon, nothing proactive happens.\n\n### 4. Session Reset as Patrol Step\n\nConnects to auto-handoff (gt-bcwn). Session reset is a patrol step, not a separate mechanism.\n\n### 5. Multi-Role Patrols\n\nEach supervisor has its own patrol:\n\n**Deacon patrol:** health-check (30s), session-gc (5m), beacon-tick (10s)\n**Witness patrol:** orphan-scan (10m), stuck-check (2m), molecule-progress (1m)\n**Refinery patrol:** queue-check (30s), pr-status (1m), merge-ready (30s)\n\n### 6. Cadence Tiers\n\n- Critical (10-30s): Health checks\n- Active (1-5m): Progress, nudges\n- Maintenance (10-30m): Orphans, GC\n- Periodic (1h+): Reports\n\n### 7. Best-Effort Scheduling\n\nNot real-time - more like cron. No hard deadlines. Catch-up, dont pile-up.\nPriority preemption (mail interrupts patrol). Graceful degradation under load.\n\n## Open Questions\n\n1. State persistence: Beads (self-describing) or file (faster)?\n2. Interruption: How does urgent mail preempt patrol?\n3. Error recovery: Backoff? Escalate? Circuit breaker?\n4. Coordination: Can patrols send mail to trigger other patrols?\n\n## Related\n\n- gt-bcwn: Auto-handoff (session reset is a patrol step)\n- Molecule system (patrols extend molecules with loops)\n- Deacon lifecycle management\n\n## Metaphor\n\nClaude was fire. Claude Code was steam. Gas Town is the steam engine. Beads is the train tracks.\n\nThe steam engine converts episodic combustion into continuous rotary motion.\nGas Town converts episodic Claude sessions into continuous autonomous work.","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-21T12:18:22.99128-08:00","updated_at":"2025-12-21T12:18:22.99128-08:00","dependencies":[{"issue_id":"gt-7918","depends_on_id":"gt-bcwn","type":"blocks","created_at":"2025-12-21T12:18:30.86651-08:00","created_by":"daemon"}]} -{"id":"gt-7hor","title":"Document the Propulsion Principle","description":"Write canonical documentation for the Universal Gas Town Propulsion Principle.\n\nLocation: gastown/mayor/rig/docs/propulsion-principle.md\n\nContent:\n- The One Rule (hook has work → work happens)\n- Why it works (stateless agents, molecule-driven)\n- The sling lifecycle diagram\n- Agent startup protocol\n- Examples and anti-patterns\n\nThis is foundational theory-of-operation documentation.","status":"in_progress","priority":2,"issue_type":"task","assignee":"gastown/slit","created_at":"2025-12-22T03:17:47.790012-08:00","updated_at":"2025-12-22T12:13:16.886084-08:00"} +{"id":"gt-7hor","title":"Document the Propulsion Principle","description":"Write canonical documentation for the Universal Gas Town Propulsion Principle.\n\nLocation: gastown/mayor/rig/docs/propulsion-principle.md\n\nContent:\n- The One Rule (hook has work → work happens)\n- Why it works (stateless agents, molecule-driven)\n- The sling lifecycle diagram\n- Agent startup protocol\n- Examples and anti-patterns\n\nThis is foundational theory-of-operation documentation.","status":"in_progress","priority":2,"issue_type":"task","assignee":"gastown/slit","created_at":"2025-12-22T03:17:47.790012-08:00","updated_at":"2025-12-22T12:20:42.807608-08:00"} {"id":"gt-7hz3","title":"Merge: gt-92l","description":"branch: polecat/Ace\ntarget: main\nsource_issue: gt-92l\nrig: gastown","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-19T16:31:37.716367-08:00","updated_at":"2025-12-19T18:26:14.102101-08:00","closed_at":"2025-12-19T17:48:09.627376-08:00"} {"id":"gt-7iek","title":"context-check","description":"Assess own context usage. If high, prepare for handoff.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-21T17:51:45.43771-08:00","updated_at":"2025-12-21T17:51:45.43771-08:00","dependencies":[{"issue_id":"gt-7iek","depends_on_id":"gt-hbnz","type":"parent-child","created_at":"2025-12-21T17:51:45.442974-08:00","created_by":"stevey"}],"wisp":true} {"id":"gt-7ik","title":"Ephemeral polecats: spawn fresh, delete on completion","description":"## Design Decision\n\nSwitch from pooled/idle polecats to ephemeral model:\n- Spawn creates fresh worktree from main\n- Polecat requests shutdown when done (bottom-up)\n- Witness verifies handoff, kills session, deletes worktree\n- No 'idle' state - polecats exist only while working\n\n## Rationale\n\n1. **Git worktrees are fast** - pooling optimization is obsolete\n2. **Pooling creates maintenance burden:**\n - Git stashes accumulate\n - Untracked artifacts pile up\n - Branches drift from main\n - Beads DB gets stale\n3. **PGT sync problems** came from persistent branches\n4. **Support infrastructure exists** - Witness, Refinery, Mayor handle continuity\n5. **Simpler mental model** - polecat exists = work in progress\n\n## Lifecycle\n\n```\nSpawn:\n gt spawn --issue \u003cid\u003e\n → Creates fresh worktree: git worktree add polecats/\u003cname\u003e -b polecat/\u003cname\u003e\n → Initializes beads in worktree\n → Starts session, assigns work\n\nWorking:\n Polecat does task\n → Pushes to polecat/\u003cname\u003e branch\n → Submits to merge queue when ready\n\nCompletion (POLECAT-INITIATED):\n Polecat runs: gt handoff\n → Verifies git state clean\n → Sends mail to Witness: \"Ready for shutdown\"\n → Marks itself done, waits for termination\n\nCleanup (WITNESS-OWNED):\n Witness receives shutdown request\n → Verifies PR merged or in queue\n → Verifies no uncommitted changes\n → Kills session: gt session stop \u003crig\u003e/\u003cpolecat\u003e\n → Deletes worktree: git worktree remove polecats/\u003cname\u003e\n → Deletes branch: git branch -d polecat/\u003cname\u003e\n → Optionally: Notifies Mayor of completion\n```\n\n## Key Insight: Bottom-Up Shutdown\n\n**Old model (wrong)**: Top-down batch shutdown - \"cancel the swarm\"\n**New model (right)**: Bottom-up individual shutdown - polecat requests, Witness executes\n\nThis enables streaming:\n- Workers come and go continuously\n- No \"swarm end\" to trigger cleanup\n- Each worker manages its own lifecycle\n- Witness is the lifecycle authority\n\n## Implementation\n\n1. Add `gt handoff` command for polecats to request shutdown\n2. Modify gt spawn to always create fresh worktree\n3. Run bd init in new worktree (beads needs initialization)\n4. Add shutdown request handler to Witness\n5. Witness verifies handoff, then cleans up:\n - Kill session\n - Remove worktree\n - Delete branch\n6. Remove 'idle' state from polecat state machine\n7. Simplify gt polecat list (only shows active)\n\n## Impact on Other Tasks\n\n- gt-17r (Zombie cleanup): Becomes trivial - orphan worktrees\n- gt-4my (Worker health): Simpler - no idle/stuck ambiguity\n- gt-f9x.5/f9x.6 (Doctor): Fewer states to validate\n- gt-eu9 (Witness handoff): Witness receives polecat shutdown requests","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-17T15:44:31.139964-08:00","updated_at":"2025-12-19T01:57:17.033547-08:00","closed_at":"2025-12-19T01:57:17.033547-08:00"} @@ -554,7 +554,7 @@ {"id":"gt-upom","title":"Witness patrol: cleanup idle orphan polecats","description":"Add patrol step to find and cleanup polecats that are idle with no assigned issue. These orphans occur when polecats crash before sending DONE or Witness misses the message. Patrol should verify git is clean before removing worktree. Part of gt-rana.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-21T23:09:41.756753-08:00","updated_at":"2025-12-21T23:09:41.756753-08:00"} {"id":"gt-us8","title":"Daemon: configurable heartbeat interval","description":"Heartbeat interval is hardcoded to 60s. Should be configurable via:\n- town.json config\n- Command line flag\n- Environment variable\n\nDefault 60s is reasonable but some deployments may want faster/slower.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-18T13:38:14.282216-08:00","updated_at":"2025-12-18T13:38:14.282216-08:00","dependencies":[{"issue_id":"gt-us8","depends_on_id":"gt-99m","type":"blocks","created_at":"2025-12-18T13:38:26.704111-08:00","created_by":"daemon"}]} {"id":"gt-usy0","title":"Merge: gt-3x0z.3","description":"branch: polecat/rictus\ntarget: main\nsource_issue: gt-3x0z.3\nrig: gastown","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-21T16:03:43.535266-08:00","updated_at":"2025-12-21T17:20:27.505696-08:00","closed_at":"2025-12-21T17:20:27.505696-08:00","close_reason":"ORPHANED: Branch never pushed, worktree deleted"} -{"id":"gt-uym5","title":"Implement gt mol status command","description":"Show what's on an agent's hook.\n\n```bash\ngt mol status [target]\n```\n\nOutput:\n- What's slung (molecule name, associated issue)\n- Current phase and progress\n- Whether it's a wisp\n- Next action hint\n\nIf no target, shows current agent's status.\n\nAcceptance:\n- [ ] Read pinned bead attachment\n- [ ] Display molecule/issue info\n- [ ] Show phase progress\n- [ ] Indicate wisp vs durable","status":"in_progress","priority":1,"issue_type":"task","assignee":"gastown/nux","created_at":"2025-12-22T03:17:34.679963-08:00","updated_at":"2025-12-22T12:12:57.452225-08:00"} +{"id":"gt-uym5","title":"Implement gt mol status command","description":"Show what's on an agent's hook.\n\n```bash\ngt mol status [target]\n```\n\nOutput:\n- What's slung (molecule name, associated issue)\n- Current phase and progress\n- Whether it's a wisp\n- Next action hint\n\nIf no target, shows current agent's status.\n\nAcceptance:\n- [ ] Read pinned bead attachment\n- [ ] Display molecule/issue info\n- [ ] Show phase progress\n- [ ] Indicate wisp vs durable","status":"in_progress","priority":1,"issue_type":"task","assignee":"gastown/nux","created_at":"2025-12-22T03:17:34.679963-08:00","updated_at":"2025-12-22T12:20:24.555738-08:00"} {"id":"gt-v2ed","title":"inbox-check","description":"Handle callbacks from agents. Check gt mail inbox, process lifecycle requests.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-21T17:51:45.436677-08:00","updated_at":"2025-12-21T17:51:45.436677-08:00","wisp":true} {"id":"gt-v5hv","title":"Work on ga-y6b: Implement Refinery as Claude agent. Conve...","description":"Work on ga-y6b: Implement Refinery as Claude agent. Convert from shell to Claude agent that processes MRs in merge queue, runs tests, merges to integration branch. When done, submit MR (not PR) to integration branch for Refinery.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T22:58:17.576892-08:00","updated_at":"2025-12-19T23:23:22.778407-08:00","closed_at":"2025-12-19T23:23:22.778407-08:00"} {"id":"gt-v5k","title":"Design: Failure modes and recovery","description":"Document failure modes and recovery strategies for Gas Town operations.\n\n## Critical Failure Modes\n\n### 1. Agent Crash Mid-Operation\n\n**Scenario**: Polecat crashes while committing, Witness crashes while verifying\n\n**Detection**:\n- Session suddenly gone (tmux check fails)\n- State shows 'working' but no session\n- Heartbeat stops (for Witness)\n\n**Recovery**:\n- Doctor detects via ZombieSessionCheck\n- Capture any recoverable state\n- Reset agent state to 'idle'\n- For Witness: auto-restart via supervisor or manual gt witness start\n\n### 2. Git State Corruption\n\n**Scenario**: Merge conflict, failed rebase, detached HEAD\n\n**Detection**:\n- Git commands fail\n- Dirty state that won't commit\n- Branch diverged from origin\n\n**Recovery**:\n- gt doctor reports git health issues\n- Manual intervention recommended\n- Severe cases: remove clone, re-clone\n\n### 3. Beads Sync Conflict\n\n**Scenario**: Two polecats modify same issue\n\n**Detection**:\n- bd sync fails with conflict\n- Beads tombstone mechanism handles most cases\n\n**Recovery**:\n- Beads has last-write-wins semantics\n- bd sync --force in extreme cases\n- Issues may need manual dedup\n\n### 4. Tmux Failure\n\n**Scenario**: Tmux server crashes, socket issues\n\n**Detection**:\n- All sessions inaccessible\n- \"no server running\" errors\n\n**Recovery**:\n- Kill any orphan processes\n- tmux kill-server \u0026\u0026 tmux start-server\n- All agent states reset to idle\n- Re-spawn active work\n\n### 5. Claude API Issues\n\n**Scenario**: Rate limits, outages, context limits\n\n**Detection**:\n- Sessions hang or produce errors\n- Repeated failure patterns\n\n**Recovery**:\n- Exponential backoff (handled by Claude Code)\n- For context limits: session cycling (mail-to-self)\n- For outages: wait and retry\n\n### 6. Disk Full\n\n**Scenario**: Clones, logs, or beads fill disk\n\n**Detection**:\n- Write operations fail\n- git/bd commands error\n\n**Recovery**:\n- Clean up logs: rm ~/.gastown/logs/*\n- Remove old polecat clones\n- gt doctor --fix can clean some cruft\n\n### 7. Network Failure\n\n**Scenario**: Can't reach GitHub, API servers\n\n**Detection**:\n- git fetch/push fails\n- Claude sessions hang\n\n**Recovery**:\n- Work continues locally\n- Queue pushes for later\n- Sync when connectivity restored\n\n## Recovery Principles\n\n1. **Fail safe**: Prefer stopping over corrupting\n2. **State is recoverable**: Git and beads have recovery mechanisms\n3. **Doctor heals**: gt doctor --fix handles common issues\n4. **Emergency stop**: gt stop --all as last resort\n5. **Human escalation**: Some failures need Overseer intervention\n\n## Implementation\n\n- Document each failure mode in architecture.md\n- Ensure doctor checks cover detection\n- Add recovery hints to error messages\n- Log all failures for debugging","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T23:19:07.198289-08:00","updated_at":"2025-12-15T23:19:28.171942-08:00"}