From 8e17dcff6da1baddc499c35d8c568afb7cb1a54b Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 21 Dec 2025 00:07:19 -0800 Subject: [PATCH] feat(mol): add bd mol run command for durable execution bd mol run = bond + assign + pin: - Bonds the molecule (creates issues from template) - Assigns root to the caller - Sets root status to in_progress - Pins root issue for session recovery After a crash or session reset, the pinned root ensures the agent can resume from where it left off by checking 'bd ready'. This is the Gas Town integration point that makes molecules immortal. Closes: bd-icnf Co-Authored-By: Claude Opus 4.5 --- .beads/issues.jsonl | 4 +- cmd/bd/mol.go | 120 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index be505c05..f6ebd362 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -64,7 +64,7 @@ {"id":"bd-7tuu","title":"Commit and push release","description":"git add -A \u0026\u0026 git commit \u0026\u0026 git push to trigger CI","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T22:56:02.053382-08:00","updated_at":"2025-12-20T01:23:52.484043-08:00","closed_at":"2025-12-20T01:23:52.484043-08:00","close_reason":"Superseded by 0.30.7 release - already committed and pushed","dependencies":[{"issue_id":"bd-7tuu","depends_on_id":"bd-6s61","type":"parent-child","created_at":"2025-12-19T22:56:15.021087-08:00","created_by":"daemon"},{"issue_id":"bd-7tuu","depends_on_id":"bd-hw3w","type":"blocks","created_at":"2025-12-19T22:56:23.291591-08:00","created_by":"daemon"}]} {"id":"bd-7yg","title":"Git merge driver uses invalid placeholders (%L, %R instead of %A, %B)","description":"## Problem\n\nThe beads git merge driver is configured with invalid Git placeholders:\n\n```\ngit config merge.beads.driver \"bd merge %A %O %L %R\"\n```\n\nGit doesn't recognize `%L` or `%R` as valid merge driver placeholders. The valid placeholders are:\n- `%O` = base (common ancestor)\n- `%A` = current version (ours)\n- `%B` = other version (theirs)\n\n## Impact\n\n- Affects ALL users when they have `.beads/beads.jsonl` merge conflicts\n- Automatic JSONL merge fails with error: \"error reading left file: failed to open file: open 7: no such file or directory\"\n- Users must manually resolve conflicts instead of getting automatic merge\n\n## Root Cause\n\nThe `bd init` command (or wherever the merge driver is configured) is using non-standard placeholders. When Git encounters `%L` and `%R`, it either passes them literally or interprets them incorrectly.\n\n## Fix\n\nUpdate the merge driver configuration to:\n```\ngit config merge.beads.driver \"bd merge %A %O %A %B\"\n```\n\nWhere:\n- 1st `%A` = output file (current file, will be overwritten)\n- `%O` = base (common ancestor)\n- 2nd `%A` = left/current version\n- `%B` = right/other version\n\n## Action Items\n\n1. Fix `bd init` (or equivalent setup command) to use correct placeholders\n2. Add migration/warning for existing users with misconfigured merge driver\n3. Update documentation with correct merge driver setup\n4. Consider adding validation when `bd init` is run","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-21T19:51:55.747608-05:00","updated_at":"2025-12-17T23:13:40.532368-08:00","closed_at":"2025-12-17T17:24:52.678668-08:00"} {"id":"bd-89f89fc0","title":"Remove unreachable RPC methods","description":"Several RPC server and client methods are unreachable and should be removed:\n\nServer methods (internal/rpc/server.go):\n- `Server.GetLastImportTime` (line 2116)\n- `Server.SetLastImportTime` (line 2123)\n- `Server.findJSONLPath` (line 2255)\n\nClient methods (internal/rpc/client.go):\n- `Client.Import` (line 311) - RPC import not used (daemon uses autoimport)\n\nEvidence:\n```bash\ngo run golang.org/x/tools/cmd/deadcode@latest -test ./...\n```\n\nImpact: Removes ~80 LOC of unused RPC code","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-28T16:20:02.432202-07:00","updated_at":"2025-12-17T22:58:34.564401-08:00","closed_at":"2025-12-17T22:58:34.564401-08:00","close_reason":"Closed"} -{"id":"bd-8b0x","title":"Remove molecule.go (simple instantiation)","description":"molecule.go uses is_template field for simple single-issue cloning. This is too simple for what molecules should be - full DAG orchestration. The use case is covered by bd mol bond with a single-issue molecule. Delete molecule.go and its commands.","status":"in_progress","priority":2,"issue_type":"task","created_at":"2025-12-20T23:52:15.041776-08:00","updated_at":"2025-12-21T00:03:59.388461-08:00","dependencies":[{"issue_id":"bd-8b0x","depends_on_id":"bd-ffjt","type":"blocks","created_at":"2025-12-20T23:52:25.807967-08:00","created_by":"daemon"}]} +{"id":"bd-8b0x","title":"Remove molecule.go (simple instantiation)","description":"molecule.go uses is_template field for simple single-issue cloning. This is too simple for what molecules should be - full DAG orchestration. The use case is covered by bd mol bond with a single-issue molecule. Delete molecule.go and its commands.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-20T23:52:15.041776-08:00","updated_at":"2025-12-21T00:04:32.335849-08:00","closed_at":"2025-12-21T00:04:32.335849-08:00","close_reason":"Removed molecule.go - use bd mol bond instead","dependencies":[{"issue_id":"bd-8b0x","depends_on_id":"bd-ffjt","type":"blocks","created_at":"2025-12-20T23:52:25.807967-08:00","created_by":"daemon"}]} {"id":"bd-8e0q","title":"Merge: beads-ocs","description":"branch: polecat/valkyrie\ntarget: main\nsource_issue: beads-ocs\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-19T23:24:45.281478-08:00","updated_at":"2025-12-20T23:17:26.995706-08:00","closed_at":"2025-12-20T23:17:26.995706-08:00","close_reason":"Branches nuked, MRs obsolete"} {"id":"bd-8fgn","title":"test hash length","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T13:49:32.113843-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-8g8","title":"Fix G304 potential file inclusion in cmd/bd/tips.go:259","description":"Linting issue: G304: Potential file inclusion via variable (gosec) at cmd/bd/tips.go:259:18. Error: if data, err := os.ReadFile(settingsPath); err == nil {","status":"closed","issue_type":"bug","created_at":"2025-12-07T15:34:57.189730843-07:00","updated_at":"2025-12-17T23:13:40.534569-08:00","closed_at":"2025-12-17T16:46:11.029837-08:00"} @@ -169,7 +169,7 @@ {"id":"bd-hy9p","title":"Add --body-file flag to bd create for reading descriptions from files","description":"## Problem\n\nCreating issues with long/complex descriptions via CLI requires shell escaping gymnastics:\n\n```bash\n# Current workaround - awkward heredoc quoting\nbd create --title=\"...\" --description=\"$(cat \u003c\u003c'EOF'\n...markdown...\nEOF\n)\"\n\n# Often fails with quote escaping errors in eval context\n# Agents resort to writing temp files then reading them\n```\n\n## Proposed Solution\n\nAdd `--body-file` and `--description-file` flags to read description from a file, matching `gh` CLI pattern.\n\n```bash\n# Natural pattern that aligns with training data\ncat \u003e /tmp/desc.md \u003c\u003c 'EOF'\n...markdown content...\nEOF\n\nbd create --title=\"...\" --body-file=/tmp/desc.md\n```\n\n## Implementation\n\n### 1. Add new flags to `bd create`\n\n```go\ncreateCmd.Flags().String(\"body-file\", \"\", \"Read description from file (use - for stdin)\")\ncreateCmd.Flags().String(\"description-file\", \"\", \"Alias for --body-file\")\n```\n\n### 2. Flag precedence\n\n- If `--body-file` or `--description-file` is provided, read from file\n- If value is `-`, read from stdin\n- Otherwise fall back to `--body` or `--description` flag\n- If neither provided, description is empty (current behavior)\n\n### 3. Error handling\n\n- File doesn't exist → clear error message\n- File not readable → clear error message\n- stdin specified but not available → clear error message\n\n## Benefits\n\n✅ **Matches training data**: `gh issue create --body-file file.txt` is a common pattern\n✅ **No shell escaping issues**: File content is read directly\n✅ **Works with any content**: Markdown, special characters, quotes, etc.\n✅ **Agent-friendly**: Agents already write complex content to temp files\n✅ **User-friendly**: Easier for humans too when pasting long descriptions\n\n## Related Commands\n\nConsider adding similar support to:\n- `bd update --body-file` (for updating descriptions)\n- `bd comment --body-file` (if/when we add comments)\n\n## Examples\n\n```bash\n# From file\nbd create --title=\"Add new feature\" --body-file=feature.md\n\n# From stdin\necho \"Quick description\" | bd create --title=\"Bug fix\" --body-file=-\n\n# With other flags\nbd create \\\n --title=\"Security issue\" \\\n --type=bug \\\n --priority=0 \\\n --body-file=security-report.md \\\n --label=security\n```\n\n## Testing\n\n- Test with normal files\n- Test with stdin (`-`)\n- Test with non-existent files (error handling)\n- Test with binary files (should handle gracefully)\n- Test with empty files (valid - empty description)\n- Test that `--description-file` and `--body-file` are equivalent aliases","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-11-22T00:02:08.762684-08:00","updated_at":"2025-12-17T23:13:40.536024-08:00","closed_at":"2025-12-17T17:28:52.505239-08:00"} {"id":"bd-hzvz","title":"Update info.go versionChanges","description":"Add entry to versionChanges in cmd/bd/info.go with agent-actionable changes for 0.30.7","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T22:56:48.649359-08:00","updated_at":"2025-12-19T22:57:31.604229-08:00","closed_at":"2025-12-19T22:57:31.604229-08:00","dependencies":[{"issue_id":"bd-hzvz","depends_on_id":"bd-8pyn","type":"parent-child","created_at":"2025-12-19T22:56:48.652068-08:00","created_by":"stevey"},{"issue_id":"bd-hzvz","depends_on_id":"bd-2ep8","type":"blocks","created_at":"2025-12-19T22:56:48.652376-08:00","created_by":"stevey"}]} {"id":"bd-i0rx","title":"Merge: bd-ao0s","description":"branch: polecat/rictus\ntarget: main\nsource_issue: bd-ao0s\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-20T01:13:42.716658-08:00","updated_at":"2025-12-20T23:17:26.993744-08:00","closed_at":"2025-12-20T23:17:26.993744-08:00","close_reason":"Branches nuked, MRs obsolete"} -{"id":"bd-icnf","title":"Add bd mol run command (bond + assign + pin)","description":"bd mol run = bond + assign root to caller + pin to startup mail. This is the Gas Town integration point. When agent restarts, check startup mail, find pinned molecule root, query bd ready for next step. Makes molecules immortal.","status":"open","priority":1,"issue_type":"feature","created_at":"2025-12-20T23:52:17.462882-08:00","updated_at":"2025-12-20T23:52:17.462882-08:00","dependencies":[{"issue_id":"bd-icnf","depends_on_id":"bd-ffjt","type":"blocks","created_at":"2025-12-20T23:52:25.871742-08:00","created_by":"daemon"}]} +{"id":"bd-icnf","title":"Add bd mol run command (bond + assign + pin)","description":"bd mol run = bond + assign root to caller + pin to startup mail. This is the Gas Town integration point. When agent restarts, check startup mail, find pinned molecule root, query bd ready for next step. Makes molecules immortal.","status":"in_progress","priority":1,"issue_type":"feature","created_at":"2025-12-20T23:52:17.462882-08:00","updated_at":"2025-12-21T00:04:59.466069-08:00","dependencies":[{"issue_id":"bd-icnf","depends_on_id":"bd-ffjt","type":"blocks","created_at":"2025-12-20T23:52:25.871742-08:00","created_by":"daemon"}]} {"id":"bd-in7","title":"Test message","description":"Hello world","status":"closed","priority":2,"issue_type":"message","created_at":"2025-12-17T23:16:13.184946-08:00","updated_at":"2025-12-18T17:42:26.000073-08:00","closed_at":"2025-12-17T23:37:38.563369-08:00"} {"id":"bd-indn","title":"bd template commands fail with daemon mode","description":"The `bd template show` and `bd template instantiate` commands fail with 'Error loading template: no database connection' when daemon is running.\n\n**Reproduction:**\n```bash\nbd daemon --start\nbd template show bd-qqc # Error: no database connection\nbd template show bd-qqc --no-daemon # Works\n```\n\n**Expected:** Template commands should work with daemon like other commands.\n\n**Workaround:** Use `--no-daemon` flag.\n\n**Location:** Likely in cmd/bd/template.go - daemon RPC path not implemented for template operations.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-18T22:57:35.16596-08:00","updated_at":"2025-12-18T22:57:35.16596-08:00"} {"id":"bd-io8c","title":"Improve test coverage for internal/syncbranch (33.0% → 70%)","description":"The syncbranch package has only 33.0% test coverage. This package handles git sync operations and is critical for data integrity.\n\nCurrent coverage: 33.0%\nTarget coverage: 70%","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T20:43:02.079145-08:00","updated_at":"2025-12-13T21:01:14.972533-08:00"} diff --git a/cmd/bd/mol.go b/cmd/bd/mol.go index e2945963..a8c485b4 100644 --- a/cmd/bd/mol.go +++ b/cmd/bd/mol.go @@ -292,14 +292,134 @@ Example: }, } +var molRunCmd = &cobra.Command{ + Use: "run ", + Short: "Bond molecule and start execution (bond + assign + pin)", + Long: `Run a molecule by bonding it and setting up for durable execution. + +This command: + 1. Bonds the molecule (creates issues from template) + 2. Assigns the root issue to the caller + 3. Sets root status to in_progress + 4. Pins the root issue for session recovery + +After a crash or session reset, the pinned root issue ensures the agent +can resume from where it left off by checking 'bd ready'. + +Example: + bd mol run mol-version-bump --var version=1.2.0 + bd mol run bd-qqc --var version=0.32.0 --var date=2025-01-01`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + CheckReadonly("mol run") + + ctx := rootCtx + + // mol run requires direct store access + if store == nil { + if daemonClient != nil { + fmt.Fprintf(os.Stderr, "Error: mol run requires direct database access\n") + fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol run %s ...\n", args[0]) + } else { + fmt.Fprintf(os.Stderr, "Error: no database connection\n") + } + os.Exit(1) + } + + varFlags, _ := cmd.Flags().GetStringSlice("var") + + // Parse variables + vars := make(map[string]string) + for _, v := range varFlags { + parts := strings.SplitN(v, "=", 2) + if len(parts) != 2 { + fmt.Fprintf(os.Stderr, "Error: invalid variable format '%s', expected 'key=value'\n", v) + os.Exit(1) + } + vars[parts[0]] = parts[1] + } + + // Resolve molecule ID + moleculeID, err := utils.ResolvePartialID(ctx, store, args[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "Error resolving molecule ID %s: %v\n", args[0], err) + os.Exit(1) + } + + // Load the molecule subgraph + subgraph, err := loadTemplateSubgraph(ctx, store, moleculeID) + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading molecule: %v\n", err) + os.Exit(1) + } + + // Check for missing variables + requiredVars := extractAllVariables(subgraph) + var missingVars []string + for _, v := range requiredVars { + if _, ok := vars[v]; !ok { + missingVars = append(missingVars, v) + } + } + if len(missingVars) > 0 { + fmt.Fprintf(os.Stderr, "Error: missing required variables: %s\n", strings.Join(missingVars, ", ")) + fmt.Fprintf(os.Stderr, "Provide them with: --var %s=\n", missingVars[0]) + os.Exit(1) + } + + // Bond the molecule with actor as assignee + result, err := bondMolecule(ctx, store, subgraph, vars, actor, actor) + if err != nil { + fmt.Fprintf(os.Stderr, "Error bonding molecule: %v\n", err) + os.Exit(1) + } + + // Update root issue: set status=in_progress and pinned=true + rootID := result.NewEpicID + updates := map[string]interface{}{ + "status": string(types.StatusInProgress), + "pinned": true, + } + if err := store.UpdateIssue(ctx, rootID, updates, actor); err != nil { + fmt.Fprintf(os.Stderr, "Error updating root issue: %v\n", err) + os.Exit(1) + } + + // Schedule auto-flush + markDirtyAndScheduleFlush() + + if jsonOutput { + outputJSON(map[string]interface{}{ + "root_id": rootID, + "created": result.Created, + "id_mapping": result.IDMapping, + "pinned": true, + "status": "in_progress", + "assignee": actor, + }) + return + } + + fmt.Printf("%s Molecule running: created %d issues\n", ui.RenderPass("✓"), result.Created) + fmt.Printf(" Root issue: %s (pinned, in_progress)\n", rootID) + fmt.Printf(" Assignee: %s\n", actor) + fmt.Println("\nNext steps:") + fmt.Printf(" bd ready # Find unblocked work in this molecule\n") + fmt.Printf(" bd show %s # View molecule status\n", rootID[:8]) + }, +} + func init() { molBondCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)") molBondCmd.Flags().Bool("dry-run", false, "Preview what would be created") molBondCmd.Flags().String("assignee", "", "Assign the root issue to this agent/user") + molRunCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)") + molCmd.AddCommand(molCatalogCmd) molCmd.AddCommand(molShowCmd) molCmd.AddCommand(molBondCmd) + molCmd.AddCommand(molRunCmd) rootCmd.AddCommand(molCmd) }