From 1a1ab4842b16660f0ee98b4b124bd259c7f19c14 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 26 Dec 2025 16:46:39 -0800 Subject: [PATCH] Eradicate hook files, use pinned beads only (gt-rgd9x) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove hook functions from internal/wisp/io.go (WriteSlungWork, ReadHook, BurnHook, etc.) - Remove hook types from internal/wisp/types.go (SlungWork, Wisp, etc.) - Update up.go to query pinned beads instead of reading hook files - Remove SlungWork field from molecule_status.go - Remove hook-*.json pattern from .beads/.gitignore - Delete live hook file /Users/stevey/gt/deacon/.beads/hook-deacon.json Work is now tracked exclusively via pinned beads (status=pinned, assignee=agent). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .beads/.gitignore | 3 - internal/cmd/molecule_status.go | 36 +--------- internal/cmd/up.go | 39 ++++------- internal/wisp/io.go | 100 --------------------------- internal/wisp/types.go | 118 ++------------------------------ 5 files changed, 21 insertions(+), 275 deletions(-) diff --git a/.beads/.gitignore b/.beads/.gitignore index 6d571347..cf04f33a 100644 --- a/.beads/.gitignore +++ b/.beads/.gitignore @@ -31,9 +31,6 @@ beads.right.meta.json # bd sync commits to beads-sync, which has its own .gitignore that tracks issues.jsonl issues.jsonl -# Hook files (ephemeral, per-agent work attachments) -hook-*.json - # MR queue (ephemeral, deleted after merge) mq/ diff --git a/internal/cmd/molecule_status.go b/internal/cmd/molecule_status.go index b3c4c10e..d2a3f579 100644 --- a/internal/cmd/molecule_status.go +++ b/internal/cmd/molecule_status.go @@ -10,7 +10,6 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/style" - "github.com/steveyegge/gastown/internal/wisp" "github.com/steveyegge/gastown/internal/workspace" ) @@ -39,8 +38,6 @@ type MoleculeStatusInfo struct { IsWisp bool `json:"is_wisp"` Progress *MoleculeProgressInfo `json:"progress,omitempty"` NextAction string `json:"next_action,omitempty"` - // SlungWork is set when there's a wisp hook file (from gt hook/sling/handoff) - SlungWork *wisp.SlungWork `json:"slung_work,omitempty"` } // MoleculeCurrentInfo contains info about what an agent should be working on. @@ -258,9 +255,6 @@ func runMoleculeStatus(cmd *cobra.Command, args []string) error { HasWork: len(pinnedBeads) > 0, } - // 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) status.PinnedBead = pinnedBeads[0] @@ -445,35 +439,7 @@ func outputMoleculeStatus(status MoleculeStatusInfo) error { return nil } - // Show slung work (wisp hook file) if present - if status.SlungWork != nil { - fmt.Printf("%s %s\n", style.Bold.Render("🎯 SLUNG WORK:"), status.SlungWork.BeadID) - if status.SlungWork.Subject != "" { - fmt.Printf(" Subject: %s\n", status.SlungWork.Subject) - } - if status.SlungWork.Context != "" { - fmt.Printf(" Context: %s\n", status.SlungWork.Context) - } - if status.SlungWork.Args != "" { - fmt.Printf(" Args: %s\n", style.Bold.Render(status.SlungWork.Args)) - } - fmt.Printf(" Slung by: %s at %s\n", - status.SlungWork.CreatedBy, - status.SlungWork.CreatedAt.Format("2006-01-02 15:04:05")) - - // Show bead details - fmt.Println() - fmt.Println(style.Bold.Render("Bead details:")) - cmd := exec.Command("bd", "show", status.SlungWork.BeadID) - cmd.Stdout = os.Stdout - cmd.Run() - - fmt.Println() - fmt.Println(style.Bold.Render("→ PROPULSION: Work is on your hook. RUN IT.")) - return nil - } - - // Show pinned bead info (legacy beads pinned field) + // Show pinned bead info if status.PinnedBead == nil { fmt.Printf("%s\n", style.Dim.Render("Work indicated but no bead found")) return nil diff --git a/internal/cmd/up.go b/internal/cmd/up.go index 003874f1..6cab235d 100644 --- a/internal/cmd/up.go +++ b/internal/cmd/up.go @@ -1,7 +1,6 @@ package cmd import ( - "encoding/json" "fmt" "os" "os/exec" @@ -10,12 +9,12 @@ import ( "time" "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/daemon" "github.com/steveyegge/gastown/internal/refinery" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" - "github.com/steveyegge/gastown/internal/wisp" "github.com/steveyegge/gastown/internal/workspace" ) @@ -39,7 +38,7 @@ spawned on demand by the Mayor or Witnesses. Use --restore to also start: • Crew - Per rig settings (settings/config.json crew.startup) - • Polecats - Those with hooks (work attached) + • Polecats - Those with pinned beads (work attached) Running 'gt up' multiple times is safe - it only starts services that aren't already running.`, @@ -144,9 +143,9 @@ func runUp(cmd *cobra.Command, args []string) error { } } - // 7. Polecats with hooks (if --restore) + // 7. Polecats with pinned work (if --restore) for _, rigName := range rigs { - polecatsStarted, polecatErrors := startPolecatsWithHooks(t, townRoot, rigName) + polecatsStarted, polecatErrors := startPolecatsWithWork(t, townRoot, rigName) for _, name := range polecatsStarted { printStatus(fmt.Sprintf("Polecat (%s/%s)", rigName, name), true, fmt.Sprintf("gt-%s-polecat-%s", rigName, name)) } @@ -512,9 +511,9 @@ func ensureCrewSession(t *tmux.Tmux, sessionName, crewPath, rigName, crewName st return nil } -// startPolecatsWithHooks starts polecats that have hook files (work attached). +// startPolecatsWithWork starts polecats that have pinned beads (work attached). // Returns list of started polecat names and map of errors. -func startPolecatsWithHooks(t *tmux.Tmux, townRoot, rigName string) ([]string, map[string]error) { +func startPolecatsWithWork(t *tmux.Tmux, townRoot, rigName string) ([]string, map[string]error) { started := []string{} errors := map[string]error{} @@ -536,24 +535,16 @@ func startPolecatsWithHooks(t *tmux.Tmux, townRoot, rigName string) ([]string, m polecatName := entry.Name() polecatPath := filepath.Join(polecatsDir, polecatName) - // Check if this polecat has a hook file + // Check if this polecat has a pinned bead (work attached) agentID := fmt.Sprintf("%s/polecats/%s", rigName, polecatName) - hookPath := filepath.Join(polecatPath, ".beads", wisp.HookFilename(agentID)) - - hookData, err := os.ReadFile(hookPath) - if err != nil { - // No hook file - skip - continue - } - - // Verify hook has work - var hook wisp.SlungWork - if err := json.Unmarshal(hookData, &hook); err != nil { - continue - } - - if hook.BeadID == "" { - // Empty hook - skip + b := beads.New(polecatPath) + pinnedBeads, err := b.List(beads.ListOptions{ + Status: beads.StatusPinned, + Assignee: agentID, + Priority: -1, + }) + if err != nil || len(pinnedBeads) == 0 { + // No pinned beads - skip continue } diff --git a/internal/wisp/io.go b/internal/wisp/io.go index df43a3f0..dbe19f39 100644 --- a/internal/wisp/io.go +++ b/internal/wisp/io.go @@ -2,19 +2,11 @@ package wisp import ( "encoding/json" - "errors" "fmt" "os" "path/filepath" ) -// Common errors. -var ( - ErrNoWispDir = errors.New("beads directory does not exist") - ErrNoHook = errors.New("no hook file found") - ErrInvalidWisp = errors.New("invalid hook file format") -) - // EnsureDir ensures the .beads directory exists in the given root. func EnsureDir(root string) (string, error) { dir := filepath.Join(root, WispDir) @@ -29,98 +21,6 @@ func WispPath(root, filename string) string { return filepath.Join(root, WispDir, filename) } -// HookPath returns the full path to an agent's hook file. -func HookPath(root, agent string) string { - return WispPath(root, HookFilename(agent)) -} - -// 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 { - return err - } - - path := filepath.Join(dir, HookFilename(agent)) - return writeJSON(path, sw) -} - -// 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) - - data, err := os.ReadFile(path) - if os.IsNotExist(err) { - return nil, ErrNoHook - } - if err != nil { - return nil, fmt.Errorf("read hook: %w", err) - } - - var sw SlungWork - if err := json.Unmarshal(data, &sw); err != nil { - return nil, fmt.Errorf("%w: %v", ErrInvalidWisp, err) - } - - if sw.Type != TypeSlungWork { - return nil, fmt.Errorf("%w: expected slung-work, got %s", ErrInvalidWisp, sw.Type) - } - - return &sw, nil -} - -// 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) - if os.IsNotExist(err) { - return nil // already burned - } - return err -} - -// 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) - return err == nil -} - -// 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) - if os.IsNotExist(err) { - return nil, nil - } - if err != nil { - return nil, err - } - - var agents []string - for _, e := range entries { - if agent := AgentFromHookFilename(e.Name()); agent != "" { - agents = append(agents, agent) - } - } - return agents, nil -} - // writeJSON is a helper to write JSON files atomically. func writeJSON(path string, v interface{}) error { data, err := json.MarshalIndent(v, "", " ") diff --git a/internal/wisp/types.go b/internal/wisp/types.go index 9af6751c..77e47c19 100644 --- a/internal/wisp/types.go +++ b/internal/wisp/types.go @@ -1,117 +1,9 @@ -// Package wisp provides hook file support for Gas Town agents. +// Package wisp provides utilities for working with the .beads directory. // -// 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 +// This package was originally for "hook files" but those are now deprecated +// in favor of pinned beads. The remaining utilities help with directory +// management for the beads system. package wisp -import ( - "strings" - "time" -) - -// WispType identifies the kind of hook file. -type WispType string - -const ( - // TypeSlungWork is a hook that attaches a bead to an agent's hook. - // Created by `gt hook`, `gt sling`, or `gt handoff`, and burned after pickup. - TypeSlungWork WispType = "slung-work" -) - -// WispDir is the directory where hook files are stored. -// Hook files (hook-.json) live alongside other beads data. +// WispDir is the directory where beads data is stored. const WispDir = ".beads" - -// HookPrefix is the filename prefix for hook files. -const HookPrefix = "hook-" - -// HookSuffix is the filename suffix for hook files. -const HookSuffix = ".json" - -// Wisp is the common header for hook files. -type Wisp struct { - // Type identifies what kind of hook file this is. - Type WispType `json:"type"` - - // CreatedAt is when the hook was created. - CreatedAt time.Time `json:"created_at"` - - // CreatedBy identifies who created the hook (e.g., "crew/joe", "deacon"). - CreatedBy string `json:"created_by"` -} - -// SlungWork represents work attached to an agent's hook. -// Created by `gt hook`, `gt sling`, or `gt handoff` and burned after pickup. -type SlungWork struct { - Wisp - - // BeadID is the issue/bead to work on (e.g., "gt-xxx"). - BeadID string `json:"bead_id"` - - // Formula is the optional formula/form to apply to the work. - // When set, this creates scaffolding around the target bead. - // Used by `gt sling --on `. - Formula string `json:"formula,omitempty"` - - // Context is optional additional context from the slinger. - Context string `json:"context,omitempty"` - - // Subject is optional subject line (used in handoff mail). - Subject string `json:"subject,omitempty"` - - // Args is optional natural language instructions for the formula executor. - // Example: "patch release" or "focus on security issues" - // The LLM executor interprets these instructions - no schema needed. - Args string `json:"args,omitempty"` -} - -// NewSlungWork creates a new slung work hook file. -func NewSlungWork(beadID, createdBy string) *SlungWork { - return &SlungWork{ - Wisp: Wisp{ - Type: TypeSlungWork, - CreatedAt: time.Now(), - CreatedBy: createdBy, - }, - BeadID: beadID, - } -} - -// HookFilename returns the filename for an agent's hook file. -// Agent identities may contain slashes (e.g., "gastown/crew/max"), -// which are replaced with underscores to create valid filenames. -func HookFilename(agent string) string { - safe := strings.ReplaceAll(agent, "/", "_") - return HookPrefix + safe + HookSuffix -} - -// AgentFromHookFilename extracts the agent identity from a hook filename. -// Reverses the slash-to-underscore transformation done by HookFilename. -func AgentFromHookFilename(filename string) string { - if len(filename) <= len(HookPrefix)+len(HookSuffix) { - return "" - } - if filename[:len(HookPrefix)] != HookPrefix { - return "" - } - if filename[len(filename)-len(HookSuffix):] != HookSuffix { - return "" - } - safe := filename[len(HookPrefix) : len(filename)-len(HookSuffix)] - return strings.ReplaceAll(safe, "_", "/") -}