Eradicate hook files, use pinned beads only (gt-rgd9x)

- 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 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-26 16:46:39 -08:00
parent 3c0208fc02
commit 1a1ab4842b
5 changed files with 21 additions and 275 deletions

View File

@@ -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, "", " ")

View File

@@ -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 <bead> --status=pinned --assignee=<agent>
//
// 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-<agent>.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-<agent>.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 <formula> --on <bead>`.
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, "_", "/")
}