From 5483ecf437da0c6b9ba6acfa0f4bb50229395366 Mon Sep 17 00:00:00 2001 From: diesel Date: Thu, 22 Jan 2026 17:32:38 -0800 Subject: [PATCH] feat(epic): add Working Model as required section for epics Epics now require a "Working Model" section in their description, in addition to "Success Criteria". This provides clear guidance on HOW the epic will be executed: - Owner role: Coordinator vs Implementer - Delegation target: Polecats, crew, external - Review process: Approval gates Closes gt-0lp Co-Authored-By: Claude Opus 4.5 --- claude-plugin/commands/epic.md | 22 ++++++++ internal/types/types.go | 77 ++++++++++++++-------------- internal/types/types_test.go | 2 +- internal/validation/template_test.go | 21 ++++++-- 4 files changed, 80 insertions(+), 42 deletions(-) diff --git a/claude-plugin/commands/epic.md b/claude-plugin/commands/epic.md index d9e825c7..a0a39dad 100644 --- a/claude-plugin/commands/epic.md +++ b/claude-plugin/commands/epic.md @@ -24,3 +24,25 @@ Manage epics (large features composed of multiple issues). 4. Auto-close when done: `bd epic close-eligible` Epics use parent-child dependencies to track subtasks. + +## Epic Template Sections + +Epics require two sections in their description: + +### Success Criteria +Define high-level criteria for epic completion. What does "done" look like? + +### Working Model +Define HOW the epic will be executed: + +- **Owner role**: Is the assignee a Coordinator (decomposes and delegates) or Implementer (does hands-on work)? +- **Delegation target**: Who does the work? Polecats? Other crew? External contributors? +- **Review process**: Who approves completed subtasks? What gates must pass? + +Example: +```markdown +## Working Model +- **Owner role**: Coordinator - decompose into subtasks and sling to polecats +- **Delegation target**: Polecat swarm (3-5 workers) +- **Review process**: Refinery MQ for each subtask, owner approval for epic closure +``` diff --git a/internal/types/types.go b/internal/types/types.go index d261f49b..2dee2e37 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -34,9 +34,9 @@ type Issue struct { EstimatedMinutes *int `json:"estimated_minutes,omitempty"` // ===== Timestamps ===== - CreatedAt time.Time `json:"created_at"` - CreatedBy string `json:"created_by,omitempty"` // Who created this issue (GH#748) - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + CreatedBy string `json:"created_by,omitempty"` // Who created this issue (GH#748) + UpdatedAt time.Time `json:"updated_at"` ClosedAt *time.Time `json:"closed_at,omitempty"` CloseReason string `json:"close_reason,omitempty"` // Reason provided when closing ClosedBySession string `json:"closed_by_session,omitempty"` // Claude Code session that closed this issue @@ -560,6 +560,7 @@ func (t IssueType) RequiredSections() []RequiredSection { case TypeEpic: return []RequiredSection{ {Heading: "## Success Criteria", Hint: "Define high-level success criteria"}, + {Heading: "## Working Model", Hint: "Owner role (coordinator/implementer), delegation target, review process"}, } default: // Chore and custom types have no required sections @@ -667,7 +668,7 @@ type IssueWithCounts struct { // Used for JSON serialization in bd show and RPC responses. type IssueDetails struct { Issue - Labels []string `json:"labels,omitempty"` + Labels []string `json:"labels,omitempty"` Dependencies []*IssueWithDependencyMetadata `json:"dependencies,omitempty"` Dependents []*IssueWithDependencyMetadata `json:"dependents,omitempty"` Comments []*Comment `json:"comments,omitempty"` @@ -690,10 +691,10 @@ const ( DepDiscoveredFrom DependencyType = "discovered-from" // Graph link types - DepRepliesTo DependencyType = "replies-to" // Conversation threading - DepRelatesTo DependencyType = "relates-to" // Loose knowledge graph edges - DepDuplicates DependencyType = "duplicates" // Deduplication link - DepSupersedes DependencyType = "supersedes" // Version chain link + DepRepliesTo DependencyType = "replies-to" // Conversation threading + DepRelatesTo DependencyType = "relates-to" // Loose knowledge graph edges + DepDuplicates DependencyType = "duplicates" // Deduplication link + DepSupersedes DependencyType = "supersedes" // Version chain link // Entity types (HOP foundation - Decision 004) DepAuthoredBy DependencyType = "authored-by" // Creator relationship @@ -820,14 +821,14 @@ type Comment struct { // Event represents an audit trail entry type Event struct { - ID int64 `json:"id"` - IssueID string `json:"issue_id"` - EventType EventType `json:"event_type"` - Actor string `json:"actor"` - OldValue *string `json:"old_value,omitempty"` - NewValue *string `json:"new_value,omitempty"` - Comment *string `json:"comment,omitempty"` - CreatedAt time.Time `json:"created_at"` + ID int64 `json:"id"` + IssueID string `json:"issue_id"` + EventType EventType `json:"event_type"` + Actor string `json:"actor"` + OldValue *string `json:"old_value,omitempty"` + NewValue *string `json:"new_value,omitempty"` + Comment *string `json:"comment,omitempty"` + CreatedAt time.Time `json:"created_at"` } // EventType categorizes audit trail events @@ -878,17 +879,17 @@ type MoleculeProgressStats struct { // Statistics provides aggregate metrics type Statistics struct { - TotalIssues int `json:"total_issues"` - OpenIssues int `json:"open_issues"` - InProgressIssues int `json:"in_progress_issues"` - ClosedIssues int `json:"closed_issues"` - BlockedIssues int `json:"blocked_issues"` - DeferredIssues int `json:"deferred_issues"` // Issues on ice - ReadyIssues int `json:"ready_issues"` - TombstoneIssues int `json:"tombstone_issues"` // Soft-deleted issues - PinnedIssues int `json:"pinned_issues"` // Persistent issues - EpicsEligibleForClosure int `json:"epics_eligible_for_closure"` - AverageLeadTime float64 `json:"average_lead_time_hours"` + TotalIssues int `json:"total_issues"` + OpenIssues int `json:"open_issues"` + InProgressIssues int `json:"in_progress_issues"` + ClosedIssues int `json:"closed_issues"` + BlockedIssues int `json:"blocked_issues"` + DeferredIssues int `json:"deferred_issues"` // Issues on ice + ReadyIssues int `json:"ready_issues"` + TombstoneIssues int `json:"tombstone_issues"` // Soft-deleted issues + PinnedIssues int `json:"pinned_issues"` // Persistent issues + EpicsEligibleForClosure int `json:"epics_eligible_for_closure"` + AverageLeadTime float64 `json:"average_lead_time_hours"` } // IssueFilter is used to filter issue queries @@ -897,18 +898,18 @@ type IssueFilter struct { Priority *int IssueType *IssueType Assignee *string - Labels []string // AND semantics: issue must have ALL these labels - LabelsAny []string // OR semantics: issue must have AT LEAST ONE of these labels + Labels []string // AND semantics: issue must have ALL these labels + LabelsAny []string // OR semantics: issue must have AT LEAST ONE of these labels TitleSearch string - IDs []string // Filter by specific issue IDs - IDPrefix string // Filter by ID prefix (e.g., "bd-" to match "bd-abc123") + IDs []string // Filter by specific issue IDs + IDPrefix string // Filter by ID prefix (e.g., "bd-" to match "bd-abc123") Limit int // Pattern matching TitleContains string DescriptionContains string NotesContains string - + // Date ranges CreatedAfter *time.Time CreatedBefore *time.Time @@ -916,12 +917,12 @@ type IssueFilter struct { UpdatedBefore *time.Time ClosedAfter *time.Time ClosedBefore *time.Time - + // Empty/null checks EmptyDescription bool NoAssignee bool NoLabels bool - + // Numeric ranges PriorityMin *int PriorityMax *int @@ -990,12 +991,12 @@ func (s SortPolicy) IsValid() bool { // WorkFilter is used to filter ready work queries type WorkFilter struct { Status Status - Type string // Filter by issue type (task, bug, feature, epic, merge-request, etc.) + Type string // Filter by issue type (task, bug, feature, epic, merge-request, etc.) Priority *int Assignee *string - Unassigned bool // Filter for issues with no assignee - Labels []string // AND semantics: issue must have ALL these labels - LabelsAny []string // OR semantics: issue must have AT LEAST ONE of these labels + Unassigned bool // Filter for issues with no assignee + Labels []string // AND semantics: issue must have ALL these labels + LabelsAny []string // OR semantics: issue must have AT LEAST ONE of these labels Limit int SortPolicy SortPolicy diff --git a/internal/types/types_test.go b/internal/types/types_test.go index 622e75d7..1fd54237 100644 --- a/internal/types/types_test.go +++ b/internal/types/types_test.go @@ -579,7 +579,7 @@ func TestIssueTypeRequiredSections(t *testing.T) { {TypeBug, 2, "## Steps to Reproduce"}, {TypeFeature, 1, "## Acceptance Criteria"}, {TypeTask, 1, "## Acceptance Criteria"}, - {TypeEpic, 1, "## Success Criteria"}, + {TypeEpic, 2, "## Success Criteria"}, {TypeChore, 0, ""}, // Gas Town types are now custom and have no required sections {IssueType("message"), 0, ""}, diff --git a/internal/validation/template_test.go b/internal/validation/template_test.go index f91e4627..c7763c53 100644 --- a/internal/validation/template_test.go +++ b/internal/validation/template_test.go @@ -97,19 +97,34 @@ Widget displays correctly`, // Epic type tests { - name: "epic with success criteria", + name: "epic with all sections", issueType: types.TypeEpic, description: `Big project ## Success Criteria - Project ships -- Users happy`, +- Users happy + +## Working Model +- Owner role: Coordinator +- Delegation target: Polecats +- Review process: Refinery MQ`, wantErr: false, }, { - name: "epic missing success criteria", + name: "epic missing all sections", issueType: types.TypeEpic, description: "Do everything", + wantErr: true, + wantMissing: 2, + }, + { + name: "epic missing working model", + issueType: types.TypeEpic, + description: `Big project + +## Success Criteria +- Project ships`, wantErr: true, wantMissing: 1, },