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:
3
.beads/.gitignore
vendored
3
.beads/.gitignore
vendored
@@ -31,9 +31,6 @@ beads.right.meta.json
|
|||||||
# bd sync commits to beads-sync, which has its own .gitignore that tracks issues.jsonl
|
# bd sync commits to beads-sync, which has its own .gitignore that tracks issues.jsonl
|
||||||
issues.jsonl
|
issues.jsonl
|
||||||
|
|
||||||
# Hook files (ephemeral, per-agent work attachments)
|
|
||||||
hook-*.json
|
|
||||||
|
|
||||||
# MR queue (ephemeral, deleted after merge)
|
# MR queue (ephemeral, deleted after merge)
|
||||||
mq/
|
mq/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/gastown/internal/beads"
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/wisp"
|
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,8 +38,6 @@ type MoleculeStatusInfo struct {
|
|||||||
IsWisp bool `json:"is_wisp"`
|
IsWisp bool `json:"is_wisp"`
|
||||||
Progress *MoleculeProgressInfo `json:"progress,omitempty"`
|
Progress *MoleculeProgressInfo `json:"progress,omitempty"`
|
||||||
NextAction string `json:"next_action,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.
|
// 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,
|
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 {
|
if len(pinnedBeads) > 0 {
|
||||||
// Take the first pinned bead (agents typically have one pinned bead)
|
// Take the first pinned bead (agents typically have one pinned bead)
|
||||||
status.PinnedBead = pinnedBeads[0]
|
status.PinnedBead = pinnedBeads[0]
|
||||||
@@ -445,35 +439,7 @@ func outputMoleculeStatus(status MoleculeStatusInfo) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show slung work (wisp hook file) if present
|
// Show pinned bead info
|
||||||
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)
|
|
||||||
if status.PinnedBead == nil {
|
if status.PinnedBead == nil {
|
||||||
fmt.Printf("%s\n", style.Dim.Render("Work indicated but no bead found"))
|
fmt.Printf("%s\n", style.Dim.Render("Work indicated but no bead found"))
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -10,12 +9,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
"github.com/steveyegge/gastown/internal/daemon"
|
"github.com/steveyegge/gastown/internal/daemon"
|
||||||
"github.com/steveyegge/gastown/internal/refinery"
|
"github.com/steveyegge/gastown/internal/refinery"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
"github.com/steveyegge/gastown/internal/wisp"
|
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ spawned on demand by the Mayor or Witnesses.
|
|||||||
|
|
||||||
Use --restore to also start:
|
Use --restore to also start:
|
||||||
• Crew - Per rig settings (settings/config.json crew.startup)
|
• 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
|
Running 'gt up' multiple times is safe - it only starts services that
|
||||||
aren't already running.`,
|
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 {
|
for _, rigName := range rigs {
|
||||||
polecatsStarted, polecatErrors := startPolecatsWithHooks(t, townRoot, rigName)
|
polecatsStarted, polecatErrors := startPolecatsWithWork(t, townRoot, rigName)
|
||||||
for _, name := range polecatsStarted {
|
for _, name := range polecatsStarted {
|
||||||
printStatus(fmt.Sprintf("Polecat (%s/%s)", rigName, name), true, fmt.Sprintf("gt-%s-polecat-%s", rigName, name))
|
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
|
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.
|
// 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{}
|
started := []string{}
|
||||||
errors := map[string]error{}
|
errors := map[string]error{}
|
||||||
|
|
||||||
@@ -536,24 +535,16 @@ func startPolecatsWithHooks(t *tmux.Tmux, townRoot, rigName string) ([]string, m
|
|||||||
polecatName := entry.Name()
|
polecatName := entry.Name()
|
||||||
polecatPath := filepath.Join(polecatsDir, polecatName)
|
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)
|
agentID := fmt.Sprintf("%s/polecats/%s", rigName, polecatName)
|
||||||
hookPath := filepath.Join(polecatPath, ".beads", wisp.HookFilename(agentID))
|
b := beads.New(polecatPath)
|
||||||
|
pinnedBeads, err := b.List(beads.ListOptions{
|
||||||
hookData, err := os.ReadFile(hookPath)
|
Status: beads.StatusPinned,
|
||||||
if err != nil {
|
Assignee: agentID,
|
||||||
// No hook file - skip
|
Priority: -1,
|
||||||
continue
|
})
|
||||||
}
|
if err != nil || len(pinnedBeads) == 0 {
|
||||||
|
// No pinned beads - skip
|
||||||
// Verify hook has work
|
|
||||||
var hook wisp.SlungWork
|
|
||||||
if err := json.Unmarshal(hookData, &hook); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if hook.BeadID == "" {
|
|
||||||
// Empty hook - skip
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,11 @@ package wisp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"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.
|
// EnsureDir ensures the .beads directory exists in the given root.
|
||||||
func EnsureDir(root string) (string, error) {
|
func EnsureDir(root string) (string, error) {
|
||||||
dir := filepath.Join(root, WispDir)
|
dir := filepath.Join(root, WispDir)
|
||||||
@@ -29,98 +21,6 @@ func WispPath(root, filename string) string {
|
|||||||
return filepath.Join(root, WispDir, filename)
|
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.
|
// writeJSON is a helper to write JSON files atomically.
|
||||||
func writeJSON(path string, v interface{}) error {
|
func writeJSON(path string, v interface{}) error {
|
||||||
data, err := json.MarshalIndent(v, "", " ")
|
data, err := json.MarshalIndent(v, "", " ")
|
||||||
|
|||||||
@@ -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.
|
// This package was originally for "hook files" but those are now deprecated
|
||||||
// Work is now tracked via beads with status=pinned and assignee=agent,
|
// in favor of pinned beads. The remaining utilities help with directory
|
||||||
// which can be discovered via query rather than explicit file management.
|
// management for the beads system.
|
||||||
//
|
|
||||||
// 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
|
|
||||||
package wisp
|
package wisp
|
||||||
|
|
||||||
import (
|
// WispDir is the directory where beads data is stored.
|
||||||
"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.
|
|
||||||
const WispDir = ".beads"
|
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, "_", "/")
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user