From 813105220784d2106a5f30153a246d563087aecc Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 26 Dec 2025 15:54:07 -0800 Subject: [PATCH] Deprecate hook files, use pinned beads for propulsion (gt-rgd9x) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hook file mechanism with discovery-based pinned beads: - gt hook: now runs bd update --status=pinned - gt sling: same, plus nudge to target - gt handoff: same when bead ID provided - gt prime: checks pinned beads instead of hook files - gt mol status: no longer checks hook files Key changes: - outputAttachmentStatus: extended to all roles (was Crew/Polecat only) - checkSlungWork: now queries pinned beads instead of reading hook files - wisp/io.go functions: marked deprecated with migration notes This follows Gas Town discovery over explicit state principle. Hook files are kept for backward compatibility but no longer written. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/handoff.go | 22 ++--- internal/cmd/hook.go | 28 +++--- internal/cmd/molecule_status.go | 12 +-- internal/cmd/prime.go | 149 ++++++++++++-------------------- internal/cmd/sling.go | 62 +++++-------- internal/wisp/io.go | 14 +++ internal/wisp/types.go | 18 +++- 7 files changed, 126 insertions(+), 179 deletions(-) diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index ab90e311..d5f1bd10 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -10,7 +10,6 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" - "github.com/steveyegge/gastown/internal/wisp" "github.com/steveyegge/gastown/internal/workspace" ) @@ -504,29 +503,26 @@ func hookBeadForHandoff(beadID string) error { return fmt.Errorf("bead '%s' not found", beadID) } - // Determine agent identity and clone root - agentID, _, cloneRoot, err := resolveSelfTarget() + // Determine agent identity + agentID, _, _, err := resolveSelfTarget() if err != nil { return fmt.Errorf("detecting agent identity: %w", err) } - // Create the slung work wisp - sw := wisp.NewSlungWork(beadID, agentID) - sw.Subject = handoffSubject - sw.Context = handoffMessage - fmt.Printf("%s Hooking %s...\n", style.Bold.Render("🪝"), beadID) if handoffDryRun { - fmt.Printf("Would create wisp: %s\n", wisp.HookPath(cloneRoot, agentID)) + fmt.Printf("Would run: bd update %s --status=pinned --assignee=%s\n", beadID, agentID) return nil } - // Write the wisp to the hook - if err := wisp.WriteSlungWork(cloneRoot, agentID, sw); err != nil { - return fmt.Errorf("writing wisp: %w", err) + // Pin the bead using bd update (discovery-based approach) + pinCmd := exec.Command("bd", "update", beadID, "--status=pinned", "--assignee="+agentID) + pinCmd.Stderr = os.Stderr + if err := pinCmd.Run(); err != nil { + return fmt.Errorf("pinning bead: %w", err) } - fmt.Printf("%s Work attached to hook\n", style.Bold.Render("✓")) + fmt.Printf("%s Work attached to hook (pinned bead)\n", style.Bold.Render("✓")) return nil } diff --git a/internal/cmd/hook.go b/internal/cmd/hook.go index 6898d1bb..5c5d1ed4 100644 --- a/internal/cmd/hook.go +++ b/internal/cmd/hook.go @@ -7,7 +7,6 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/style" - "github.com/steveyegge/gastown/internal/wisp" ) var hookCmd = &cobra.Command{ @@ -64,38 +63,33 @@ func runHook(cmd *cobra.Command, args []string) error { return err } - // Determine agent identity and clone root - agentID, _, cloneRoot, err := resolveSelfTarget() + // Determine agent identity + agentID, _, _, err := resolveSelfTarget() if err != nil { return fmt.Errorf("detecting agent identity: %w", err) } - // Create the slung work wisp - sw := wisp.NewSlungWork(beadID, agentID) - sw.Subject = hookSubject - sw.Context = hookMessage - fmt.Printf("%s Hooking %s...\n", style.Bold.Render("🪝"), beadID) if hookDryRun { - fmt.Printf("Would create wisp: %s\n", wisp.HookPath(cloneRoot, agentID)) - fmt.Printf(" bead_id: %s\n", beadID) - fmt.Printf(" agent: %s\n", agentID) + fmt.Printf("Would run: bd update %s --status=pinned --assignee=%s\n", beadID, agentID) if hookSubject != "" { - fmt.Printf(" subject: %s\n", hookSubject) + fmt.Printf(" subject (for handoff mail): %s\n", hookSubject) } if hookMessage != "" { - fmt.Printf(" context: %s\n", hookMessage) + fmt.Printf(" context (for handoff mail): %s\n", hookMessage) } return nil } - // Write the wisp to the hook - if err := wisp.WriteSlungWork(cloneRoot, agentID, sw); err != nil { - return fmt.Errorf("writing wisp: %w", err) + // Pin the bead using bd update (discovery-based approach) + pinCmd := exec.Command("bd", "update", beadID, "--status=pinned", "--assignee="+agentID) + pinCmd.Stderr = os.Stderr + if err := pinCmd.Run(); err != nil { + return fmt.Errorf("pinning bead: %w", err) } - fmt.Printf("%s Work attached to hook\n", style.Bold.Render("✓")) + fmt.Printf("%s Work attached to hook (pinned bead)\n", style.Bold.Render("✓")) fmt.Printf(" Use 'gt handoff' to restart with this work\n") fmt.Printf(" Use 'gt mol status' to see hook status\n") diff --git a/internal/cmd/molecule_status.go b/internal/cmd/molecule_status.go index 0ff2ef10..b3c4c10e 100644 --- a/internal/cmd/molecule_status.go +++ b/internal/cmd/molecule_status.go @@ -258,16 +258,8 @@ func runMoleculeStatus(cmd *cobra.Command, args []string) error { HasWork: len(pinnedBeads) > 0, } - // Also check for wisp hook files (from gt hook/sling/handoff) - // These are stored at the git clone root, not the beads dir - gitRoot, _ := getGitRootForMolStatus() - if gitRoot != "" { - sw, err := wisp.ReadHook(gitRoot, target) - if err == nil && sw != nil { - status.SlungWork = sw - status.HasWork = true - } - } + // Note: Hook files are deprecated. Work is now tracked via pinned beads only. + // The SlungWork field is kept for backward compatibility but will be nil. if len(pinnedBeads) > 0 { // Take the first pinned bead (agents typically have one pinned bead) diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index 4154c492..fe4cff4d 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -15,7 +15,6 @@ import ( "github.com/steveyegge/gastown/internal/lock" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/templates" - "github.com/steveyegge/gastown/internal/wisp" "github.com/steveyegge/gastown/internal/workspace" ) @@ -533,20 +532,18 @@ func runMailCheckInject(workDir string) { // This is key for the autonomous overnight work pattern. // The Propulsion Principle: "If you find something on your hook, YOU RUN IT." func outputAttachmentStatus(ctx RoleContext) { - if ctx.Role != RoleCrew && ctx.Role != RolePolecat { + // Skip only unknown roles - all valid roles can have pinned work + if ctx.Role == RoleUnknown { return } // Check for pinned beads with attachments b := beads.New(ctx.WorkDir) - // Build assignee string based on role - var assignee string - switch ctx.Role { - case RoleCrew: - assignee = fmt.Sprintf("%s/crew/%s", ctx.Rig, ctx.Polecat) - case RolePolecat: - assignee = fmt.Sprintf("%s/%s", ctx.Rig, ctx.Polecat) + // Build assignee string based on role (same as getAgentIdentity) + assignee := getAgentIdentity(ctx) + if assignee == "" { + return } // Find pinned beads for this agent @@ -1271,78 +1268,41 @@ func outputRefineryPatrolContext(ctx RoleContext) { } } -// checkSlungWork checks for slung work on the agent's hook. +// checkSlungWork checks for pinned work on the agent's hook. // If found, displays it prominently and tells the agent to execute it. -// The wisp is burned after the agent acknowledges it. -// Returns true if slung work was found (caller should skip normal startup directive). +// Returns true if pinned work was found (caller should skip normal startup directive). func checkSlungWork(ctx RoleContext) bool { - // Determine agent identity for hook lookup + // Determine agent identity agentID := getAgentIdentity(ctx) if agentID == "" { return false } - // Get the git clone root (hooks are stored at clone root, not cwd) - cloneRoot, err := getGitRoot() - if err != nil { - // Not in a git repo - can't have hooks + // Check for pinned beads (the discovery-based hook) + b := beads.New(ctx.WorkDir) + pinnedBeads, err := b.List(beads.ListOptions{ + Status: beads.StatusPinned, + Assignee: agentID, + Priority: -1, + }) + if err != nil || len(pinnedBeads) == 0 { + // No pinned beads - no slung work return false } - sw, err := wisp.ReadHook(cloneRoot, agentID) - if err != nil { - if errors.Is(err, wisp.ErrNoHook) { - // No hook - normal case, nothing to do - return false - } - // Log other errors (permission, corruption) but continue - fmt.Printf("%s Warning: error reading hook: %v\n", style.Dim.Render("⚠"), err) - return false - } - - // Verify bead exists before showing autonomous mode - // Try multiple beads locations: cwd, clone root, and rig's beads dir - var stdout bytes.Buffer - beadExists := false - beadSearchDirs := []string{ctx.WorkDir, cloneRoot} - // For Mayor, also try the gastown rig's beads location - if ctx.Role == RoleMayor { - beadSearchDirs = append(beadSearchDirs, filepath.Join(ctx.TownRoot, "gastown", "mayor", "rig")) - } - for _, dir := range beadSearchDirs { - cmd := exec.Command("bd", "show", sw.BeadID) - cmd.Dir = dir - stdout.Reset() - cmd.Stdout = &stdout - cmd.Stderr = nil - if cmd.Run() == nil { - beadExists = true - break - } - } - - if !beadExists { - fmt.Println() - fmt.Printf("%s\n\n", style.Bold.Render("## 🎯 SLUNG WORK ON HOOK")) - fmt.Printf(" Bead ID: %s\n", style.Bold.Render(sw.BeadID)) - fmt.Printf(" %s Bead %s not found! It may have been deleted.\n", - style.Bold.Render("⚠ WARNING:"), sw.BeadID) - fmt.Println(" The hook will NOT be burned. Investigate this issue.") - fmt.Println() - // Don't burn - leave hook for debugging - return false - } + // Use the first pinned bead (agents typically have one) + pinnedBead := pinnedBeads[0] // Build the role announcement string roleAnnounce := buildRoleAnnouncement(ctx) - // Found slung work! Display AUTONOMOUS MODE prominently + // Found pinned work! Display AUTONOMOUS MODE prominently fmt.Println() fmt.Printf("%s\n\n", style.Bold.Render("## 🚨 AUTONOMOUS WORK MODE 🚨")) - fmt.Println("Work is slung on your hook. After announcing your role, begin IMMEDIATELY.") + fmt.Println("Work is pinned to your hook. After announcing your role, begin IMMEDIATELY.") fmt.Println() fmt.Println("1. Announce: \"" + roleAnnounce + "\" (ONE line, no elaboration)") - fmt.Printf("2. Then IMMEDIATELY run: `bd show %s`\n", sw.BeadID) + fmt.Printf("2. Then IMMEDIATELY run: `bd show %s`\n", pinnedBead.ID) fmt.Println("3. Begin execution - no waiting for user input") fmt.Println() fmt.Println("**DO NOT:**") @@ -1351,40 +1311,43 @@ func checkSlungWork(ctx RoleContext) bool { fmt.Println("- Describe what you're going to do") fmt.Println() - // Show the slung work details - fmt.Printf("%s\n\n", style.Bold.Render("## Slung Work")) - fmt.Printf(" Bead ID: %s\n", style.Bold.Render(sw.BeadID)) - if sw.Subject != "" { - fmt.Printf(" Subject: %s\n", sw.Subject) - } - if sw.Context != "" { - fmt.Printf(" Context: %s\n", sw.Context) - } - if sw.Args != "" { - fmt.Printf(" Args: %s\n", style.Bold.Render(sw.Args)) - fmt.Println() - fmt.Printf(" %s Use these args to guide your execution.\n", style.Bold.Render("→")) - } - fmt.Printf(" Slung by: %s at %s\n", sw.CreatedBy, sw.CreatedAt.Format("2006-01-02 15:04:05")) - fmt.Println() - - // Show bead preview (first 15 lines) - lines := strings.Split(stdout.String(), "\n") - maxLines := 15 - if len(lines) > maxLines { - lines = lines[:maxLines] - lines = append(lines, "...") - } - fmt.Println("**Bead preview:**") - for _, line := range lines { - fmt.Printf(" %s\n", line) + // Show the pinned work details + fmt.Printf("%s\n\n", style.Bold.Render("## Pinned Work")) + fmt.Printf(" Bead ID: %s\n", style.Bold.Render(pinnedBead.ID)) + fmt.Printf(" Title: %s\n", pinnedBead.Title) + if pinnedBead.Description != "" { + // Show first few lines of description + lines := strings.Split(pinnedBead.Description, "\n") + maxLines := 5 + if len(lines) > maxLines { + lines = lines[:maxLines] + lines = append(lines, "...") + } + fmt.Println(" Description:") + for _, line := range lines { + fmt.Printf(" %s\n", line) + } } fmt.Println() - // Burn the hook now that it's been read and verified - if err := wisp.BurnHook(cloneRoot, agentID); err != nil { - fmt.Printf("%s Warning: could not burn hook: %v\n", style.Dim.Render("⚠"), err) + // Show bead preview using bd show + fmt.Println("**Bead details:**") + cmd := exec.Command("bd", "show", pinnedBead.ID) + var stdout bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = nil + if cmd.Run() == nil { + lines := strings.Split(stdout.String(), "\n") + maxLines := 15 + if len(lines) > maxLines { + lines = lines[:maxLines] + lines = append(lines, "...") + } + for _, line := range lines { + fmt.Printf(" %s\n", line) + } } + fmt.Println() return true } diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go index d2d4e307..36d855d8 100644 --- a/internal/cmd/sling.go +++ b/internal/cmd/sling.go @@ -10,7 +10,6 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" - "github.com/steveyegge/gastown/internal/wisp" ) var slingCmd = &cobra.Command{ @@ -144,7 +143,6 @@ func runSling(cmd *cobra.Command, args []string) error { // Determine target agent (self or specified) var targetAgent string var targetPane string - var hookRoot string // Where to store the hook (role's home) var err error if len(args) > 1 { @@ -157,7 +155,6 @@ func runSling(cmd *cobra.Command, args []string) error { fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName) targetAgent = fmt.Sprintf("%s/polecats/", rigName) targetPane = "" - hookRoot = fmt.Sprintf("", rigName) } else { // Spawn a fresh polecat in the rig fmt.Printf("Target is rig '%s', spawning fresh polecat...\n", rigName) @@ -167,30 +164,22 @@ func runSling(cmd *cobra.Command, args []string) error { } targetAgent = spawnInfo.AgentID() targetPane = spawnInfo.Pane - hookRoot = spawnInfo.ClonePath } } else { // Slinging to an existing agent - targetAgent, targetPane, hookRoot, err = resolveTargetAgent(target) + targetAgent, targetPane, _, err = resolveTargetAgent(target) if err != nil { return fmt.Errorf("resolving target: %w", err) } } } else { // Slinging to self - targetAgent, targetPane, hookRoot, err = resolveSelfTarget() + targetAgent, targetPane, _, err = resolveSelfTarget() if err != nil { return err } } - // Create the slung work wisp - sw := wisp.NewSlungWork(beadID, targetAgent) - sw.Subject = slingSubject - sw.Context = slingMessage - sw.Formula = formulaName - sw.Args = slingArgs - // Display what we're doing if formulaName != "" { fmt.Printf("%s Slinging formula %s on %s to %s...\n", style.Bold.Render("🎯"), formulaName, beadID, targetAgent) @@ -199,32 +188,31 @@ func runSling(cmd *cobra.Command, args []string) error { } if slingDryRun { - fmt.Printf("Would create wisp: %s\n", wisp.HookPath(hookRoot, targetAgent)) - fmt.Printf(" bead_id: %s\n", beadID) + fmt.Printf("Would run: bd update %s --status=pinned --assignee=%s\n", beadID, targetAgent) if formulaName != "" { fmt.Printf(" formula: %s\n", formulaName) } - fmt.Printf(" agent: %s\n", targetAgent) - fmt.Printf(" hook_root: %s\n", hookRoot) if slingSubject != "" { - fmt.Printf(" subject: %s\n", slingSubject) + fmt.Printf(" subject (in nudge): %s\n", slingSubject) } if slingMessage != "" { fmt.Printf(" context: %s\n", slingMessage) } if slingArgs != "" { - fmt.Printf(" args: %s\n", slingArgs) + fmt.Printf(" args (in nudge): %s\n", slingArgs) } fmt.Printf("Would inject start prompt to pane: %s\n", targetPane) return nil } - // Write the wisp to the hook - if err := wisp.WriteSlungWork(hookRoot, targetAgent, sw); err != nil { - return fmt.Errorf("writing wisp: %w", err) + // Pin the bead using bd update (discovery-based approach) + pinCmd := exec.Command("bd", "update", beadID, "--status=pinned", "--assignee="+targetAgent) + pinCmd.Stderr = os.Stderr + if err := pinCmd.Run(); err != nil { + return fmt.Errorf("pinning bead: %w", err) } - fmt.Printf("%s Work attached to hook\n", style.Bold.Render("✓")) + fmt.Printf("%s Work attached to hook (pinned bead)\n", style.Bold.Render("✓")) // Inject the "start now" prompt if err := injectStartPrompt(targetPane, beadID, slingSubject, slingArgs); err != nil { @@ -406,7 +394,6 @@ func runSlingFormula(args []string) error { // Resolve target agent and pane var targetAgent string var targetPane string - var hookRoot string var err error if target != "" { @@ -417,7 +404,6 @@ func runSlingFormula(args []string) error { fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName) targetAgent = fmt.Sprintf("%s/polecats/", rigName) targetPane = "" - hookRoot = fmt.Sprintf("", rigName) } else { // Spawn a fresh polecat in the rig fmt.Printf("Target is rig '%s', spawning fresh polecat...\n", rigName) @@ -427,18 +413,17 @@ func runSlingFormula(args []string) error { } targetAgent = spawnInfo.AgentID() targetPane = spawnInfo.Pane - hookRoot = spawnInfo.ClonePath } } else { // Slinging to an existing agent - targetAgent, targetPane, hookRoot, err = resolveTargetAgent(target) + targetAgent, targetPane, _, err = resolveTargetAgent(target) if err != nil { return fmt.Errorf("resolving target: %w", err) } } } else { // Slinging to self - targetAgent, targetPane, hookRoot, err = resolveSelfTarget() + targetAgent, targetPane, _, err = resolveSelfTarget() if err != nil { return err } @@ -448,7 +433,7 @@ func runSlingFormula(args []string) error { if slingDryRun { fmt.Printf("Would cook formula: %s\n", formulaName) - fmt.Printf("Would create wisp and attach to hook: %s\n", wisp.HookPath(hookRoot, targetAgent)) + fmt.Printf("Would create wisp and pin to: %s\n", targetAgent) for _, v := range slingVars { fmt.Printf(" --var %s\n", v) } @@ -492,20 +477,13 @@ func runSlingFormula(args []string) error { fmt.Printf("%s Wisp created: %s\n", style.Bold.Render("✓"), wispResult.RootID) - // Step 3: Attach to hook - sw := wisp.NewSlungWork(wispResult.RootID, targetAgent) - sw.Subject = slingSubject - if sw.Subject == "" { - sw.Subject = fmt.Sprintf("Formula: %s", formulaName) + // Step 3: Pin the wisp bead using bd update (discovery-based approach) + pinCmd := exec.Command("bd", "update", wispResult.RootID, "--status=pinned", "--assignee="+targetAgent) + pinCmd.Stderr = os.Stderr + if err := pinCmd.Run(); err != nil { + return fmt.Errorf("pinning wisp bead: %w", err) } - sw.Context = slingMessage - sw.Formula = formulaName - sw.Args = slingArgs - - if err := wisp.WriteSlungWork(hookRoot, targetAgent, sw); err != nil { - return fmt.Errorf("writing to hook: %w", err) - } - fmt.Printf("%s Attached to hook\n", style.Bold.Render("✓")) + fmt.Printf("%s Attached to hook (pinned bead)\n", style.Bold.Render("✓")) // Step 4: Nudge to start if targetPane == "" { diff --git a/internal/wisp/io.go b/internal/wisp/io.go index b7ce9b9c..df43a3f0 100644 --- a/internal/wisp/io.go +++ b/internal/wisp/io.go @@ -35,6 +35,10 @@ func HookPath(root, agent string) string { } // WriteSlungWork writes a slung work hook to the agent's hook file. +// +// Deprecated: Hook files are deprecated. Use bd update --status=pinned instead. +// Work is now tracked via pinned beads (discoverable via query) rather than +// explicit hook files. This function is kept for backward compatibility. func WriteSlungWork(root, agent string, sw *SlungWork) error { dir, err := EnsureDir(root) if err != nil { @@ -47,6 +51,9 @@ func WriteSlungWork(root, agent string, sw *SlungWork) error { // ReadHook reads the slung work from an agent's hook file. // Returns ErrNoHook if no hook file exists. +// +// Deprecated: Hook files are deprecated. Query pinned beads instead. +// Use beads.List with Status=pinned and Assignee=agent. func ReadHook(root, agent string) (*SlungWork, error) { path := HookPath(root, agent) @@ -71,6 +78,9 @@ func ReadHook(root, agent string) (*SlungWork, error) { } // BurnHook removes an agent's hook file after it has been picked up. +// +// Deprecated: Hook files are deprecated. Work is tracked via pinned beads +// which don't need burning - just unpin with bd update --status=open. func BurnHook(root, agent string) error { path := HookPath(root, agent) err := os.Remove(path) @@ -81,6 +91,8 @@ func BurnHook(root, agent string) error { } // HasHook checks if an agent has a hook file. +// +// Deprecated: Hook files are deprecated. Query pinned beads instead. func HasHook(root, agent string) bool { path := HookPath(root, agent) _, err := os.Stat(path) @@ -88,6 +100,8 @@ func HasHook(root, agent string) bool { } // ListHooks returns a list of agents with active hooks. +// +// Deprecated: Hook files are deprecated. Query pinned beads instead. func ListHooks(root string) ([]string, error) { dir := filepath.Join(root, WispDir) entries, err := os.ReadDir(dir) diff --git a/internal/wisp/types.go b/internal/wisp/types.go index e9409230..9af6751c 100644 --- a/internal/wisp/types.go +++ b/internal/wisp/types.go @@ -1,12 +1,22 @@ // Package wisp provides hook file support for Gas Town agents. // -// Hooks are used to attach work to an agent for restart-and-resume: -// - hook-.json files track what bead is assigned to an agent +// DEPRECATED: Hook files are deprecated in favor of pinned beads. +// Work is now tracked via beads with status=pinned and assignee=agent, +// which can be discovered via query rather than explicit file management. +// +// Commands like `gt hook`, `gt sling`, `gt handoff` now use: +// +// bd update --status=pinned --assignee= +// +// On session start, agents query for pinned beads rather than reading hook files. +// This follows Gas Town's "discovery over explicit state" principle. +// +// The hook file functions are kept for backward compatibility but are deprecated. +// Old hook files: +// - hook-.json files tracked what bead was assigned to an agent // - Created by `gt hook`, `gt sling`, `gt handoff` // - Read on session start to restore work context // - Burned after pickup -// -// Hook files live in .beads/ alongside other beads data. package wisp import (