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 <noreply@anthropic.com>
This commit is contained in:
120
cmd/bd/mol.go
120
cmd/bd/mol.go
@@ -292,14 +292,134 @@ Example:
|
||||
},
|
||||
}
|
||||
|
||||
var molRunCmd = &cobra.Command{
|
||||
Use: "run <molecule-id>",
|
||||
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=<value>\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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user