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:
@@ -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
|
||||
|
||||
|
||||
@@ -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, ""},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user