From f8fc01ecb5fad4a0971f2fd9af128317e5db0570 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 09:46:37 -0800 Subject: [PATCH] Add gt feed command for activity streaming (gt-3pm0f) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps bd activity with Gas Town integration: - Default follow mode for streaming - --rig flag to target specific rig beads - Passthrough of bd activity flags (--since, --mol, --type, --limit) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/feed.go | 141 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 internal/cmd/feed.go diff --git a/internal/cmd/feed.go b/internal/cmd/feed.go new file mode 100644 index 00000000..8d43f907 --- /dev/null +++ b/internal/cmd/feed.go @@ -0,0 +1,141 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "syscall" + + "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/workspace" +) + +var ( + feedFollow bool + feedLimit int + feedSince string + feedMol string + feedType string + feedRig string + feedNoFollow bool +) + +func init() { + rootCmd.AddCommand(feedCmd) + + feedCmd.Flags().BoolVarP(&feedFollow, "follow", "f", false, "Stream events in real-time (default when no other flags)") + feedCmd.Flags().BoolVar(&feedNoFollow, "no-follow", false, "Show events once and exit") + feedCmd.Flags().IntVarP(&feedLimit, "limit", "n", 100, "Maximum number of events to show") + feedCmd.Flags().StringVar(&feedSince, "since", "", "Show events since duration (e.g., 5m, 1h, 30s)") + feedCmd.Flags().StringVar(&feedMol, "mol", "", "Filter by molecule/issue ID prefix") + feedCmd.Flags().StringVar(&feedType, "type", "", "Filter by event type (create, update, delete, comment)") + feedCmd.Flags().StringVar(&feedRig, "rig", "", "Run from specific rig's beads directory") +} + +var feedCmd = &cobra.Command{ + Use: "feed", + GroupID: GroupDiag, + Short: "Show real-time activity feed from beads", + Long: `Display a real-time feed of issue and molecule state changes. + +This command wraps 'bd activity' to show mutations as they happen, +providing visibility into workflow progress across Gas Town. + +By default, streams in follow mode. Use --no-follow to show events once. + +Event symbols: + + created/bonded - New issue or molecule created + → in_progress - Work started on an issue + ✓ completed - Issue closed or step completed + ✗ failed - Step or issue failed + ⊘ deleted - Issue removed + +Examples: + gt feed # Stream all events (default: --follow) + gt feed --no-follow # Show last 100 events and exit + gt feed --since 1h # Events from last hour + gt feed --mol gt-xyz # Filter by issue prefix + gt feed --rig gastown # Use gastown rig's beads`, + RunE: runFeed, +} + +func runFeed(cmd *cobra.Command, args []string) error { + // Find bd binary + bdPath, err := exec.LookPath("bd") + if err != nil { + return fmt.Errorf("bd not found in PATH: %w", err) + } + + // Determine working directory + workDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("getting current directory: %w", err) + } + + // If --rig specified, find that rig's beads directory + if feedRig != "" { + townRoot, err := workspace.FindFromCwdOrError() + if err != nil { + return fmt.Errorf("not in a Gas Town workspace: %w", err) + } + + // Try common beads locations for the rig + candidates := []string{ + fmt.Sprintf("%s/%s/mayor/rig", townRoot, feedRig), + fmt.Sprintf("%s/%s", townRoot, feedRig), + } + + found := false + for _, candidate := range candidates { + if _, err := os.Stat(candidate + "/.beads"); err == nil { + workDir = candidate + found = true + break + } + } + + if !found { + return fmt.Errorf("rig '%s' not found or has no .beads directory", feedRig) + } + } + + // Build bd activity command args + bdArgs := []string{"bd", "activity"} + + // Default to follow mode unless --no-follow or other display flags set + shouldFollow := !feedNoFollow + if feedFollow { + shouldFollow = true + } + + if shouldFollow { + bdArgs = append(bdArgs, "--follow") + } + + if feedLimit != 100 { + bdArgs = append(bdArgs, "--limit", fmt.Sprintf("%d", feedLimit)) + } + + if feedSince != "" { + bdArgs = append(bdArgs, "--since", feedSince) + } + + if feedMol != "" { + bdArgs = append(bdArgs, "--mol", feedMol) + } + + if feedType != "" { + bdArgs = append(bdArgs, "--type", feedType) + } + + // Use exec to replace the current process with bd + // This gives clean signal handling and terminal control + env := os.Environ() + + // Change to the target directory before exec + if err := os.Chdir(workDir); err != nil { + return fmt.Errorf("changing to directory %s: %w", workDir, err) + } + + return syscall.Exec(bdPath, bdArgs, env) +}