refactor: remove unused bd pin/unpin/hook commands (bd-x0zl)

Analysis found these commands are dead code:
- gt never calls `bd pin` - uses `bd update --status=pinned` instead
- Beads.Pin() wrapper exists but is never called
- bd hook functionality duplicated by gt mol status
- Code comment says "pinned field is cosmetic for bd hook visibility"

Removed:
- cmd/bd/pin.go
- cmd/bd/unpin.go
- cmd/bd/hook.go

🤖 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-27 16:02:15 -08:00
parent c8b912cbe6
commit 1611f16751
178 changed files with 10291 additions and 1682 deletions

340
internal/routing/routes.go Normal file
View File

@@ -0,0 +1,340 @@
package routing
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/sqlite"
)
// RoutesFileName is the name of the routes configuration file
const RoutesFileName = "routes.jsonl"
// Route represents a prefix-to-path routing rule
type Route struct {
Prefix string `json:"prefix"` // Issue ID prefix (e.g., "gt-")
Path string `json:"path"` // Relative path to .beads directory
}
// LoadRoutes loads routes from routes.jsonl in the given beads directory.
// Returns an empty slice if the file doesn't exist.
func LoadRoutes(beadsDir string) ([]Route, error) {
routesPath := filepath.Join(beadsDir, RoutesFileName)
file, err := os.Open(routesPath) //nolint:gosec // routesPath is constructed from known beadsDir
if err != nil {
if os.IsNotExist(err) {
return nil, nil // No routes file is not an error
}
return nil, err
}
defer file.Close()
var routes []Route
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue // Skip empty lines and comments
}
var route Route
if err := json.Unmarshal([]byte(line), &route); err != nil {
continue // Skip malformed lines
}
if route.Prefix != "" && route.Path != "" {
routes = append(routes, route)
}
}
return routes, scanner.Err()
}
// ExtractPrefix extracts the prefix from an issue ID.
// For "gt-abc123", returns "gt-".
// For "bd-abc123", returns "bd-".
// Returns empty string if no prefix found.
func ExtractPrefix(id string) string {
idx := strings.Index(id, "-")
if idx < 0 {
return ""
}
return id[:idx+1] // Include the hyphen
}
// ExtractProjectFromPath extracts the project name from a route path.
// For "beads/mayor/rig", returns "beads".
// For "gastown/crew/max", returns "gastown".
func ExtractProjectFromPath(path string) string {
// Get the first component of the path
parts := strings.Split(path, "/")
if len(parts) > 0 && parts[0] != "" {
return parts[0]
}
return ""
}
// LookupRigByName finds a route by rig name (first path component).
// For example, LookupRigByName("beads", beadsDir) would find the route
// with path "beads/mayor/rig" and return it.
//
// Returns the matching route and true if found, or zero Route and false if not.
func LookupRigByName(rigName, beadsDir string) (Route, bool) {
routes, err := LoadRoutes(beadsDir)
if err != nil || len(routes) == 0 {
return Route{}, false
}
for _, route := range routes {
project := ExtractProjectFromPath(route.Path)
if project == rigName {
return route, true
}
}
return Route{}, false
}
// LookupRigForgiving finds a route using flexible matching.
// Accepts any of these formats and normalizes them:
// - "bd-" (exact prefix)
// - "bd" (prefix without hyphen, will try "bd-")
// - "beads" (rig name)
//
// This provides good agent UX - meet them where they are.
func LookupRigForgiving(input, beadsDir string) (Route, bool) {
routes, err := LoadRoutes(beadsDir)
if err != nil || len(routes) == 0 {
return Route{}, false
}
// Normalize: remove trailing hyphen for comparison
normalized := strings.TrimSuffix(input, "-")
for _, route := range routes {
// Try exact prefix match (with or without hyphen)
prefixBase := strings.TrimSuffix(route.Prefix, "-")
if normalized == prefixBase || input == route.Prefix {
return route, true
}
// Try rig name match
project := ExtractProjectFromPath(route.Path)
if input == project {
return route, true
}
}
return Route{}, false
}
// ResolveBeadsDirForRig returns the beads directory for a given rig identifier.
// This is used by --rig and --prefix flags to create issues in a different rig.
//
// The input is forgiving - accepts any of:
// - "beads", "gastown" (rig names)
// - "bd-", "gt-" (exact prefixes)
// - "bd", "gt" (prefixes without hyphen)
//
// Parameters:
// - rigOrPrefix: rig name or prefix in any format
// - currentBeadsDir: the current .beads directory (used to find routes.jsonl)
//
// Returns:
// - beadsDir: the target .beads directory path
// - prefix: the issue prefix for that rig (e.g., "bd-")
// - err: error if rig not found or path doesn't exist
func ResolveBeadsDirForRig(rigOrPrefix, currentBeadsDir string) (beadsDir string, prefix string, err error) {
route, found := LookupRigForgiving(rigOrPrefix, currentBeadsDir)
if !found {
return "", "", fmt.Errorf("rig or prefix %q not found in routes.jsonl", rigOrPrefix)
}
// Resolve the target beads directory
projectRoot := filepath.Dir(currentBeadsDir)
targetPath := filepath.Join(projectRoot, route.Path, ".beads")
// Follow redirect if present
targetPath = resolveRedirect(targetPath)
// Verify the target exists
if info, statErr := os.Stat(targetPath); statErr != nil || !info.IsDir() {
return "", "", fmt.Errorf("rig %q beads directory not found: %s", rigOrPrefix, targetPath)
}
if os.Getenv("BD_DEBUG_ROUTING") != "" {
fmt.Fprintf(os.Stderr, "[routing] Rig %q -> prefix %s, path %s\n", rigOrPrefix, route.Prefix, targetPath)
}
return targetPath, route.Prefix, nil
}
// ResolveToExternalRef attempts to convert a foreign issue ID to an external reference
// using routes.jsonl for prefix-based routing.
//
// If the ID's prefix matches a route, returns "external:<project>:<id>".
// Otherwise, returns empty string (no route found).
//
// Example: If routes.jsonl has {"prefix": "bd-", "path": "beads/mayor/rig"}
// then ResolveToExternalRef("bd-abc", beadsDir) returns "external:beads:bd-abc"
func ResolveToExternalRef(id, beadsDir string) string {
routes, err := LoadRoutes(beadsDir)
if err != nil || len(routes) == 0 {
return ""
}
prefix := ExtractPrefix(id)
if prefix == "" {
return ""
}
for _, route := range routes {
if route.Prefix == prefix {
project := ExtractProjectFromPath(route.Path)
if project != "" {
return fmt.Sprintf("external:%s:%s", project, id)
}
}
}
return ""
}
// ResolveBeadsDirForID determines which beads directory contains the given issue ID.
// It first checks the local beads directory, then consults routes.jsonl for prefix-based routing.
//
// Parameters:
// - ctx: context for database operations
// - id: the issue ID to look up
// - currentBeadsDir: the current/local .beads directory path
//
// Returns:
// - beadsDir: the resolved .beads directory path
// - routed: true if the ID was routed to a different directory
// - err: any error encountered
func ResolveBeadsDirForID(ctx context.Context, id, currentBeadsDir string) (string, bool, error) {
// Step 1: Check for routes.jsonl FIRST based on ID prefix
// This allows prefix-based routing without needing to check the local store
routes, loadErr := LoadRoutes(currentBeadsDir)
if loadErr == nil && len(routes) > 0 {
prefix := ExtractPrefix(id)
if prefix != "" {
for _, route := range routes {
if route.Prefix == prefix {
// Found a matching route - resolve the path
projectRoot := filepath.Dir(currentBeadsDir)
targetPath := filepath.Join(projectRoot, route.Path, ".beads")
// Follow redirect if present
targetPath = resolveRedirect(targetPath)
// Verify the target exists
if info, err := os.Stat(targetPath); err == nil && info.IsDir() {
// Debug logging
if os.Getenv("BD_DEBUG_ROUTING") != "" {
fmt.Fprintf(os.Stderr, "[routing] ID %s matched prefix %s -> %s\n", id, prefix, targetPath)
}
return targetPath, true, nil
}
}
}
}
}
// Step 2: No route matched or no routes file - use local store
return currentBeadsDir, false, nil
}
// resolveRedirect checks for a redirect file in the beads directory
// and resolves the redirect path if present.
func resolveRedirect(beadsDir string) string {
redirectFile := filepath.Join(beadsDir, "redirect")
data, err := os.ReadFile(redirectFile) //nolint:gosec // redirectFile is constructed from known beadsDir
if err != nil {
if os.Getenv("BD_DEBUG_ROUTING") != "" {
fmt.Fprintf(os.Stderr, "[routing] No redirect file at %s: %v\n", redirectFile, err)
}
return beadsDir // No redirect
}
redirectPath := strings.TrimSpace(string(data))
if os.Getenv("BD_DEBUG_ROUTING") != "" {
fmt.Fprintf(os.Stderr, "[routing] Read redirect: %q from %s\n", redirectPath, redirectFile)
}
if redirectPath == "" {
return beadsDir
}
// Handle relative paths
if !filepath.IsAbs(redirectPath) {
redirectPath = filepath.Join(beadsDir, redirectPath)
}
// Clean and resolve the path
redirectPath = filepath.Clean(redirectPath)
if os.Getenv("BD_DEBUG_ROUTING") != "" {
fmt.Fprintf(os.Stderr, "[routing] Resolved redirect path: %s\n", redirectPath)
}
// Verify the redirect target exists
if info, err := os.Stat(redirectPath); err == nil && info.IsDir() {
if os.Getenv("BD_DEBUG_ROUTING") != "" {
fmt.Fprintf(os.Stderr, "[routing] Followed redirect from %s -> %s\n", beadsDir, redirectPath)
}
return redirectPath
} else if os.Getenv("BD_DEBUG_ROUTING") != "" {
fmt.Fprintf(os.Stderr, "[routing] Redirect target check failed: %v\n", err)
}
return beadsDir
}
// RoutedStorage represents a storage connection that may have been routed
// to a different beads directory than the local one.
type RoutedStorage struct {
Storage storage.Storage
BeadsDir string
Routed bool // true if this is a routed (non-local) storage
}
// Close closes the storage connection
func (rs *RoutedStorage) Close() error {
if rs.Storage != nil {
return rs.Storage.Close()
}
return nil
}
// GetRoutedStorageForID returns a storage connection for the given issue ID.
// If the ID matches a route, it opens a connection to the routed database.
// Otherwise, it returns nil (caller should use their existing storage).
//
// The caller is responsible for closing the returned RoutedStorage.
func GetRoutedStorageForID(ctx context.Context, id, currentBeadsDir string) (*RoutedStorage, error) {
beadsDir, routed, err := ResolveBeadsDirForID(ctx, id, currentBeadsDir)
if err != nil {
return nil, err
}
if !routed {
return nil, nil // No routing needed, caller should use existing storage
}
// Open storage for the routed directory
dbPath := filepath.Join(beadsDir, "beads.db")
store, err := sqlite.New(ctx, dbPath)
if err != nil {
return nil, err
}
return &RoutedStorage{
Storage: store,
BeadsDir: beadsDir,
Routed: true,
}, nil
}

