From 21f307de89ca0eb487e6d126f7eaa9d3482e6e3f Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 27 Dec 2025 00:44:39 -0800 Subject: [PATCH] feat: add --rig flag for cross-rig issue creation (bd-4nzq) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds --rig flag to bd create that allows creating issues in a different rig without having to cd to that directory. Example: bd create --rig beads --title='Bug report' 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .beads/issues.jsonl | 2 + cmd/bd/create.go | 111 +++++++++++++++++++++++++++++++++++++ internal/routing/routes.go | 57 +++++++++++++++++++ 3 files changed, 170 insertions(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index e73c2af5..b61a5bc6 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -78,6 +78,7 @@ {"id":"bd-4hn","title":"wish: list \u0026 ready show issues as hierarchy tree","description":"`bd ready` and `bd list` just show a flat list, and it's up to the reader to parse which ones are dependent or sub-issues of others. It would be much easier to understand if they were shown in a tree format","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-08T06:38:24.016316945-07:00","updated_at":"2025-12-08T06:39:04.065882225-07:00"} {"id":"bd-4lm3","title":"Correction: Pinned field already in v0.31.0","description":"Quick correction - the Pinned field is already in the current bd v0.31.0:\n\n```go\n// In beads internal/types/types.go\nPinned bool `json:\"pinned,omitempty\"`\n```\n\nSo you just need to:\n1. Add `Pinned bool `json:\"pinned,omitempty\"`` to BeadsMessage in types.go\n2. Sort pinned messages first in listBeads() after fetching\n\nNo migration needed - the field is already there.\n\n-- Mayor","status":"closed","priority":2,"issue_type":"message","assignee":"gastown/crew/max","created_at":"2025-12-20T17:52:27.321458-08:00","updated_at":"2025-12-21T17:52:18.617995-08:00","closed_at":"2025-12-21T17:52:18.617995-08:00","close_reason":"Stale correction message","labels":["from:beads-crew-dave","thread:thread-4dd70157dbc1"]} {"id":"bd-4nqq","title":"Remove dead test code in info_test.go","description":"Code health review found cmd/bd/info_test.go has two tests permanently skipped:\n\n- TestInfoCommand\n- TestInfoCommandNoDaemon\n\nBoth skip with: 'Manual test - bd info command is working, see manual testing'\n\nThese are essentially dead code. Either automate them or remove them entirely.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-16T18:17:27.554019-08:00","updated_at":"2025-12-22T21:01:24.524963-08:00","closed_at":"2025-12-22T21:01:24.524963-08:00","close_reason":"Removed 2 permanently skipped tests (TestInfoCommand, TestInfoWithNoDaemon). Kept 3 useful tests for versionChanges."} +{"id":"bd-4nzq","title":"bd create --rig flag for cross-rig issue filing","description":"## Problem\n\nTo create an issue in a different rig, you must cd to that rig's directory. This is clunky when coordinating across rigs.\n\n## Proposed Solution\n\nAdd a --rig flag to bd create that leverages existing routes.jsonl:\n\n```bash\n# From anywhere in town\nbd create --rig beads --title=\"bug report\" --type=bug\n\n# Equivalent to:\ncd ~/gt/beads/mayor/rig \u0026\u0026 bd create --title=\"...\"\n```\n\n## How it works\n\n1. Parse --rig flag (rig name, not prefix)\n2. Look up rig in routes.jsonl to find beads location\n3. Determine prefix from rig config\n4. Create issue in that location with auto-generated ID\n\n## Why elegant\n\n- Leverages existing routing infrastructure\n- Rig names are human-memorable (not cryptic prefixes)\n- Works from anywhere in town\n- Prefix auto-determined from rig config\n- Reads naturally: \"create in beads\"\n\n## Alternative\n\nAlso consider --prefix for power users who know prefixes:\n```bash\nbd create --prefix bd \"Title here\"\n```","status":"closed","priority":2,"issue_type":"feature","assignee":"beads/crew/dave","created_at":"2025-12-27T00:39:02.233664-08:00","created_by":"mayor","updated_at":"2025-12-27T00:44:04.079867-08:00","closed_at":"2025-12-27T00:44:04.079867-08:00","close_reason":"Implemented --rig flag for cross-rig issue creation"} {"id":"bd-4opy","title":"Refactor long SQLite test files","description":"The SQLite test files have grown unwieldy. Review and refactor.\n\n## Goals\n- Break up large test files into focused modules\n- Improve test organization by feature area\n- Reduce test duplication\n- Make tests easier to maintain and extend\n\n## Areas to Review\n- main_test.go (likely the largest)\n- Any test files over 500 lines\n- Shared test fixtures and helpers\n- Test coverage gaps\n\n## Approach\n- Group tests by feature (CRUD, sync, queries, transactions)\n- Extract common fixtures to test helpers\n- Consider table-driven tests where appropriate\n- Ensure each test file has clear focus\n\n## Reference\nSee docs/dev-notes/ for any existing test audit notes","status":"closed","priority":2,"issue_type":"task","assignee":"beads/angharad","created_at":"2025-12-21T23:41:47.025285-08:00","updated_at":"2025-12-23T01:33:25.733299-08:00","closed_at":"2025-12-23T01:33:25.733299-08:00","close_reason":"Merged to main"} {"id":"bd-4or","title":"Add tests for daemon functionality","description":"Critical daemon functions have 0% test coverage including daemon lifecycle, health checks, and RPC server functionality. These are essential for system reliability and need comprehensive test coverage.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-18T07:00:26.916050465-07:00","updated_at":"2025-12-19T09:54:57.017114822-07:00","closed_at":"2025-12-18T12:29:06.134014366-07:00","dependencies":[{"issue_id":"bd-4or","depends_on_id":"bd-6ss","type":"discovered-from","created_at":"2025-12-18T07:00:26.919347253-07:00","created_by":"matt"}]} {"id":"bd-4p3k","title":"Release v0.34.0","description":"Minor version release for beads v0.34.0. This bead serves as my persistent work assignment; the actual release steps are tracked in an attached wisp.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-22T03:03:20.73092-08:00","updated_at":"2025-12-22T03:05:03.168622-08:00","deleted_at":"2025-12-22T03:05:03.168622-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"} @@ -263,6 +264,7 @@ {"id":"bd-dyy","title":"Review PR #513: fix hooks install docs","description":"Review and merge PR #513 from aspiers. This PR fixes incorrect docs for how to install git hooks - updates README to use bd hooks install instead of removed install.sh. Simple 1-line change. URL: https://github.com/anthropics/beads/pull/513","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-13T08:15:14.838772+11:00","updated_at":"2025-12-25T01:21:01.952723-08:00","deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"} {"id":"bd-e1085716","title":"bd validate - Comprehensive health check","description":"Run all validation checks in one command.\n\nChecks:\n- Duplicates\n- Orphaned dependencies\n- Test pollution\n- Git conflicts\n\nSupports --fix-all for auto-repair.\n\nDepends on bd-cbed9619.1, bd-0dcea000, bd-31aab707, bd-9826b69a.\n\nFiles: cmd/bd/validate.go (new)","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-29T23:05:13.980679-07:00","updated_at":"2025-12-25T01:21:01.952723-08:00","close_reason":"Closed","deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"} {"id":"bd-e7ou","title":"Fix --as flag: uses title instead of ID in mol bond","description":"In bondProtoProto, the --as flag is documented as 'Custom ID for compound proto' but the implementation uses it as the title, not the issue ID.\n\n**Current behavior (mol.go:637-638):**\n```go\nif customID != '' {\n compoundTitle = customID // Used as title, not ID\n}\n```\n\n**Options:**\n1. Change flag description to say 'Custom title' (documentation fix)\n2. Actually use it as a custom ID prefix or full ID (feature change)\n3. Add separate --title flag and make --as actually set ID\n\nRecommend option 1 for simplest fix - change 'Custom ID' to 'Custom title' in the flag description.","status":"closed","priority":3,"issue_type":"bug","created_at":"2025-12-21T10:22:59.069368-08:00","updated_at":"2025-12-21T21:18:48.514513-08:00","closed_at":"2025-12-21T21:18:48.514513-08:00","close_reason":"Fixed - renamed customID to customTitle and updated dry-run output"} +{"id":"bd-efo6","title":"Test cross-rig issue creation","description":"Testing --rig flag from town root","status":"tombstone","priority":3,"issue_type":"task","created_at":"2025-12-27T00:43:21.006012-08:00","created_by":"stevey","updated_at":"2025-12-27T00:43:27.389223-08:00","deleted_at":"2025-12-27T00:43:27.389223-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"} {"id":"bd-eijl","title":"bd ship command for publishing capabilities","description":"Add `bd ship \u003ccapability\u003e` command that:\n\n1. Finds issue with `export:\u003ccapability\u003e` label\n2. Validates issue is closed (or --force to override)\n3. Adds `provides:\u003ccapability\u003e` label\n4. Protects `provides:*` namespace (only bd ship can add these labels)\n\nExample:\n```bash\nbd ship mol-run-assignee\n# Output: Shipped mol-run-assignee (bd-xyz)\n```\n\nPart of cross-project dependency system.\nSee: gastown/docs/cross-project-deps.md","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-21T22:37:19.123024-08:00","updated_at":"2025-12-21T23:11:47.498859-08:00","closed_at":"2025-12-21T23:11:47.498859-08:00","close_reason":"Implemented: bd ship command with export:/provides: labels, namespace protection in label add"} {"id":"bd-elqd","title":"Systematic bd sync stability investigation","description":"## Context\n\nbd sync has chronic instability issues that have persisted since inception:\n- issues.jsonl is always dirty after push\n- bd sync often creates messes requiring manual cleanup\n- Problems escalating despite accumulated bug fixes\n- Workarounds are getting increasingly draconian\n\n## Goal\n\nSystematically observe and diagnose bd sync failures rather than applying band-aid fixes.\n\n## Approach\n\n1. Start fresh session with latest binary (all fixes applied)\n2. Run bd sync and carefully observe what happens\n3. Document exact sequence of events when things go wrong\n4. File specific issues for each discrete problem identified\n5. Track the root causes, not just symptoms\n\n## Test Environment\n\n- Fresh clone or clean state\n- Latest bd binary with all bug fixes\n- Monitor both local and remote JSONL state\n- Check for timing issues, race conditions, merge conflicts\n\n## Success Criteria\n\n- Identify root causes of sync instability\n- Create actionable issues for each problem\n- Eventually achieve stable bd sync (no manual intervention needed)","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T22:57:25.35289-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"} {"id":"bd-etyv","title":"Smart --var detection for mol distill","description":"Implemented bidirectional syntax support for mol distill --var flag.\n\n**Problem:**\n- spawn uses: --var variable=value (assignment style)\n- distill used: --var value=variable (substitution style)\n- Agents would naturally guess spawn-style for both\n\n**Solution:**\nSmart detection that accepts BOTH syntaxes by checking which side appears in the epic text:\n- --var branch=feature-auth → finds 'feature-auth' in text → works\n- --var feature-auth=branch → finds 'feature-auth' in text → also works\n\n**Changes:**\n- Added parseDistillVar() with smart detection\n- Added collectSubgraphText() helper\n- Restructured runMolDistill to load subgraph before parsing vars\n- Updated help text to document both syntaxes\n- Added comprehensive tests in mol_test.go\n\n**Edge cases handled:**\n- Both sides found: prefers spawn-style (more common guess)\n- Neither found: helpful error message\n- Empty sides: validation error\n- Values containing '=' (e.g., KEY=VALUE): works via SplitN\n\nEmbodies the Beads philosophy: watch what agents do, make their guess correct.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-21T11:08:50.83923-08:00","updated_at":"2025-12-21T11:08:56.432536-08:00","closed_at":"2025-12-21T11:08:56.432536-08:00","close_reason":"Implemented"} diff --git a/cmd/bd/create.go b/cmd/bd/create.go index eb3a45e0..6e93f036 100644 --- a/cmd/bd/create.go +++ b/cmd/bd/create.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "strings" "github.com/spf13/cobra" @@ -12,6 +13,7 @@ import ( "github.com/steveyegge/beads/internal/hooks" "github.com/steveyegge/beads/internal/routing" "github.com/steveyegge/beads/internal/rpc" + "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/ui" "github.com/steveyegge/beads/internal/validation" @@ -107,8 +109,15 @@ var createCmd = &cobra.Command{ waitsForGate, _ := cmd.Flags().GetString("waits-for-gate") forceCreate, _ := cmd.Flags().GetBool("force") repoOverride, _ := cmd.Flags().GetString("repo") + rigOverride, _ := cmd.Flags().GetString("rig") wisp, _ := cmd.Flags().GetBool("ephemeral") + // Handle --rig flag: create issue in a different rig + if rigOverride != "" { + createInRig(cmd, rigOverride, title, description, issueType, priority, design, acceptance, assignee, labels, externalRef, wisp) + return + } + // Get estimate if provided var estimatedMinutes *int if cmd.Flags().Changed("estimate") { @@ -447,8 +456,110 @@ func init() { createCmd.Flags().String("waits-for-gate", "all-children", "Gate type: all-children (wait for all) or any-children (wait for first)") createCmd.Flags().Bool("force", false, "Force creation even if prefix doesn't match database prefix") createCmd.Flags().String("repo", "", "Target repository for issue (overrides auto-routing)") + createCmd.Flags().String("rig", "", "Create issue in a different rig (e.g., --rig beads)") createCmd.Flags().IntP("estimate", "e", 0, "Time estimate in minutes (e.g., 60 for 1 hour)") createCmd.Flags().Bool("ephemeral", false, "Create as ephemeral (ephemeral, not exported to JSONL)") // Note: --json flag is defined as a persistent flag in main.go, not here rootCmd.AddCommand(createCmd) } + +// createInRig creates an issue in a different rig using --rig flag. +// This bypasses the normal daemon/direct flow and directly creates in the target rig. +func createInRig(cmd *cobra.Command, rigName, title, description, issueType string, priority int, design, acceptance, assignee string, labels []string, externalRef string, wisp bool) { + ctx := rootCtx + + // Find the town-level beads directory (where routes.jsonl lives) + townBeadsDir, err := findTownBeadsDir() + if err != nil { + FatalError("cannot use --rig: %v", err) + } + + // Resolve the target rig's beads directory + targetBeadsDir, _, err := routing.ResolveBeadsDirForRig(rigName, townBeadsDir) + if err != nil { + FatalError("%v", err) + } + + // Open storage for the target rig + targetDBPath := filepath.Join(targetBeadsDir, "beads.db") + targetStore, err := sqlite.New(ctx, targetDBPath) + if err != nil { + FatalError("failed to open rig %q database: %v", rigName, err) + } + defer targetStore.Close() + + var externalRefPtr *string + if externalRef != "" { + externalRefPtr = &externalRef + } + + // Create issue without ID - CreateIssue will generate one with the correct prefix + issue := &types.Issue{ + Title: title, + Description: description, + Design: design, + AcceptanceCriteria: acceptance, + Status: types.StatusOpen, + Priority: priority, + IssueType: types.IssueType(issueType), + Assignee: assignee, + ExternalRef: externalRefPtr, + Ephemeral: wisp, + CreatedBy: getActorWithGit(), + } + + if err := targetStore.CreateIssue(ctx, issue, actor); err != nil { + FatalError("failed to create issue in rig %q: %v", rigName, err) + } + + // Add labels if specified + for _, label := range labels { + if err := targetStore.AddLabel(ctx, issue.ID, label, actor); err != nil { + WarnError("failed to add label %s: %v", label, err) + } + } + + // Get silent flag + silent, _ := cmd.Flags().GetBool("silent") + + if jsonOutput { + outputJSON(issue) + } else if silent { + fmt.Println(issue.ID) + } else { + fmt.Printf("%s Created issue in rig %q: %s\n", ui.RenderPass("✓"), rigName, issue.ID) + fmt.Printf(" Title: %s\n", issue.Title) + fmt.Printf(" Priority: P%d\n", issue.Priority) + fmt.Printf(" Status: %s\n", issue.Status) + } +} + +// findTownBeadsDir finds the town-level .beads directory (where routes.jsonl lives). +// It walks up from the current directory looking for a .beads directory with routes.jsonl. +func findTownBeadsDir() (string, error) { + // Start from current directory and walk up + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for { + beadsDir := filepath.Join(dir, ".beads") + routesFile := filepath.Join(beadsDir, routing.RoutesFileName) + + // Check if this .beads directory has routes.jsonl + if _, err := os.Stat(routesFile); err == nil { + return beadsDir, nil + } + + // Move up one directory + parent := filepath.Dir(dir) + if parent == dir { + // Reached filesystem root + break + } + dir = parent + } + + return "", fmt.Errorf("no routes.jsonl found in any parent .beads directory") +} diff --git a/internal/routing/routes.go b/internal/routing/routes.go index d22967c8..8eb56ce0 100644 --- a/internal/routing/routes.go +++ b/internal/routing/routes.go @@ -79,6 +79,63 @@ func ExtractProjectFromPath(path string) string { 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 +} + +// ResolveBeadsDirForRig returns the beads directory for a given rig name. +// This is used by --rig flag to create issues in a different rig. +// +// Parameters: +// - rigName: the rig name (e.g., "beads", "gastown") +// - 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(rigName, currentBeadsDir string) (beadsDir string, prefix string, err error) { + route, found := LookupRigByName(rigName, currentBeadsDir) + if !found { + return "", "", fmt.Errorf("rig %q not found in routes.jsonl", rigName) + } + + // 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", rigName, targetPath) + } + + if os.Getenv("BD_DEBUG_ROUTING") != "" { + fmt.Fprintf(os.Stderr, "[routing] Rig %q -> prefix %s, path %s\n", rigName, 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. //