diff --git a/cmd/pm/tasks/sprintPlanning.go b/cmd/pm/tasks/sprintPlanning.go index 572fa41..9d5a5fd 100644 --- a/cmd/pm/tasks/sprintPlanning.go +++ b/cmd/pm/tasks/sprintPlanning.go @@ -48,6 +48,7 @@ func (t *SprintPlanningTask) Questions(interfaceType InterfaceType) Questions { return BaseQuestions(interfaceType).With( huh.NewGroup( huh.NewSelect[int](). + Key("sprint-planning-selected-issues"). Title("How many issues did you select for the sprint?"). Options( huh.NewOption("2-3 issues", 1), @@ -58,6 +59,10 @@ func (t *SprintPlanningTask) Questions(interfaceType InterfaceType) Questions { ) } +func (t *SprintPlanningTask) QuestionnaireKeys(_ InterfaceType) []string { + return []string{"task_completed", "task_difficulty", "sprint-planning-selected-issues"} +} + func (t *SprintPlanningTask) Setup(ctx context.Context) error { if err := ClearIssues(t.app); err != nil { return err @@ -116,6 +121,50 @@ func (t *SprintPlanningTask) Setup(ctx context.Context) error { func (t *SprintPlanningTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - return expect.Complete() + issues, err := FetchIssues(ctx, t.app, t.setupIssue) + if err != nil { + return expect.ValidationFeedback + } + if len(issues) == 0 { + expect.Fail("No backlog issues found to plan a sprint with.") + return expect.ValidationFeedback + } + + // Sort by priority ascending (0 is highest priority). + sorted := make([]*models.Issue, len(issues)) + copy(sorted, issues) + for i := 0; i < len(sorted); i++ { + for j := i + 1; j < len(sorted); j++ { + if sorted[j].Priority < sorted[i].Priority { + sorted[i], sorted[j] = sorted[j], sorted[i] + } + } + } + + topN := 5 + if len(sorted) < topN { + topN = len(sorted) + } + top := sorted[:topN] + + var plannedCount int + var readyToSprintCount int + for _, issue := range top { + if issue.Status == models.StatusReadyToSprint || + issue.Status == models.StatusInProgress || + issue.Status == models.StatusClosed { + plannedCount++ + } + if issue.Status == models.StatusReadyToSprint { + readyToSprintCount++ + } + } + + expect.Assert(plannedCount >= 3, + "Expected at least 3 of the 5 highest-priority issues to be moved into 'ready_to_sprint', 'in_progress', or 'closed' for the sprint.") + expect.Assert(readyToSprintCount >= 1, + "Expected at least one of the highest-priority issues to be marked as 'ready_to_sprint' to indicate it is planned for the sprint.") + + return expect.Complete() } diff --git a/internal/commands/issues/completion.go b/internal/commands/issues/completion.go index 0a532e1..19c8a35 100644 --- a/internal/commands/issues/completion.go +++ b/internal/commands/issues/completion.go @@ -11,7 +11,7 @@ import ( // Variables for completion options and functions. var ( typeOptions = []string{"bug", "feature", "task", "chore"} - statusOptions = []string{"open", "closed", "in_progress"} + statusOptions = []string{"open", "closed", "in_progress", "ready_to_sprint"} priorityRange = []string{"0", "1", "2", "3", "4"} ) diff --git a/internal/commands/issues/create.go b/internal/commands/issues/create.go index 2deafed..238e295 100644 --- a/internal/commands/issues/create.go +++ b/internal/commands/issues/create.go @@ -112,7 +112,7 @@ func runCreateInteractive() error { func init() { //CreateCmd.Flags().BoolVarP(&createFlags.interactive, "interactive", "i", false, "Create issue interactively") CreateCmd.Flags().StringVarP(&createFlags.description, "desc", "d", "", "Issue description") - CreateCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress)") + CreateCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress, ready_to_sprint)") CreateCmd.Flags().StringVarP(&createFlags.issueType, "type", "t", "task", "Issue type(bug, feature, task)") CreateCmd.Flags().IntVarP(&createFlags.priority, "priority", "p", 0, "Issue priority(0-4)") CreateCmd.Flags().StringVarP(&createFlags.assignee, "assignee", "a", "", "Issue assignee") diff --git a/internal/commands/issues/list.go b/internal/commands/issues/list.go index f61d869..e9eae24 100644 --- a/internal/commands/issues/list.go +++ b/internal/commands/issues/list.go @@ -76,7 +76,7 @@ func runGetIssuesCmd(cmd *cobra.Command, args []string) error { func init() { ListCmd.Flags().StringVar(&listFlags.title, "title", "", "Filter issues by title") ListCmd.Flags().StringVarP(&listFlags.description, "desc", "d", "", "Filter issues by description") - ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress)") + ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress, ready_to_sprint)") ListCmd.Flags().StringVarP(&listFlags.issueType, "type", "t", "", "Filter issues by type (bug, feature, task)") ListCmd.Flags().IntVarP(&listFlags.priority, "priority", "p", 0, "Filter issues by priority (0-4)") ListCmd.Flags().StringVarP(&listFlags.assignee, "assignee", "a", "", "Filter issues by assignee") diff --git a/internal/commands/issues/update.go b/internal/commands/issues/update.go index f1dac02..29a6b29 100644 --- a/internal/commands/issues/update.go +++ b/internal/commands/issues/update.go @@ -58,7 +58,7 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { func init() { UpdateCmd.Flags().StringVar(&updateFlags.title, "title", "", "New issue title") UpdateCmd.Flags().StringVarP(&updateFlags.description, "desc", "d", "", "New issue description") - UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress)") + UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress, ready_to_sprint)") UpdateCmd.Flags().StringVarP(&updateFlags.issueType, "type", "t", "", "New issue type(bug, feature, task)") UpdateCmd.Flags().IntVarP(&updateFlags.priority, "priority", "p", 0, "New issue priority(0-4)") UpdateCmd.Flags().StringVarP(&updateFlags.assignee, "assignee", "a", "", "New issue assignee") diff --git a/internal/models/beads.go b/internal/models/beads.go index a25d480..ff7730a 100644 --- a/internal/models/beads.go +++ b/internal/models/beads.go @@ -33,11 +33,12 @@ type ( // Status constants const ( - StatusOpen = beads.StatusOpen - StatusInProgress = beads.StatusInProgress - StatusBlocked = beads.StatusBlocked - StatusDeferred = beads.StatusDeferred - StatusClosed = beads.StatusClosed + StatusOpen = beads.StatusOpen + StatusInProgress = beads.StatusInProgress + StatusBlocked = beads.StatusBlocked + StatusDeferred = beads.StatusDeferred + StatusClosed = beads.StatusClosed + StatusReadyToSprint Status = "ready_to_sprint" ) // IssueType constants diff --git a/pkg/tui/components/issue_list.go b/pkg/tui/components/issue_list.go index 1667e34..2f237d4 100644 --- a/pkg/tui/components/issue_list.go +++ b/pkg/tui/components/issue_list.go @@ -213,7 +213,9 @@ func OpenAndInProgressOnly(issues []*models.Issue) []*models.Issue { // used to display open & in-progress issues in the first window in the dashboard out := make([]*models.Issue, 0, len(issues)) for _, issue := range issues { - if issue.Status == models.StatusOpen || issue.Status == models.StatusInProgress { + if issue.Status == models.StatusOpen || + issue.Status == models.StatusInProgress || + issue.Status == models.StatusReadyToSprint { out = append(out, issue) } } @@ -237,7 +239,8 @@ func ClosedOnly(issues []*models.Issue) []*models.Issue { func StatusOnly(issues []*models.Issue, status models.Status) []*models.Issue { out := make([]*models.Issue, 0, len(issues)) for _, issue := range issues { - if issue.Status == status { + if issue.Status == status || + (status == models.StatusOpen && issue.Status == models.StatusReadyToSprint) { out = append(out, issue) } } diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 305f24f..2ad7524 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -343,6 +343,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.choosingStatus = false m.statusIssueID = "" return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) + case "r": + m.logAction("tui selected issue status ready_to_sprint") + issueID := m.statusIssueID + m.choosingStatus = false + m.statusIssueID = "" + return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) case "c": m.logAction("tui selected issue status closed") issueID := m.statusIssueID diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index 03a6beb..8a0e040 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -244,6 +244,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.choosingStatus = false m.statusIssueID = "" return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) + case "r": + issueID := m.statusIssueID + m.choosingStatus = false + m.statusIssueID = "" + return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) case "c": issueID := m.statusIssueID m.choosingStatus = false diff --git a/pkg/web/components/issue.templ b/pkg/web/components/issue.templ index 894bdbd..faedc3c 100644 --- a/pkg/web/components/issue.templ +++ b/pkg/web/components/issue.templ @@ -57,6 +57,7 @@ templ IssueForm(props IssueFormProps) { Options: []base.SelectOption{ {Label: "Open", Value: "open", Selected: props.Status == "open"}, {Label: "In Progress", Value: "in_progress", Selected: props.Status == "in_progress"}, + {Label: "Ready to sprint", Value: "ready_to_sprint", Selected: props.Status == "ready_to_sprint"}, {Label: "Closed", Value: "closed", Selected: props.Status == "closed"}, }, Size: "md", @@ -159,7 +160,9 @@ templ IssueRows(issues []*models.Issue) { hx-push-url="true" >{ issue.Title } - { string(issue.Status) } + + @StatusBadge(issue.Status) + { string(issue.IssueType) } { fmt.Sprintf("P%d", issue.Priority) } diff --git a/pkg/web/components/issue_detail.templ b/pkg/web/components/issue_detail.templ index f8b811a..04f101a 100644 --- a/pkg/web/components/issue_detail.templ +++ b/pkg/web/components/issue_detail.templ @@ -60,6 +60,8 @@ templ StatusBadge(status models.Status) { Open case "in_progress": In Progress + case "ready_to_sprint": + Ready to sprint case "closed": Closed case "blocked": diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index 3306c7a..aad3f63 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -17,7 +17,7 @@ const commentsKey = "comments" type IssueForm struct { Title string `form:"title" validate:"required,max=255"` Description string `form:"description" validate:"max=2000"` - Status models.Status `form:"status" validate:"required,oneof=open in_progress closed"` + Status models.Status `form:"status" validate:"required,oneof=open in_progress ready_to_sprint closed"` IssueType models.IssueType `form:"issue_type" validate:"required,oneof=task bug feature chore"` Priority int `form:"priority" validate:"gte=0,lte=4"` } @@ -25,7 +25,7 @@ type IssueForm struct { type UpdateIssueForm struct { Title *string `form:"title" validate:"omitempty,max=255"` Description *string `form:"description" validate:"omitempty,max=2000"` - Status *models.Status `form:"status" validate:"omitempty,oneof=open in_progress closed"` + Status *models.Status `form:"status" validate:"omitempty,oneof=open in_progress ready_to_sprint closed"` CloseReason *string `form:"close_reason" validate:"omitempty,max=2000"` IssueType *models.IssueType `form:"issue_type" validate:"omitempty,oneof=task bug feature chore"` Priority *int `form:"priority" validate:"omitempty,gte=0,lte=4"` diff --git a/pkg/web/routes/boardview.templ b/pkg/web/routes/boardview.templ index 13927dc..e926309 100644 --- a/pkg/web/routes/boardview.templ +++ b/pkg/web/routes/boardview.templ @@ -62,6 +62,7 @@ templ BoardColumns(issues []*models.Issue) { {{ openIssues := []*models.Issue{} inProgressIssues := []*models.Issue{} + readyToSprintIssues := []*models.Issue{} closedIssues := []*models.Issue{} for _, issue := range issues { @@ -70,6 +71,8 @@ templ BoardColumns(issues []*models.Issue) { openIssues = append(openIssues, issue) case "in_progress": inProgressIssues = append(inProgressIssues, issue) + case "ready_to_sprint": + readyToSprintIssues = append(readyToSprintIssues, issue) case "closed": closedIssues = append(closedIssues, issue) } @@ -78,6 +81,7 @@ templ BoardColumns(issues []*models.Issue) {
@BoardColumn("Open", "open", openIssues, "badge-info") @BoardColumn("In Progress", "in_progress", inProgressIssues, "badge-warning") + @BoardColumn("Ready to sprint", "ready_to_sprint", readyToSprintIssues, "badge-primary") @BoardColumn("Closed", "closed", closedIssues, "badge-success")
}