diff --git a/HANDOFF-template-redesign.md b/HANDOFF-template-redesign.md deleted file mode 100644 index 2273944b..00000000 --- a/HANDOFF-template-redesign.md +++ /dev/null @@ -1,116 +0,0 @@ -# Handoff: Template System Redesign (bd-r6a) - -## Status - -- **bd-r6a.1**: DONE - Reverted YAML workflow code, deleted workflow.go and types -- **bd-r6a.2**: IN PROGRESS - Implementing subgraph cloning with variable substitution - -## What Was Removed - -- `cmd/bd/workflow.go` - entire file -- `cmd/bd/templates/workflows/` - YAML templates directory -- `internal/types/workflow.go` - WorkflowTemplate types - -Build passes. Tests pass. - -## The New Design - -### Core Principle - -**Templates are just Beads.** An epic with the `template` label and `{{variable}}` placeholders in titles/descriptions. - -Beads provides **primitives**: -- Clone a subgraph (epic + children + dependencies) -- Substitute `{{variables}}` -- Return ID mapping (old → new) - -Orchestrators (Gas Town) provide **composition**: -- Multiple instantiations -- Cross-template dependencies -- Dynamic task generation - -### Commands - -```bash -bd template list # List templates (label=template) -bd template instantiate --var key=value # Clone + substitute -bd template instantiate --dry-run # Preview -``` - -### Gas Town Use Case: Witness - -The Witness manages polecat lifecycles with a dynamic DAG: - -``` -Witness Round (1 instance) -├── Check context -├── Initialize tracking -├── [polecat tasks wired in by Gas Town] -├── Submit to merge queue -└── Finalize - -Polecat Lifecycle (N instances, one per polecat) -├── Verify startup -├── Monitor progress -├── Verify shutdown -└── Decommission -``` - -Gas Town instantiates templates in a loop and wires dependencies between them. - -## Implementation Started - -Was creating `cmd/bd/template.go` with: - -- `templateCmd` - parent command -- `templateListCmd` - list templates (issues with template label) -- `templateInstantiateCmd` - clone subgraph with substitution -- `TemplateSubgraph` struct - holds issues + dependencies -- `loadTemplateSubgraph()` - recursive load of epic + descendants -- `cloneSubgraph()` - create new issues with ID remapping -- `extractVariables()` - find `{{name}}` patterns -- `substituteVariables()` - replace patterns with values - -File was not written yet (got interrupted). - -## Key Functions Needed - -```go -// Load template and all descendants -func loadTemplateSubgraph(templateID string) (*TemplateSubgraph, error) - -// Clone with substitution, return new epic ID and ID mapping -func cloneSubgraph(subgraph *TemplateSubgraph, vars map[string]string) (string, map[string]string, error) - -// Extract {{variable}} patterns -func extractVariables(text string) []string - -// Replace {{variable}} with values -func substituteVariables(text string, vars map[string]string) string -``` - -## Remaining Tasks - -1. **bd-r6a.2**: Implement subgraph cloning (in progress) -2. **bd-r6a.3**: Create version-bump as native Beads template -3. **bd-r6a.4**: Add `bd template list` command -4. **bd-r6a.5**: Update documentation - -## HOP Context - -Templates feed into HOP vision: -- Work is fractal (templates are reusable work patterns) -- Beads IS the ledger (templates are ledger entries) -- Gas Town is execution engine (composes templates into swarms) - -See `~/gt/hop/CONTEXT.md` for full HOP context. - -## To Resume - -```bash -cd /Users/stevey/src/dave/beads -bd show bd-r6a # See epic and tasks -bd ready # bd-r6a.2 should be ready -bd update bd-r6a.2 --status=in_progress -# Create cmd/bd/template.go with the design above -``` diff --git a/cmd/bd/mail.go b/cmd/bd/mail.go index d2873aa8..0c3b3a47 100644 --- a/cmd/bd/mail.go +++ b/cmd/bd/mail.go @@ -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") diff --git a/cmd/bd/template.go b/cmd/bd/template.go index fa1d4c1e..bb831ee6 100644 --- a/cmd/bd/template.go +++ b/cmd/bd/template.go @@ -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(), diff --git a/cmd/bd/template_test.go b/cmd/bd/template_test.go index 7731d059..c8180482 100644 --- a/cmd/bd/template_test.go +++ b/cmd/bd/template_test.go @@ -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