- Add TypeRig constant to IssueType enum - Update IsValid() method to include TypeRig - Add UI color (orange) and style for rig type - Update CLI flag descriptions in create and update commands - Add Obsidian export tag for rig type - Add comprehensive test cases for rig and other newer types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
624 lines
20 KiB
Go
624 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/beads/internal/hooks"
|
|
"github.com/steveyegge/beads/internal/rpc"
|
|
"github.com/steveyegge/beads/internal/timeparsing"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
"github.com/steveyegge/beads/internal/ui"
|
|
"github.com/steveyegge/beads/internal/validation"
|
|
)
|
|
|
|
var updateCmd = &cobra.Command{
|
|
Use: "update [id...]",
|
|
GroupID: "issues",
|
|
Short: "Update one or more issues",
|
|
Long: `Update one or more issues.
|
|
|
|
If no issue ID is provided, updates the last touched issue (from most recent
|
|
create, update, show, or close operation).`,
|
|
Args: cobra.MinimumNArgs(0),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
CheckReadonly("update")
|
|
|
|
// If no IDs provided, use last touched issue
|
|
if len(args) == 0 {
|
|
lastTouched := GetLastTouchedID()
|
|
if lastTouched == "" {
|
|
FatalErrorRespectJSON("no issue ID provided and no last touched issue")
|
|
}
|
|
args = []string{lastTouched}
|
|
}
|
|
|
|
updates := make(map[string]interface{})
|
|
|
|
if cmd.Flags().Changed("status") {
|
|
status, _ := cmd.Flags().GetString("status")
|
|
updates["status"] = status
|
|
|
|
// If status is being set to closed, include session if provided
|
|
if status == "closed" {
|
|
session, _ := cmd.Flags().GetString("session")
|
|
if session == "" {
|
|
session = os.Getenv("CLAUDE_SESSION_ID")
|
|
}
|
|
if session != "" {
|
|
updates["closed_by_session"] = session
|
|
}
|
|
}
|
|
}
|
|
if cmd.Flags().Changed("priority") {
|
|
priorityStr, _ := cmd.Flags().GetString("priority")
|
|
priority, err := validation.ValidatePriority(priorityStr)
|
|
if err != nil {
|
|
FatalErrorRespectJSON("%v", err)
|
|
}
|
|
updates["priority"] = priority
|
|
}
|
|
if cmd.Flags().Changed("title") {
|
|
title, _ := cmd.Flags().GetString("title")
|
|
updates["title"] = title
|
|
}
|
|
if cmd.Flags().Changed("assignee") {
|
|
assignee, _ := cmd.Flags().GetString("assignee")
|
|
updates["assignee"] = assignee
|
|
}
|
|
description, descChanged := getDescriptionFlag(cmd)
|
|
if descChanged {
|
|
updates["description"] = description
|
|
}
|
|
if cmd.Flags().Changed("design") {
|
|
design, _ := cmd.Flags().GetString("design")
|
|
updates["design"] = design
|
|
}
|
|
if cmd.Flags().Changed("notes") {
|
|
notes, _ := cmd.Flags().GetString("notes")
|
|
updates["notes"] = notes
|
|
}
|
|
if cmd.Flags().Changed("acceptance") || cmd.Flags().Changed("acceptance-criteria") {
|
|
var acceptanceCriteria string
|
|
if cmd.Flags().Changed("acceptance") {
|
|
acceptanceCriteria, _ = cmd.Flags().GetString("acceptance")
|
|
} else {
|
|
acceptanceCriteria, _ = cmd.Flags().GetString("acceptance-criteria")
|
|
}
|
|
updates["acceptance_criteria"] = acceptanceCriteria
|
|
}
|
|
if cmd.Flags().Changed("external-ref") {
|
|
externalRef, _ := cmd.Flags().GetString("external-ref")
|
|
updates["external_ref"] = externalRef
|
|
}
|
|
if cmd.Flags().Changed("estimate") {
|
|
estimate, _ := cmd.Flags().GetInt("estimate")
|
|
if estimate < 0 {
|
|
FatalErrorRespectJSON("estimate must be a non-negative number of minutes")
|
|
}
|
|
updates["estimated_minutes"] = estimate
|
|
}
|
|
if cmd.Flags().Changed("type") {
|
|
issueType, _ := cmd.Flags().GetString("type")
|
|
// Validate issue type
|
|
if !types.IssueType(issueType).IsValid() {
|
|
FatalErrorRespectJSON("invalid issue type %q. Valid types: bug, feature, task, epic, chore, merge-request, molecule, gate, agent, role, rig, convoy, event, slot", issueType)
|
|
}
|
|
updates["issue_type"] = issueType
|
|
}
|
|
if cmd.Flags().Changed("add-label") {
|
|
addLabels, _ := cmd.Flags().GetStringSlice("add-label")
|
|
updates["add_labels"] = addLabels
|
|
}
|
|
if cmd.Flags().Changed("remove-label") {
|
|
removeLabels, _ := cmd.Flags().GetStringSlice("remove-label")
|
|
updates["remove_labels"] = removeLabels
|
|
}
|
|
if cmd.Flags().Changed("set-labels") {
|
|
setLabels, _ := cmd.Flags().GetStringSlice("set-labels")
|
|
updates["set_labels"] = setLabels
|
|
}
|
|
if cmd.Flags().Changed("parent") {
|
|
parent, _ := cmd.Flags().GetString("parent")
|
|
updates["parent"] = parent
|
|
}
|
|
if cmd.Flags().Changed("type") {
|
|
issueType, _ := cmd.Flags().GetString("type")
|
|
// Validate issue type
|
|
if _, err := validation.ParseIssueType(issueType); err != nil {
|
|
FatalErrorRespectJSON("%v", err)
|
|
}
|
|
updates["issue_type"] = issueType
|
|
}
|
|
// Gate fields (bd-z6kw)
|
|
if cmd.Flags().Changed("await-id") {
|
|
awaitID, _ := cmd.Flags().GetString("await-id")
|
|
updates["await_id"] = awaitID
|
|
}
|
|
// Time-based scheduling flags (GH#820)
|
|
if cmd.Flags().Changed("due") {
|
|
dueStr, _ := cmd.Flags().GetString("due")
|
|
if dueStr == "" {
|
|
// Empty string clears the due date
|
|
updates["due_at"] = nil
|
|
} else {
|
|
t, err := timeparsing.ParseRelativeTime(dueStr, time.Now())
|
|
if err != nil {
|
|
FatalErrorRespectJSON("invalid --due format %q. Examples: +6h, tomorrow, next monday, 2025-01-15", dueStr)
|
|
}
|
|
updates["due_at"] = t
|
|
}
|
|
}
|
|
if cmd.Flags().Changed("defer") {
|
|
deferStr, _ := cmd.Flags().GetString("defer")
|
|
if deferStr == "" {
|
|
// Empty string clears the defer_until
|
|
updates["defer_until"] = nil
|
|
} else {
|
|
t, err := timeparsing.ParseRelativeTime(deferStr, time.Now())
|
|
if err != nil {
|
|
FatalErrorRespectJSON("invalid --defer format %q. Examples: +1h, tomorrow, next monday, 2025-01-15", deferStr)
|
|
}
|
|
// Warn if defer date is in the past (user probably meant future)
|
|
if t.Before(time.Now()) && !jsonOutput {
|
|
fmt.Fprintf(os.Stderr, "%s Defer date %q is in the past. Issue will appear in bd ready immediately.\n",
|
|
ui.RenderWarn("!"), t.Format("2006-01-02 15:04"))
|
|
fmt.Fprintf(os.Stderr, " Did you mean a future date? Use --defer=+1h or --defer=tomorrow\n")
|
|
}
|
|
updates["defer_until"] = t
|
|
}
|
|
}
|
|
|
|
// Get claim flag
|
|
claimFlag, _ := cmd.Flags().GetBool("claim")
|
|
|
|
if len(updates) == 0 && !claimFlag {
|
|
fmt.Println("No updates specified")
|
|
return
|
|
}
|
|
|
|
ctx := rootCtx
|
|
|
|
// Resolve partial IDs first, checking for cross-rig routing
|
|
var resolvedIDs []string
|
|
var routedArgs []string // IDs that need cross-repo routing (bypass daemon)
|
|
if daemonClient != nil {
|
|
// In daemon mode, resolve via RPC - but check routing first
|
|
for _, id := range args {
|
|
// Check if this ID needs routing to a different beads directory
|
|
if needsRouting(id) {
|
|
routedArgs = append(routedArgs, id)
|
|
continue
|
|
}
|
|
resolveArgs := &rpc.ResolveIDArgs{ID: id}
|
|
resp, err := daemonClient.ResolveID(resolveArgs)
|
|
if err != nil {
|
|
FatalErrorRespectJSON("resolving ID %s: %v", id, err)
|
|
}
|
|
var resolvedID string
|
|
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
|
|
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
|
|
}
|
|
resolvedIDs = append(resolvedIDs, resolvedID)
|
|
}
|
|
}
|
|
// Note: Direct mode (no daemon) uses resolveAndGetIssueWithRouting in the loop below
|
|
|
|
// If daemon is running, use RPC
|
|
if daemonClient != nil {
|
|
updatedIssues := []*types.Issue{}
|
|
var firstUpdatedID string // Track first successful update for last-touched
|
|
for _, id := range resolvedIDs {
|
|
updateArgs := &rpc.UpdateArgs{ID: id}
|
|
|
|
// Map updates to RPC args
|
|
if status, ok := updates["status"].(string); ok {
|
|
updateArgs.Status = &status
|
|
}
|
|
if priority, ok := updates["priority"].(int); ok {
|
|
updateArgs.Priority = &priority
|
|
}
|
|
if title, ok := updates["title"].(string); ok {
|
|
updateArgs.Title = &title
|
|
}
|
|
if assignee, ok := updates["assignee"].(string); ok {
|
|
updateArgs.Assignee = &assignee
|
|
}
|
|
if description, ok := updates["description"].(string); ok {
|
|
updateArgs.Description = &description
|
|
}
|
|
if design, ok := updates["design"].(string); ok {
|
|
updateArgs.Design = &design
|
|
}
|
|
if notes, ok := updates["notes"].(string); ok {
|
|
updateArgs.Notes = ¬es
|
|
}
|
|
if acceptanceCriteria, ok := updates["acceptance_criteria"].(string); ok {
|
|
updateArgs.AcceptanceCriteria = &acceptanceCriteria
|
|
}
|
|
if externalRef, ok := updates["external_ref"].(string); ok {
|
|
updateArgs.ExternalRef = &externalRef
|
|
}
|
|
if estimate, ok := updates["estimated_minutes"].(int); ok {
|
|
updateArgs.EstimatedMinutes = &estimate
|
|
}
|
|
if issueType, ok := updates["issue_type"].(string); ok {
|
|
updateArgs.IssueType = &issueType
|
|
}
|
|
if addLabels, ok := updates["add_labels"].([]string); ok {
|
|
updateArgs.AddLabels = addLabels
|
|
}
|
|
if removeLabels, ok := updates["remove_labels"].([]string); ok {
|
|
updateArgs.RemoveLabels = removeLabels
|
|
}
|
|
if setLabels, ok := updates["set_labels"].([]string); ok {
|
|
updateArgs.SetLabels = setLabels
|
|
}
|
|
if issueType, ok := updates["issue_type"].(string); ok {
|
|
updateArgs.IssueType = &issueType
|
|
}
|
|
if parent, ok := updates["parent"].(string); ok {
|
|
updateArgs.Parent = &parent
|
|
}
|
|
// Gate fields (bd-z6kw)
|
|
if awaitID, ok := updates["await_id"].(string); ok {
|
|
updateArgs.AwaitID = &awaitID
|
|
}
|
|
// Time-based scheduling (GH#820)
|
|
if dueAt, ok := updates["due_at"].(time.Time); ok {
|
|
s := dueAt.Format(time.RFC3339)
|
|
updateArgs.DueAt = &s
|
|
} else if updates["due_at"] == nil && cmd.Flags().Changed("due") {
|
|
// Explicit clear
|
|
empty := ""
|
|
updateArgs.DueAt = &empty
|
|
}
|
|
if deferUntil, ok := updates["defer_until"].(time.Time); ok {
|
|
s := deferUntil.Format(time.RFC3339)
|
|
updateArgs.DeferUntil = &s
|
|
} else if updates["defer_until"] == nil && cmd.Flags().Changed("defer") {
|
|
// Explicit clear
|
|
empty := ""
|
|
updateArgs.DeferUntil = &empty
|
|
}
|
|
|
|
// Set claim flag for atomic claim operation
|
|
updateArgs.Claim = claimFlag
|
|
|
|
resp, err := daemonClient.Update(updateArgs)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
|
continue
|
|
}
|
|
|
|
var issue types.Issue
|
|
if err := json.Unmarshal(resp.Data, &issue); err == nil {
|
|
// Run update hook
|
|
if hookRunner != nil {
|
|
hookRunner.Run(hooks.EventUpdate, &issue)
|
|
}
|
|
if jsonOutput {
|
|
updatedIssues = append(updatedIssues, &issue)
|
|
}
|
|
}
|
|
if !jsonOutput {
|
|
fmt.Printf("%s Updated issue: %s\n", ui.RenderPass("✓"), id)
|
|
}
|
|
|
|
// Track first successful update for last-touched
|
|
if firstUpdatedID == "" {
|
|
firstUpdatedID = id
|
|
}
|
|
}
|
|
|
|
// Handle routed IDs via direct mode (bypass daemon)
|
|
for _, id := range routedArgs {
|
|
result, err := resolveAndGetIssueWithRouting(ctx, store, id)
|
|
if err != nil {
|
|
if result != nil {
|
|
result.Close()
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
|
continue
|
|
}
|
|
if result == nil || result.Issue == nil {
|
|
if result != nil {
|
|
result.Close()
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
|
|
continue
|
|
}
|
|
issue := result.Issue
|
|
issueStore := result.Store
|
|
|
|
if err := validateIssueUpdatable(id, issue); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
|
|
// Handle claim operation atomically
|
|
if claimFlag {
|
|
if issue.Assignee != "" {
|
|
fmt.Fprintf(os.Stderr, "Error claiming %s: already claimed by %s\n", id, issue.Assignee)
|
|
result.Close()
|
|
continue
|
|
}
|
|
claimUpdates := map[string]interface{}{
|
|
"assignee": actor,
|
|
"status": "in_progress",
|
|
}
|
|
if err := issueStore.UpdateIssue(ctx, result.ResolvedID, claimUpdates, actor); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error claiming %s: %v\n", id, err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Apply regular field updates if any
|
|
regularUpdates := make(map[string]interface{})
|
|
for k, v := range updates {
|
|
if k != "add_labels" && k != "remove_labels" && k != "set_labels" && k != "parent" {
|
|
regularUpdates[k] = v
|
|
}
|
|
}
|
|
if len(regularUpdates) > 0 {
|
|
if err := issueStore.UpdateIssue(ctx, result.ResolvedID, regularUpdates, actor); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Handle label operations
|
|
var setLabels, addLabels, removeLabels []string
|
|
if v, ok := updates["set_labels"].([]string); ok {
|
|
setLabels = v
|
|
}
|
|
if v, ok := updates["add_labels"].([]string); ok {
|
|
addLabels = v
|
|
}
|
|
if v, ok := updates["remove_labels"].([]string); ok {
|
|
removeLabels = v
|
|
}
|
|
if len(setLabels) > 0 || len(addLabels) > 0 || len(removeLabels) > 0 {
|
|
if err := applyLabelUpdates(ctx, issueStore, result.ResolvedID, actor, setLabels, addLabels, removeLabels); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error updating labels for %s: %v\n", id, err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Run update hook
|
|
updatedIssue, _ := issueStore.GetIssue(ctx, result.ResolvedID)
|
|
if updatedIssue != nil && hookRunner != nil {
|
|
hookRunner.Run(hooks.EventUpdate, updatedIssue)
|
|
}
|
|
|
|
if jsonOutput {
|
|
if updatedIssue != nil {
|
|
updatedIssues = append(updatedIssues, updatedIssue)
|
|
}
|
|
} else {
|
|
fmt.Printf("%s Updated issue: %s\n", ui.RenderPass("✓"), result.ResolvedID)
|
|
}
|
|
|
|
if firstUpdatedID == "" {
|
|
firstUpdatedID = result.ResolvedID
|
|
}
|
|
result.Close()
|
|
}
|
|
|
|
if jsonOutput && len(updatedIssues) > 0 {
|
|
outputJSON(updatedIssues)
|
|
}
|
|
|
|
// Set last touched after all updates complete
|
|
if firstUpdatedID != "" {
|
|
SetLastTouchedID(firstUpdatedID)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Direct mode - use routed resolution for cross-repo lookups
|
|
updatedIssues := []*types.Issue{}
|
|
var firstUpdatedID string // Track first successful update for last-touched
|
|
for _, id := range args {
|
|
// Resolve and get issue with routing (e.g., gt-xyz routes to gastown)
|
|
result, err := resolveAndGetIssueWithRouting(ctx, store, id)
|
|
if err != nil {
|
|
if result != nil {
|
|
result.Close()
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
|
continue
|
|
}
|
|
if result == nil || result.Issue == nil {
|
|
if result != nil {
|
|
result.Close()
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
|
|
continue
|
|
}
|
|
issue := result.Issue
|
|
issueStore := result.Store
|
|
|
|
if err := validateIssueUpdatable(id, issue); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
|
|
// Handle claim operation atomically
|
|
if claimFlag {
|
|
// Check if already claimed (has non-empty assignee)
|
|
if issue.Assignee != "" {
|
|
fmt.Fprintf(os.Stderr, "Error claiming %s: already claimed by %s\n", id, issue.Assignee)
|
|
result.Close()
|
|
continue
|
|
}
|
|
// Atomically set assignee and status
|
|
claimUpdates := map[string]interface{}{
|
|
"assignee": actor,
|
|
"status": "in_progress",
|
|
}
|
|
if err := issueStore.UpdateIssue(ctx, result.ResolvedID, claimUpdates, actor); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error claiming %s: %v\n", id, err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Apply regular field updates if any
|
|
regularUpdates := make(map[string]interface{})
|
|
for k, v := range updates {
|
|
if k != "add_labels" && k != "remove_labels" && k != "set_labels" && k != "parent" {
|
|
regularUpdates[k] = v
|
|
}
|
|
}
|
|
if len(regularUpdates) > 0 {
|
|
if err := issueStore.UpdateIssue(ctx, result.ResolvedID, regularUpdates, actor); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Handle label operations
|
|
var setLabels, addLabels, removeLabels []string
|
|
if v, ok := updates["set_labels"].([]string); ok {
|
|
setLabels = v
|
|
}
|
|
if v, ok := updates["add_labels"].([]string); ok {
|
|
addLabels = v
|
|
}
|
|
if v, ok := updates["remove_labels"].([]string); ok {
|
|
removeLabels = v
|
|
}
|
|
if len(setLabels) > 0 || len(addLabels) > 0 || len(removeLabels) > 0 {
|
|
if err := applyLabelUpdates(ctx, issueStore, result.ResolvedID, actor, setLabels, addLabels, removeLabels); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error updating labels for %s: %v\n", id, err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Handle parent reparenting
|
|
if newParent, ok := updates["parent"].(string); ok {
|
|
// Validate new parent exists (unless empty string to remove parent)
|
|
if newParent != "" {
|
|
parentIssue, err := issueStore.GetIssue(ctx, newParent)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error getting parent %s: %v\n", newParent, err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
if parentIssue == nil {
|
|
fmt.Fprintf(os.Stderr, "Error: parent issue %s not found\n", newParent)
|
|
result.Close()
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Find and remove existing parent-child dependency
|
|
deps, err := issueStore.GetDependencyRecords(ctx, result.ResolvedID)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error getting dependencies for %s: %v\n", id, err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
for _, dep := range deps {
|
|
if dep.Type == types.DepParentChild {
|
|
if err := issueStore.RemoveDependency(ctx, result.ResolvedID, dep.DependsOnID, actor); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error removing old parent dependency: %v\n", err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Add new parent-child dependency (if not removing parent)
|
|
if newParent != "" {
|
|
newDep := &types.Dependency{
|
|
IssueID: result.ResolvedID,
|
|
DependsOnID: newParent,
|
|
Type: types.DepParentChild,
|
|
}
|
|
if err := issueStore.AddDependency(ctx, newDep, actor); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error adding parent dependency: %v\n", err)
|
|
result.Close()
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run update hook
|
|
updatedIssue, _ := issueStore.GetIssue(ctx, result.ResolvedID)
|
|
if updatedIssue != nil && hookRunner != nil {
|
|
hookRunner.Run(hooks.EventUpdate, updatedIssue)
|
|
}
|
|
|
|
if jsonOutput {
|
|
if updatedIssue != nil {
|
|
updatedIssues = append(updatedIssues, updatedIssue)
|
|
}
|
|
} else {
|
|
fmt.Printf("%s Updated issue: %s\n", ui.RenderPass("✓"), result.ResolvedID)
|
|
}
|
|
|
|
// Track first successful update for last-touched
|
|
if firstUpdatedID == "" {
|
|
firstUpdatedID = result.ResolvedID
|
|
}
|
|
result.Close()
|
|
}
|
|
|
|
// Set last touched after all updates complete
|
|
if firstUpdatedID != "" {
|
|
SetLastTouchedID(firstUpdatedID)
|
|
}
|
|
|
|
// Schedule auto-flush if any issues were updated
|
|
if len(args) > 0 {
|
|
markDirtyAndScheduleFlush()
|
|
}
|
|
|
|
if jsonOutput && len(updatedIssues) > 0 {
|
|
outputJSON(updatedIssues)
|
|
}
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
updateCmd.Flags().StringP("status", "s", "", "New status")
|
|
registerPriorityFlag(updateCmd, "")
|
|
updateCmd.Flags().String("title", "", "New title")
|
|
updateCmd.Flags().StringP("type", "t", "", "New type (bug|feature|task|epic|chore|merge-request|molecule|gate|agent|role|rig|convoy|event|slot)")
|
|
registerCommonIssueFlags(updateCmd)
|
|
updateCmd.Flags().String("acceptance-criteria", "", "DEPRECATED: use --acceptance")
|
|
_ = updateCmd.Flags().MarkHidden("acceptance-criteria") // Only fails if flag missing (caught in tests)
|
|
updateCmd.Flags().IntP("estimate", "e", 0, "Time estimate in minutes (e.g., 60 for 1 hour)")
|
|
updateCmd.Flags().StringSlice("add-label", nil, "Add labels (repeatable)")
|
|
updateCmd.Flags().StringSlice("remove-label", nil, "Remove labels (repeatable)")
|
|
updateCmd.Flags().StringSlice("set-labels", nil, "Set labels, replacing all existing (repeatable)")
|
|
updateCmd.Flags().String("parent", "", "New parent issue ID (reparents the issue, use empty string to remove parent)")
|
|
updateCmd.Flags().Bool("claim", false, "Atomically claim the issue (sets assignee to you, status to in_progress; fails if already claimed)")
|
|
updateCmd.Flags().String("session", "", "Claude Code session ID for status=closed (or set CLAUDE_SESSION_ID env var)")
|
|
// Time-based scheduling flags (GH#820)
|
|
// Examples:
|
|
// --due=+6h Due in 6 hours
|
|
// --due=tomorrow Due tomorrow
|
|
// --due="next monday" Due next Monday
|
|
// --due=2025-01-15 Due on specific date
|
|
// --due="" Clear due date
|
|
// --defer=+1h Hidden from bd ready for 1 hour
|
|
// --defer="" Clear defer (show in bd ready immediately)
|
|
updateCmd.Flags().String("due", "", "Due date/time (empty to clear). Formats: +6h, +1d, +2w, tomorrow, next monday, 2025-01-15")
|
|
updateCmd.Flags().String("defer", "", "Defer until date (empty to clear). Issue hidden from bd ready until then")
|
|
// Gate fields (bd-z6kw)
|
|
updateCmd.Flags().String("await-id", "", "Set gate await_id (e.g., GitHub run ID for gh:run gates)")
|
|
rootCmd.AddCommand(updateCmd)
|
|
}
|