View File

@@ -88,3 +88,57 @@ func TestDetectUserRole_Fallback(t *testing.T) {
t.Errorf("DetectUserRole() = %v, want %v (fallback)", role, Contributor)
}
}
func TestExtractPrefix(t *testing.T) {
tests := []struct {
id string
want string
}{
{"gt-abc123", "gt-"},
{"bd-xyz", "bd-"},
{"hq-1234", "hq-"},
{"abc123", ""}, // No hyphen
{"", ""}, // Empty string
{"-abc", "-"}, // Starts with hyphen
}
for _, tt := range tests {
t.Run(tt.id, func(t *testing.T) {
got := ExtractPrefix(tt.id)
if got != tt.want {
t.Errorf("ExtractPrefix(%q) = %q, want %q", tt.id, got, tt.want)
}
})
}
}
func TestExtractProjectFromPath(t *testing.T) {
tests := []struct {
path string
want string
}{
{"beads/mayor/rig", "beads"},
{"gastown/crew/max", "gastown"},
{"simple", "simple"},
{"", ""},
{"/absolute/path", ""}, // Starts with /, first component is empty
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
got := ExtractProjectFromPath(tt.path)
if got != tt.want {
t.Errorf("ExtractProjectFromPath(%q) = %q, want %q", tt.path, got, tt.want)
}
})
}
}
func TestResolveToExternalRef(t *testing.T) {
// This test is limited since it requires a routes.jsonl file
// Just test that it returns empty string for nonexistent directory
got := ResolveToExternalRef("bd-abc", "/nonexistent/path")
if got != "" {
t.Errorf("ResolveToExternalRef() = %q, want empty string for nonexistent path", got)
}
}