feat: Add --assignee flag to template instantiate, fix mail inbox --identity
- Add --assignee flag to `bd template instantiate` for work delegation - Assigns the root epic to specified agent/user - Child issues retain their template-defined assignees - Enables Gas Town orchestration: instantiate + assign in one step - Add missing --identity flag to `bd mail inbox` for consistency - All mail subcommands now support --identity override - Delete obsolete HANDOFF-template-redesign.md (work complete) - Add test for assignee override behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -130,6 +130,7 @@ func init() {
|
||||
// Inbox command flags
|
||||
mailInboxCmd.Flags().StringVar(&mailFrom, "from", "", "Filter by sender")
|
||||
mailInboxCmd.Flags().IntVar(&mailPriorityFlag, "priority", -1, "Filter by priority (0-4)")
|
||||
mailInboxCmd.Flags().StringVar(&mailIdentity, "identity", "", "Override recipient identity")
|
||||
|
||||
// Read command flags
|
||||
mailReadCmd.Flags().StringVar(&mailIdentity, "identity", "", "Override identity for access check")
|
||||
|
||||
@@ -383,6 +383,7 @@ Example:
|
||||
ctx := rootCtx
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
varFlags, _ := cmd.Flags().GetStringSlice("var")
|
||||
assignee, _ := cmd.Flags().GetString("assignee")
|
||||
|
||||
// Parse variables
|
||||
vars := make(map[string]string)
|
||||
@@ -446,7 +447,11 @@ Example:
|
||||
fmt.Printf("\nDry run: would create %d issues from template %s\n\n", len(subgraph.Issues), templateID)
|
||||
for _, issue := range subgraph.Issues {
|
||||
newTitle := substituteVariables(issue.Title, vars)
|
||||
fmt.Printf(" - %s (from %s)\n", newTitle, issue.ID)
|
||||
suffix := ""
|
||||
if issue.ID == subgraph.Root.ID && assignee != "" {
|
||||
suffix = fmt.Sprintf(" (assignee: %s)", assignee)
|
||||
}
|
||||
fmt.Printf(" - %s (from %s)%s\n", newTitle, issue.ID, suffix)
|
||||
}
|
||||
if len(vars) > 0 {
|
||||
fmt.Printf("\nVariables:\n")
|
||||
@@ -458,7 +463,7 @@ Example:
|
||||
}
|
||||
|
||||
// Clone the subgraph
|
||||
result, err := cloneSubgraph(ctx, store, subgraph, vars, actor)
|
||||
result, err := cloneSubgraph(ctx, store, subgraph, vars, assignee, actor)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error instantiating template: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -484,6 +489,7 @@ func init() {
|
||||
|
||||
templateInstantiateCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)")
|
||||
templateInstantiateCmd.Flags().Bool("dry-run", false, "Preview what would be created")
|
||||
templateInstantiateCmd.Flags().String("assignee", "", "Assign the root epic to this agent/user")
|
||||
|
||||
templateCmd.AddCommand(templateListCmd)
|
||||
templateCmd.AddCommand(templateShowCmd)
|
||||
@@ -734,7 +740,8 @@ func substituteVariables(text string, vars map[string]string) string {
|
||||
}
|
||||
|
||||
// cloneSubgraph creates new issues from the template with variable substitution
|
||||
func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSubgraph, vars map[string]string, actorName string) (*InstantiateResult, error) {
|
||||
// If assignee is non-empty, it will be set on the root epic
|
||||
func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSubgraph, vars map[string]string, assignee string, actorName string) (*InstantiateResult, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("no database connection")
|
||||
}
|
||||
@@ -746,6 +753,12 @@ func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSub
|
||||
err := s.RunInTransaction(ctx, func(tx storage.Transaction) error {
|
||||
// First pass: create all issues with new IDs
|
||||
for _, oldIssue := range subgraph.Issues {
|
||||
// Determine assignee: use override for root epic, otherwise keep template's
|
||||
issueAssignee := oldIssue.Assignee
|
||||
if oldIssue.ID == subgraph.Root.ID && assignee != "" {
|
||||
issueAssignee = assignee
|
||||
}
|
||||
|
||||
newIssue := &types.Issue{
|
||||
// Don't set ID - let the system generate it
|
||||
Title: substituteVariables(oldIssue.Title, vars),
|
||||
@@ -756,7 +769,7 @@ func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSub
|
||||
Status: types.StatusOpen, // Always start fresh
|
||||
Priority: oldIssue.Priority,
|
||||
IssueType: oldIssue.IssueType,
|
||||
Assignee: oldIssue.Assignee,
|
||||
Assignee: issueAssignee,
|
||||
EstimatedMinutes: oldIssue.EstimatedMinutes,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
|
||||
@@ -450,7 +450,7 @@ func TestCloneSubgraph(t *testing.T) {
|
||||
}
|
||||
|
||||
vars := map[string]string{"version": "2.0.0"}
|
||||
result, err := cloneSubgraph(ctx, s, subgraph, vars, "test-user")
|
||||
result, err := cloneSubgraph(ctx, s, subgraph, vars, "", "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("cloneSubgraph failed: %v", err)
|
||||
}
|
||||
@@ -490,7 +490,7 @@ func TestCloneSubgraph(t *testing.T) {
|
||||
}
|
||||
|
||||
vars := map[string]string{"service": "api-gateway"}
|
||||
result, err := cloneSubgraph(ctx, s, subgraph, vars, "test-user")
|
||||
result, err := cloneSubgraph(ctx, s, subgraph, vars, "", "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("cloneSubgraph failed: %v", err)
|
||||
}
|
||||
@@ -549,7 +549,7 @@ func TestCloneSubgraph(t *testing.T) {
|
||||
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
||||
}
|
||||
|
||||
result, err := cloneSubgraph(ctx, s, subgraph, nil, "test-user")
|
||||
result, err := cloneSubgraph(ctx, s, subgraph, nil, "", "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("cloneSubgraph failed: %v", err)
|
||||
}
|
||||
@@ -562,6 +562,52 @@ func TestCloneSubgraph(t *testing.T) {
|
||||
t.Errorf("Status = %s, want %s", newEpic.Status, types.StatusOpen)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("assignee override applies to root epic only", func(t *testing.T) {
|
||||
epic := h.createIssue("Root Epic", "", types.TypeEpic, 1)
|
||||
child := h.createIssue("Child Task", "", types.TypeTask, 2)
|
||||
h.addParentChild(child.ID, epic.ID)
|
||||
|
||||
// Set assignees on template
|
||||
err := s.UpdateIssue(ctx, epic.ID, map[string]interface{}{"assignee": "template-owner"}, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set epic assignee: %v", err)
|
||||
}
|
||||
err = s.UpdateIssue(ctx, child.ID, map[string]interface{}{"assignee": "child-owner"}, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set child assignee: %v", err)
|
||||
}
|
||||
|
||||
subgraph, err := loadTemplateSubgraph(ctx, s, epic.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
||||
}
|
||||
|
||||
// Clone with assignee override
|
||||
result, err := cloneSubgraph(ctx, s, subgraph, nil, "new-assignee", "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("cloneSubgraph failed: %v", err)
|
||||
}
|
||||
|
||||
// Root epic should have override assignee
|
||||
newEpic, err := s.GetIssue(ctx, result.NewEpicID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get cloned epic: %v", err)
|
||||
}
|
||||
if newEpic.Assignee != "new-assignee" {
|
||||
t.Errorf("Epic assignee = %q, want %q", newEpic.Assignee, "new-assignee")
|
||||
}
|
||||
|
||||
// Child should keep template assignee
|
||||
newChildID := result.IDMapping[child.ID]
|
||||
newChild, err := s.GetIssue(ctx, newChildID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get cloned child: %v", err)
|
||||
}
|
||||
if newChild.Assignee != "child-owner" {
|
||||
t.Errorf("Child assignee = %q, want %q", newChild.Assignee, "child-owner")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtractAllVariables tests extracting variables from entire subgraph
|
||||
|
||||
Reference in New Issue
Block a user