refactor: split gt sling into gt hook + gt handoff <bead> (gt-z4bw)
- Add gt hook <bead>: durability primitive, attaches work to hook - Update gt handoff: accept optional bead arg (detects bead vs role) - Deprecate gt sling: shows warning, points to new commands - Update doctor fix hint to reference new commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
176
internal/cmd/hook.go
Normal file
176
internal/cmd/hook.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/wisp"
|
||||
)
|
||||
|
||||
var hookCmd = &cobra.Command{
|
||||
Use: "hook <bead-id>",
|
||||
Short: "Attach work to your hook (durable across restarts)",
|
||||
Long: `Attach a bead (issue) to your hook for durable work tracking.
|
||||
|
||||
The hook is a lightweight, ephemeral attachment point. When you restart
|
||||
(via gt handoff), your SessionStart hook finds the slung work and you
|
||||
continue from where you left off.
|
||||
|
||||
This is the "durability primitive" - work on your hook survives session
|
||||
restarts. For the full restart-and-resume flow, use: gt handoff <bead>
|
||||
|
||||
Examples:
|
||||
gt hook gt-abc # Attach issue gt-abc to your hook
|
||||
gt hook gt-abc -s "Fix the bug" # With subject for handoff mail
|
||||
gt hook gt-abc -m "Check tests" # With context message
|
||||
|
||||
Related commands:
|
||||
gt mol status # See what's on your hook
|
||||
gt handoff <bead> # Hook + restart in one step
|
||||
gt handoff # Restart (uses existing hook if present)`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runHook,
|
||||
}
|
||||
|
||||
var (
|
||||
hookSubject string
|
||||
hookMessage string
|
||||
hookDryRun bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
hookCmd.Flags().StringVarP(&hookSubject, "subject", "s", "", "Subject for handoff mail (optional)")
|
||||
hookCmd.Flags().StringVarP(&hookMessage, "message", "m", "", "Message for handoff mail (optional)")
|
||||
hookCmd.Flags().BoolVarP(&hookDryRun, "dry-run", "n", false, "Show what would be done")
|
||||
rootCmd.AddCommand(hookCmd)
|
||||
}
|
||||
|
||||
func runHook(cmd *cobra.Command, args []string) error {
|
||||
beadID := args[0]
|
||||
|
||||
// Polecats cannot hook - they use gt done for lifecycle
|
||||
if polecatName := os.Getenv("GT_POLECAT"); polecatName != "" {
|
||||
return fmt.Errorf("polecats cannot hook work (use gt done for handoff)")
|
||||
}
|
||||
|
||||
// Verify the bead exists
|
||||
if err := verifyBeadExists(beadID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine agent identity
|
||||
agentID, err := detectAgentIdentity()
|
||||
if err != nil {
|
||||
return fmt.Errorf("detecting agent identity: %w", err)
|
||||
}
|
||||
|
||||
// Get cwd for wisp storage (use clone root, not town root)
|
||||
cloneRoot, err := detectCloneRoot()
|
||||
if err != nil {
|
||||
return fmt.Errorf("detecting clone root: %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)
|
||||
if hookSubject != "" {
|
||||
fmt.Printf(" subject: %s\n", hookSubject)
|
||||
}
|
||||
if hookMessage != "" {
|
||||
fmt.Printf(" context: %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)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Work attached to hook\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")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyBeadExists checks that the bead exists using bd show.
|
||||
// Defined in sling.go but duplicated here for clarity. Will be consolidated
|
||||
// when sling.go is removed.
|
||||
func verifyBeadExistsForHook(beadID string) error {
|
||||
cmd := exec.Command("bd", "show", beadID, "--json")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("bead '%s' not found (bd show failed)", beadID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectAgentIdentityForHook figures out who we are (crew/joe, witness, etc).
|
||||
// Duplicated from sling.go - will be consolidated when sling.go is removed.
|
||||
func detectAgentIdentityForHook() (string, error) {
|
||||
// Check environment first
|
||||
if crew := os.Getenv("GT_CREW"); crew != "" {
|
||||
if rig := os.Getenv("GT_RIG"); rig != "" {
|
||||
return fmt.Sprintf("%s/crew/%s", rig, crew), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're a polecat
|
||||
if polecat := os.Getenv("GT_POLECAT"); polecat != "" {
|
||||
if rig := os.Getenv("GT_RIG"); rig != "" {
|
||||
return fmt.Sprintf("%s/polecats/%s", rig, polecat), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to detect from cwd
|
||||
detected, err := detectCrewFromCwd()
|
||||
if err == nil {
|
||||
return fmt.Sprintf("%s/crew/%s", detected.rigName, detected.crewName), nil
|
||||
}
|
||||
|
||||
// Check for other role markers in session name
|
||||
if session := os.Getenv("TMUX"); session != "" {
|
||||
sessionName, err := getCurrentTmuxSession()
|
||||
if err == nil {
|
||||
if sessionName == "gt-mayor" {
|
||||
return "mayor", nil
|
||||
}
|
||||
if sessionName == "gt-deacon" {
|
||||
return "deacon", nil
|
||||
}
|
||||
if strings.HasSuffix(sessionName, "-witness") {
|
||||
rig := strings.TrimSuffix(strings.TrimPrefix(sessionName, "gt-"), "-witness")
|
||||
return fmt.Sprintf("%s/witness", rig), nil
|
||||
}
|
||||
if strings.HasSuffix(sessionName, "-refinery") {
|
||||
rig := strings.TrimSuffix(strings.TrimPrefix(sessionName, "gt-"), "-refinery")
|
||||
return fmt.Sprintf("%s/refinery", rig), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("cannot determine agent identity - set GT_RIG/GT_CREW or run from clone directory")
|
||||
}
|
||||
|
||||
// detectCloneRootForHook finds the root of the current git clone.
|
||||
// Duplicated from sling.go - will be consolidated when sling.go is removed.
|
||||
func detectCloneRootForHook() (string, error) {
|
||||
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("not in a git repository")
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
Reference in New Issue
Block a user