feat: Add bd swarm create command (bd-fa1q)
Implements `bd swarm create <epic-id>` to create a swarm molecule that orchestrates parallel work on an epic. Features: - Creates molecule with mol_type=swarm for discovery - Links to epic via relates-to dependency - Validates epic structure before creation - Auto-wraps single issues in an epic when needed - Optional --coordinator flag to specify coordinator agent - Supports JSON output for machine consumption 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
172
cmd/bd/swarm.go
172
cmd/bd/swarm.go
@@ -827,10 +827,182 @@ func renderSwarmStatus(status *SwarmStatus) {
|
||||
fmt.Printf(" (%.0f%%)\n\n", status.Progress)
|
||||
}
|
||||
|
||||
var swarmCreateCmd = &cobra.Command{
|
||||
Use: "create [epic-id]",
|
||||
Short: "Create a swarm molecule from an epic",
|
||||
Long: `Create a swarm molecule to orchestrate parallel work on an epic.
|
||||
|
||||
The swarm molecule:
|
||||
- Links to the epic it orchestrates
|
||||
- Has mol_type=swarm for discovery
|
||||
- Specifies a coordinator (optional)
|
||||
- Can be picked up by any coordinator agent
|
||||
|
||||
If given a single issue (not an epic), it will be auto-wrapped:
|
||||
- Creates an epic with that issue as its only child
|
||||
- Then creates the swarm molecule for that epic
|
||||
|
||||
Examples:
|
||||
bd swarm create gt-epic-123 # Create swarm for epic
|
||||
bd swarm create gt-epic-123 --coordinator=witness/ # With specific coordinator
|
||||
bd swarm create gt-task-456 # Auto-wrap single issue`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckReadonly("swarm create")
|
||||
ctx := rootCtx
|
||||
coordinator, _ := cmd.Flags().GetString("coordinator")
|
||||
|
||||
// Swarm commands require direct store access
|
||||
if store == nil {
|
||||
if daemonClient != nil {
|
||||
var err error
|
||||
store, err = sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
FatalErrorRespectJSON("failed to open database: %v", err)
|
||||
}
|
||||
defer func() { _ = store.Close() }()
|
||||
} else {
|
||||
FatalErrorRespectJSON("no database connection")
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the input ID
|
||||
inputID, err := utils.ResolvePartialID(ctx, store, args[0])
|
||||
if err != nil {
|
||||
FatalErrorRespectJSON("issue '%s' not found: %v", args[0], err)
|
||||
}
|
||||
|
||||
// Get the issue
|
||||
issue, err := store.GetIssue(ctx, inputID)
|
||||
if err != nil {
|
||||
FatalErrorRespectJSON("failed to get issue: %v", err)
|
||||
}
|
||||
if issue == nil {
|
||||
FatalErrorRespectJSON("issue '%s' not found", inputID)
|
||||
}
|
||||
|
||||
var epicID string
|
||||
var epicTitle string
|
||||
|
||||
// Check if it's an epic or single issue that needs wrapping
|
||||
if issue.IssueType == types.TypeEpic || issue.IssueType == types.TypeMolecule {
|
||||
epicID = issue.ID
|
||||
epicTitle = issue.Title
|
||||
} else {
|
||||
// Auto-wrap: create an epic with this issue as child
|
||||
if !jsonOutput {
|
||||
fmt.Printf("Auto-wrapping single issue as epic...\n")
|
||||
}
|
||||
|
||||
wrapperEpic := &types.Issue{
|
||||
Title: fmt.Sprintf("Swarm Epic: %s", issue.Title),
|
||||
Description: fmt.Sprintf("Auto-generated epic to wrap single issue %s for swarm execution.", issue.ID),
|
||||
Status: types.StatusOpen,
|
||||
Priority: issue.Priority,
|
||||
IssueType: types.TypeEpic,
|
||||
CreatedBy: actor,
|
||||
}
|
||||
|
||||
if err := store.CreateIssue(ctx, wrapperEpic, actor); err != nil {
|
||||
FatalErrorRespectJSON("failed to create wrapper epic: %v", err)
|
||||
}
|
||||
|
||||
// Add parent-child dependency: issue depends on epic (epic is parent)
|
||||
dep := &types.Dependency{
|
||||
IssueID: issue.ID,
|
||||
DependsOnID: wrapperEpic.ID,
|
||||
Type: types.DepParentChild,
|
||||
CreatedBy: actor,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep, actor); err != nil {
|
||||
FatalErrorRespectJSON("failed to link issue to epic: %v", err)
|
||||
}
|
||||
|
||||
epicID = wrapperEpic.ID
|
||||
epicTitle = wrapperEpic.Title
|
||||
|
||||
if !jsonOutput {
|
||||
fmt.Printf("Created wrapper epic: %s\n", epicID)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the epic structure
|
||||
epic, err := store.GetIssue(ctx, epicID)
|
||||
if err != nil {
|
||||
FatalErrorRespectJSON("failed to get epic: %v", err)
|
||||
}
|
||||
|
||||
analysis, err := analyzeEpicForSwarm(ctx, store, epic)
|
||||
if err != nil {
|
||||
FatalErrorRespectJSON("failed to analyze epic: %v", err)
|
||||
}
|
||||
|
||||
if !analysis.Swarmable {
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
"error": "epic is not swarmable",
|
||||
"analysis": analysis,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("\n%s Epic is not swarmable. Fix errors first:\n", ui.RenderFail("✗"))
|
||||
for _, e := range analysis.Errors {
|
||||
fmt.Printf(" • %s\n", e)
|
||||
}
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create the swarm molecule
|
||||
swarmMol := &types.Issue{
|
||||
Title: fmt.Sprintf("Swarm: %s", epicTitle),
|
||||
Description: fmt.Sprintf("Swarm molecule orchestrating epic %s.\n\nEpic: %s\nCoordinator: %s", epicID, epicID, coordinator),
|
||||
Status: types.StatusOpen,
|
||||
Priority: epic.Priority,
|
||||
IssueType: types.TypeMolecule,
|
||||
MolType: types.MolTypeSwarm,
|
||||
Assignee: coordinator,
|
||||
CreatedBy: actor,
|
||||
}
|
||||
|
||||
if err := store.CreateIssue(ctx, swarmMol, actor); err != nil {
|
||||
FatalErrorRespectJSON("failed to create swarm molecule: %v", err)
|
||||
}
|
||||
|
||||
// Link swarm molecule to epic with relates-to dependency
|
||||
dep := &types.Dependency{
|
||||
IssueID: swarmMol.ID,
|
||||
DependsOnID: epicID,
|
||||
Type: types.DepRelatesTo,
|
||||
CreatedBy: actor,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep, actor); err != nil {
|
||||
FatalErrorRespectJSON("failed to link swarm to epic: %v", err)
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
"swarm_id": swarmMol.ID,
|
||||
"epic_id": epicID,
|
||||
"coordinator": coordinator,
|
||||
"analysis": analysis,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("\n%s Created swarm molecule: %s\n", ui.RenderPass("✓"), ui.RenderID(swarmMol.ID))
|
||||
fmt.Printf(" Epic: %s (%s)\n", epicID, epicTitle)
|
||||
fmt.Printf(" Coordinator: %s\n", coordinator)
|
||||
fmt.Printf(" Total issues: %d\n", analysis.TotalIssues)
|
||||
fmt.Printf(" Max parallelism: %d\n", analysis.MaxParallelism)
|
||||
fmt.Printf(" Waves: %d\n", len(analysis.ReadyFronts))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
swarmValidateCmd.Flags().Bool("verbose", false, "Include detailed issue graph in output")
|
||||
swarmCreateCmd.Flags().String("coordinator", "", "Coordinator address (e.g., gastown/witness)")
|
||||
|
||||
swarmCmd.AddCommand(swarmValidateCmd)
|
||||
swarmCmd.AddCommand(swarmStatusCmd)
|
||||
swarmCmd.AddCommand(swarmCreateCmd)
|
||||
rootCmd.AddCommand(swarmCmd)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user