# Conflicts:
#	.beads/issues.jsonl
This commit is contained in:
Steve Yegge
2025-11-20 19:19:28 -05:00
8 changed files with 2708 additions and 204 deletions

File diff suppressed because one or more lines are too long

View File

@@ -579,6 +579,17 @@ func TestCLI_PriorityFormats(t *testing.T) {
if issue["priority"].(float64) != 3 {
t.Errorf("Expected priority 3, got: %v", issue["priority"])
}
// Test update with P-format
id := issue["id"].(string)
runBDInProcess(t, tmpDir, "update", id, "-p", "P1")
out = runBDInProcess(t, tmpDir, "show", id, "--json")
var updated []map[string]interface{}
json.Unmarshal([]byte(out), &updated)
if updated[0]["priority"].(float64) != 1 {
t.Errorf("Expected priority 1 after update, got: %v", updated[0]["priority"])
}
}
func TestCLI_Reopen(t *testing.T) {

View File

@@ -14,6 +14,7 @@ import (
"github.com/steveyegge/beads/internal/routing"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/validation"
)
var createCmd = &cobra.Command{
@@ -94,7 +95,7 @@ var createCmd = &cobra.Command{
// Parse priority (supports both "1" and "P1" formats)
priorityStr, _ := cmd.Flags().GetString("priority")
priority := parsePriority(priorityStr)
priority := validation.ParsePriority(priorityStr)
if priority == -1 {
fmt.Fprintf(os.Stderr, "Error: invalid priority %q (expected 0-4 or P0-P4)\n", priorityStr)
os.Exit(1)
@@ -394,18 +395,14 @@ func init() {
createCmd.Flags().StringP("file", "f", "", "Create multiple issues from markdown file")
createCmd.Flags().String("from-template", "", "Create issue from template (e.g., 'epic', 'bug', 'feature')")
createCmd.Flags().String("title", "", "Issue title (alternative to positional argument)")
createCmd.Flags().StringP("description", "d", "", "Issue description")
createCmd.Flags().String("design", "", "Design notes")
createCmd.Flags().String("acceptance", "", "Acceptance criteria")
createCmd.Flags().StringP("priority", "p", "2", "Priority (0-4 or P0-P4, 0=highest)")
registerPriorityFlag(createCmd, "2")
createCmd.Flags().StringP("type", "t", "task", "Issue type (bug|feature|task|epic|chore)")
createCmd.Flags().StringP("assignee", "a", "", "Assignee")
registerCommonIssueFlags(createCmd)
createCmd.Flags().StringSliceP("labels", "l", []string{}, "Labels (comma-separated)")
createCmd.Flags().StringSlice("label", []string{}, "Alias for --labels")
_ = createCmd.Flags().MarkHidden("label")
createCmd.Flags().String("id", "", "Explicit issue ID (e.g., 'bd-42' for partitioning)")
createCmd.Flags().String("parent", "", "Parent issue ID for hierarchical child (e.g., 'bd-a3f8e9')")
createCmd.Flags().String("external-ref", "", "External reference (e.g., 'gh-9', 'jira-ABC')")
createCmd.Flags().StringSlice("deps", []string{}, "Dependencies in format 'type:id' or 'id' (e.g., 'discovered-from:bd-20,blocks:bd-15' or 'bd-20')")
createCmd.Flags().Bool("force", false, "Force creation even if prefix doesn't match database prefix")
createCmd.Flags().String("repo", "", "Target repository for issue (overrides auto-routing)")

17
cmd/bd/flags.go Normal file
View File

@@ -0,0 +1,17 @@
package main
import "github.com/spf13/cobra"
// registerCommonIssueFlags registers flags common to create and update commands.
func registerCommonIssueFlags(cmd *cobra.Command) {
cmd.Flags().StringP("assignee", "a", "", "Assignee")
cmd.Flags().StringP("description", "d", "", "Issue description")
cmd.Flags().String("design", "", "Design notes")
cmd.Flags().String("acceptance", "", "Acceptance criteria")
cmd.Flags().String("external-ref", "", "External reference (e.g., 'gh-9', 'jira-ABC')")
}
// registerPriorityFlag registers the priority flag with a specific default value.
func registerPriorityFlag(cmd *cobra.Command, defaultVal string) {
cmd.Flags().StringP("priority", "p", defaultVal, "Priority (0-4 or P0-P4, 0=highest)")
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/validation"
)
var (
@@ -39,47 +40,7 @@ type IssueTemplate struct {
Dependencies []string
}
// parsePriority extracts and validates a priority value from content.
// Supports both numeric (0-4) and P-prefix format (P0-P4).
// Returns the parsed priority (0-4) or -1 if invalid.
func parsePriority(content string) int {
content = strings.TrimSpace(content)
// Handle "P1", "P0", etc. format
if strings.HasPrefix(strings.ToUpper(content), "P") {
content = content[1:] // Strip the "P" prefix
}
var p int
if _, err := fmt.Sscanf(content, "%d", &p); err == nil && p >= 0 && p <= 4 {
return p
}
return -1 // Invalid
}
// parseIssueType extracts and validates an issue type from content.
// Returns the validated type or empty string if invalid.
func parseIssueType(content, issueTitle string) types.IssueType {
issueType := types.IssueType(strings.TrimSpace(content))
// Validate issue type
validTypes := map[types.IssueType]bool{
types.TypeBug: true,
types.TypeFeature: true,
types.TypeTask: true,
types.TypeEpic: true,
types.TypeChore: true,
}
if !validTypes[issueType] {
// Warn but continue with default
fmt.Fprintf(os.Stderr, "Warning: invalid issue type '%s' in '%s', using default 'task'\n",
issueType, issueTitle)
return types.TypeTask
}
return issueType
}
// parseStringList extracts a list of strings from content, splitting by comma or whitespace.
// This is a generic helper used by parseLabels and parseDependencies.
@@ -116,11 +77,18 @@ func processIssueSection(issue *IssueTemplate, section, content string) {
switch strings.ToLower(section) {
case "priority":
if p := parsePriority(content); p != -1 {
if p := validation.ParsePriority(content); p != -1 {
issue.Priority = p
}
case "type":
issue.IssueType = parseIssueType(content, issue.Title)
t, err := validation.ParseIssueType(content)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: invalid issue type '%s' in '%s', using default 'task'\n",
strings.TrimSpace(content), issue.Title)
issue.IssueType = types.TypeTask
} else {
issue.IssueType = t
}
case "description":
issue.Description = content
case "design":

View File

@@ -13,6 +13,7 @@ import (
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/utils"
"github.com/steveyegge/beads/internal/validation"
)
var showCmd = &cobra.Command{
@@ -343,7 +344,12 @@ var updateCmd = &cobra.Command{
updates["status"] = status
}
if cmd.Flags().Changed("priority") {
priority, _ := cmd.Flags().GetInt("priority")
priorityStr, _ := cmd.Flags().GetString("priority")
priority := validation.ParsePriority(priorityStr)
if priority == -1 {
fmt.Fprintf(os.Stderr, "Error: invalid priority %q (expected 0-4 or P0-P4)\n", priorityStr)
os.Exit(1)
}
updates["priority"] = priority
}
if cmd.Flags().Changed("title") {
@@ -802,16 +808,13 @@ func init() {
rootCmd.AddCommand(showCmd)
updateCmd.Flags().StringP("status", "s", "", "New status")
updateCmd.Flags().IntP("priority", "p", 0, "New priority")
registerPriorityFlag(updateCmd, "")
updateCmd.Flags().String("title", "", "New title")
updateCmd.Flags().StringP("assignee", "a", "", "New assignee")
updateCmd.Flags().StringP("description", "d", "", "Issue description")
updateCmd.Flags().String("design", "", "Design notes")
registerCommonIssueFlags(updateCmd)
updateCmd.Flags().String("notes", "", "Additional notes")
updateCmd.Flags().String("acceptance", "", "Acceptance criteria")
updateCmd.Flags().String("acceptance-criteria", "", "DEPRECATED: use --acceptance")
_ = updateCmd.Flags().MarkHidden("acceptance-criteria")
updateCmd.Flags().String("external-ref", "", "External reference (e.g., 'gh-9', 'jira-ABC')")
updateCmd.Flags().Bool("json", false, "Output JSON format")
rootCmd.AddCommand(updateCmd)

View File

@@ -0,0 +1,47 @@
package validation
import (
"fmt"
"strings"
"github.com/steveyegge/beads/internal/types"
)
// ParsePriority extracts and validates a priority value from content.
// Supports both numeric (0-4) and P-prefix format (P0-P4).
// Returns the parsed priority (0-4) or -1 if invalid.
func ParsePriority(content string) int {
content = strings.TrimSpace(content)
// Handle "P1", "P0", etc. format
if strings.HasPrefix(strings.ToUpper(content), "P") {
content = content[1:] // Strip the "P" prefix
}
var p int
if _, err := fmt.Sscanf(content, "%d", &p); err == nil && p >= 0 && p <= 4 {
return p
}
return -1 // Invalid
}
// ParseIssueType extracts and validates an issue type from content.
// Returns the validated type or error if invalid.
func ParseIssueType(content string) (types.IssueType, error) {
issueType := types.IssueType(strings.TrimSpace(content))
// Validate issue type
validTypes := map[types.IssueType]bool{
types.TypeBug: true,
types.TypeFeature: true,
types.TypeTask: true,
types.TypeEpic: true,
types.TypeChore: true,
}
if !validTypes[issueType] {
return types.TypeTask, fmt.Errorf("invalid issue type: %s", content)
}
return issueType, nil
}

View File

@@ -1,4 +1,4 @@
package main
package validation
import (
"testing"
@@ -43,9 +43,9 @@ func TestParsePriority(t *testing.T) {
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := parsePriority(tt.input)
got := ParsePriority(tt.input)
if got != tt.expected {
t.Errorf("parsePriority(%q) = %d, want %d", tt.input, got, tt.expected)
t.Errorf("ParsePriority(%q) = %d, want %d", tt.input, got, tt.expected)
}
})
}