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 <noreply@anthropic.com>
This commit is contained in:
@@ -24,3 +24,25 @@ Manage epics (large features composed of multiple issues).
|
|||||||
4. Auto-close when done: `bd epic close-eligible`
|
4. Auto-close when done: `bd epic close-eligible`
|
||||||
|
|
||||||
Epics use parent-child dependencies to track subtasks.
|
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
|
||||||
|
```
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ type Issue struct {
|
|||||||
EstimatedMinutes *int `json:"estimated_minutes,omitempty"`
|
EstimatedMinutes *int `json:"estimated_minutes,omitempty"`
|
||||||
|
|
||||||
// ===== Timestamps =====
|
// ===== Timestamps =====
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
CreatedBy string `json:"created_by,omitempty"` // Who created this issue (GH#748)
|
CreatedBy string `json:"created_by,omitempty"` // Who created this issue (GH#748)
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
||||||
CloseReason string `json:"close_reason,omitempty"` // Reason provided when closing
|
CloseReason string `json:"close_reason,omitempty"` // Reason provided when closing
|
||||||
ClosedBySession string `json:"closed_by_session,omitempty"` // Claude Code session that closed this issue
|
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:
|
case TypeEpic:
|
||||||
return []RequiredSection{
|
return []RequiredSection{
|
||||||
{Heading: "## Success Criteria", Hint: "Define high-level success criteria"},
|
{Heading: "## Success Criteria", Hint: "Define high-level success criteria"},
|
||||||
|
{Heading: "## Working Model", Hint: "Owner role (coordinator/implementer), delegation target, review process"},
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Chore and custom types have no required sections
|
// 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.
|
// Used for JSON serialization in bd show and RPC responses.
|
||||||
type IssueDetails struct {
|
type IssueDetails struct {
|
||||||
Issue
|
Issue
|
||||||
Labels []string `json:"labels,omitempty"`
|
Labels []string `json:"labels,omitempty"`
|
||||||
Dependencies []*IssueWithDependencyMetadata `json:"dependencies,omitempty"`
|
Dependencies []*IssueWithDependencyMetadata `json:"dependencies,omitempty"`
|
||||||
Dependents []*IssueWithDependencyMetadata `json:"dependents,omitempty"`
|
Dependents []*IssueWithDependencyMetadata `json:"dependents,omitempty"`
|
||||||
Comments []*Comment `json:"comments,omitempty"`
|
Comments []*Comment `json:"comments,omitempty"`
|
||||||
@@ -690,10 +691,10 @@ const (
|
|||||||
DepDiscoveredFrom DependencyType = "discovered-from"
|
DepDiscoveredFrom DependencyType = "discovered-from"
|
||||||
|
|
||||||
// Graph link types
|
// Graph link types
|
||||||
DepRepliesTo DependencyType = "replies-to" // Conversation threading
|
DepRepliesTo DependencyType = "replies-to" // Conversation threading
|
||||||
DepRelatesTo DependencyType = "relates-to" // Loose knowledge graph edges
|
DepRelatesTo DependencyType = "relates-to" // Loose knowledge graph edges
|
||||||
DepDuplicates DependencyType = "duplicates" // Deduplication link
|
DepDuplicates DependencyType = "duplicates" // Deduplication link
|
||||||
DepSupersedes DependencyType = "supersedes" // Version chain link
|
DepSupersedes DependencyType = "supersedes" // Version chain link
|
||||||
|
|
||||||
// Entity types (HOP foundation - Decision 004)
|
// Entity types (HOP foundation - Decision 004)
|
||||||
DepAuthoredBy DependencyType = "authored-by" // Creator relationship
|
DepAuthoredBy DependencyType = "authored-by" // Creator relationship
|
||||||
@@ -820,14 +821,14 @@ type Comment struct {
|
|||||||
|
|
||||||
// Event represents an audit trail entry
|
// Event represents an audit trail entry
|
||||||
type Event struct {
|
type Event struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
IssueID string `json:"issue_id"`
|
IssueID string `json:"issue_id"`
|
||||||
EventType EventType `json:"event_type"`
|
EventType EventType `json:"event_type"`
|
||||||
Actor string `json:"actor"`
|
Actor string `json:"actor"`
|
||||||
OldValue *string `json:"old_value,omitempty"`
|
OldValue *string `json:"old_value,omitempty"`
|
||||||
NewValue *string `json:"new_value,omitempty"`
|
NewValue *string `json:"new_value,omitempty"`
|
||||||
Comment *string `json:"comment,omitempty"`
|
Comment *string `json:"comment,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventType categorizes audit trail events
|
// EventType categorizes audit trail events
|
||||||
@@ -878,17 +879,17 @@ type MoleculeProgressStats struct {
|
|||||||
|
|
||||||
// Statistics provides aggregate metrics
|
// Statistics provides aggregate metrics
|
||||||
type Statistics struct {
|
type Statistics struct {
|
||||||
TotalIssues int `json:"total_issues"`
|
TotalIssues int `json:"total_issues"`
|
||||||
OpenIssues int `json:"open_issues"`
|
OpenIssues int `json:"open_issues"`
|
||||||
InProgressIssues int `json:"in_progress_issues"`
|
InProgressIssues int `json:"in_progress_issues"`
|
||||||
ClosedIssues int `json:"closed_issues"`
|
ClosedIssues int `json:"closed_issues"`
|
||||||
BlockedIssues int `json:"blocked_issues"`
|
BlockedIssues int `json:"blocked_issues"`
|
||||||
DeferredIssues int `json:"deferred_issues"` // Issues on ice
|
DeferredIssues int `json:"deferred_issues"` // Issues on ice
|
||||||
ReadyIssues int `json:"ready_issues"`
|
ReadyIssues int `json:"ready_issues"`
|
||||||
TombstoneIssues int `json:"tombstone_issues"` // Soft-deleted issues
|
TombstoneIssues int `json:"tombstone_issues"` // Soft-deleted issues
|
||||||
PinnedIssues int `json:"pinned_issues"` // Persistent issues
|
PinnedIssues int `json:"pinned_issues"` // Persistent issues
|
||||||
EpicsEligibleForClosure int `json:"epics_eligible_for_closure"`
|
EpicsEligibleForClosure int `json:"epics_eligible_for_closure"`
|
||||||
AverageLeadTime float64 `json:"average_lead_time_hours"`
|
AverageLeadTime float64 `json:"average_lead_time_hours"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssueFilter is used to filter issue queries
|
// IssueFilter is used to filter issue queries
|
||||||
@@ -897,18 +898,18 @@ type IssueFilter struct {
|
|||||||
Priority *int
|
Priority *int
|
||||||
IssueType *IssueType
|
IssueType *IssueType
|
||||||
Assignee *string
|
Assignee *string
|
||||||
Labels []string // AND semantics: issue must have ALL 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
|
LabelsAny []string // OR semantics: issue must have AT LEAST ONE of these labels
|
||||||
TitleSearch string
|
TitleSearch string
|
||||||
IDs []string // Filter by specific issue IDs
|
IDs []string // Filter by specific issue IDs
|
||||||
IDPrefix string // Filter by ID prefix (e.g., "bd-" to match "bd-abc123")
|
IDPrefix string // Filter by ID prefix (e.g., "bd-" to match "bd-abc123")
|
||||||
Limit int
|
Limit int
|
||||||
|
|
||||||
// Pattern matching
|
// Pattern matching
|
||||||
TitleContains string
|
TitleContains string
|
||||||
DescriptionContains string
|
DescriptionContains string
|
||||||
NotesContains string
|
NotesContains string
|
||||||
|
|
||||||
// Date ranges
|
// Date ranges
|
||||||
CreatedAfter *time.Time
|
CreatedAfter *time.Time
|
||||||
CreatedBefore *time.Time
|
CreatedBefore *time.Time
|
||||||
@@ -916,12 +917,12 @@ type IssueFilter struct {
|
|||||||
UpdatedBefore *time.Time
|
UpdatedBefore *time.Time
|
||||||
ClosedAfter *time.Time
|
ClosedAfter *time.Time
|
||||||
ClosedBefore *time.Time
|
ClosedBefore *time.Time
|
||||||
|
|
||||||
// Empty/null checks
|
// Empty/null checks
|
||||||
EmptyDescription bool
|
EmptyDescription bool
|
||||||
NoAssignee bool
|
NoAssignee bool
|
||||||
NoLabels bool
|
NoLabels bool
|
||||||
|
|
||||||
// Numeric ranges
|
// Numeric ranges
|
||||||
PriorityMin *int
|
PriorityMin *int
|
||||||
PriorityMax *int
|
PriorityMax *int
|
||||||
@@ -990,12 +991,12 @@ func (s SortPolicy) IsValid() bool {
|
|||||||
// WorkFilter is used to filter ready work queries
|
// WorkFilter is used to filter ready work queries
|
||||||
type WorkFilter struct {
|
type WorkFilter struct {
|
||||||
Status Status
|
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
|
Priority *int
|
||||||
Assignee *string
|
Assignee *string
|
||||||
Unassigned bool // Filter for issues with no assignee
|
Unassigned bool // Filter for issues with no assignee
|
||||||
Labels []string // AND semantics: issue must have ALL 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
|
LabelsAny []string // OR semantics: issue must have AT LEAST ONE of these labels
|
||||||
Limit int
|
Limit int
|
||||||
SortPolicy SortPolicy
|
SortPolicy SortPolicy
|
||||||
|
|
||||||
|
|||||||
@@ -579,7 +579,7 @@ func TestIssueTypeRequiredSections(t *testing.T) {
|
|||||||
{TypeBug, 2, "## Steps to Reproduce"},
|
{TypeBug, 2, "## Steps to Reproduce"},
|
||||||
{TypeFeature, 1, "## Acceptance Criteria"},
|
{TypeFeature, 1, "## Acceptance Criteria"},
|
||||||
{TypeTask, 1, "## Acceptance Criteria"},
|
{TypeTask, 1, "## Acceptance Criteria"},
|
||||||
{TypeEpic, 1, "## Success Criteria"},
|
{TypeEpic, 2, "## Success Criteria"},
|
||||||
{TypeChore, 0, ""},
|
{TypeChore, 0, ""},
|
||||||
// Gas Town types are now custom and have no required sections
|
// Gas Town types are now custom and have no required sections
|
||||||
{IssueType("message"), 0, ""},
|
{IssueType("message"), 0, ""},
|
||||||
|
|||||||
@@ -97,19 +97,34 @@ Widget displays correctly`,
|
|||||||
|
|
||||||
// Epic type tests
|
// Epic type tests
|
||||||
{
|
{
|
||||||
name: "epic with success criteria",
|
name: "epic with all sections",
|
||||||
issueType: types.TypeEpic,
|
issueType: types.TypeEpic,
|
||||||
description: `Big project
|
description: `Big project
|
||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
- Project ships
|
- Project ships
|
||||||
- Users happy`,
|
- Users happy
|
||||||
|
|
||||||
|
## Working Model
|
||||||
|
- Owner role: Coordinator
|
||||||
|
- Delegation target: Polecats
|
||||||
|
- Review process: Refinery MQ`,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "epic missing success criteria",
|
name: "epic missing all sections",
|
||||||
issueType: types.TypeEpic,
|
issueType: types.TypeEpic,
|
||||||
description: "Do everything",
|
description: "Do everything",
|
||||||
|
wantErr: true,
|
||||||
|
wantMissing: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "epic missing working model",
|
||||||
|
issueType: types.TypeEpic,
|
||||||
|
description: `Big project
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- Project ships`,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
wantMissing: 1,
|
wantMissing: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user