feat(task): implement update command

Amolith created

Full task update with fluent builder pattern from go-lunatask. Supports
name, area, goal, status, note, priority, estimate, motivation,
eisenhower (via --important/--urgent), and schedule.

Add dupl linter exclusion for cmd/ since TaskBuilder and
TaskUpdateBuilder share method signatures but differ in type.

Assisted-by: Claude Opus 4 via Crush

Change summary

.golangci.yaml     |   1 
AGENTS.md          |   2 
cmd/task/update.go | 191 +++++++++++++++++++++++++++++++++++++++++++----
3 files changed, 178 insertions(+), 16 deletions(-)

Detailed changes

.golangci.yaml 🔗

@@ -145,6 +145,7 @@ linters:
           - godox             # TODOs are intentional placeholders
           - wrapcheck         # CLI returns errors for display, wrapping adds noise
           - mnd               # Magic numbers in cobra.ExactArgs are clear in context
+          - dupl              # Builder types differ but share method signatures
       - path: cmd/
         text: unused-parameter  # Cobra callback signatures can't be changed
       - path: internal/ui/

AGENTS.md 🔗

@@ -108,6 +108,8 @@ The `.golangci.yaml` disables several linters for `cmd/`:
 - `errcheck`, `godox`: Stub implementations deliberately print without checking,
   TODOs are placeholders
 - `wrapcheck`: CLI returns errors for display; wrapping adds noise
+- `dupl`: Builder types (`TaskBuilder`, `TaskUpdateBuilder`) share method
+  signatures but differ in type; duplication is structural
 
 These patterns are intentional.
 

cmd/task/update.go 🔗

@@ -7,7 +7,12 @@ package task
 import (
 	"fmt"
 
+	"git.secluded.site/go-lunatask"
+	"git.secluded.site/lune/internal/client"
 	"git.secluded.site/lune/internal/completion"
+	"git.secluded.site/lune/internal/config"
+	"git.secluded.site/lune/internal/dateutil"
+	"git.secluded.site/lune/internal/ui"
 	"git.secluded.site/lune/internal/validate"
 	"github.com/spf13/cobra"
 )
@@ -16,38 +21,192 @@ import (
 var UpdateCmd = &cobra.Command{
 	Use:   "update ID",
 	Short: "Update a task",
-	Args:  cobra.ExactArgs(1),
-	RunE: func(cmd *cobra.Command, args []string) error {
-		id, err := validate.Reference(args[0])
-		if err != nil {
-			return err
-		}
-
-		// TODO: implement task update
-		fmt.Fprintf(cmd.OutOrStdout(), "Updating task %s (not yet implemented)\n", id)
+	Long: `Update an existing task in Lunatask.
 
-		return nil
-	},
+Accepts a UUID or lunatask:// deep link.
+Only specified flags are modified; other fields remain unchanged.`,
+	Args: cobra.ExactArgs(1),
+	RunE: runUpdate,
 }
 
 func init() {
-	UpdateCmd.Flags().String("name", "", "New task name")
+	UpdateCmd.Flags().String("name", "", "New task name (use - for stdin)")
 	UpdateCmd.Flags().StringP("area", "a", "", "Move to area key")
 	UpdateCmd.Flags().StringP("goal", "g", "", "Move to goal key")
 	UpdateCmd.Flags().StringP("status", "s", "", "Status: later, next, started, waiting, completed")
 	UpdateCmd.Flags().StringP("note", "n", "", "Task note (use - for stdin)")
-	UpdateCmd.Flags().IntP("priority", "p", 0, "Priority: -2 to 2")
+	UpdateCmd.Flags().StringP("priority", "p", "", "Priority: lowest, low, normal, high, highest")
 	UpdateCmd.Flags().IntP("estimate", "e", 0, "Estimate in minutes (0-720)")
 	UpdateCmd.Flags().StringP("motivation", "m", "", "Motivation: must, should, want")
-	UpdateCmd.Flags().Int("eisenhower", 0, "Eisenhower quadrant: 1-4")
+	UpdateCmd.Flags().Bool("important", false, "Mark as important (Eisenhower matrix)")
+	UpdateCmd.Flags().Bool("not-important", false, "Mark as not important")
+	UpdateCmd.Flags().Bool("urgent", false, "Mark as urgent (Eisenhower matrix)")
+	UpdateCmd.Flags().Bool("not-urgent", false, "Mark as not urgent")
 	UpdateCmd.Flags().String("schedule", "", "Schedule date (natural language)")
 
 	_ = UpdateCmd.RegisterFlagCompletionFunc("area", completion.Areas)
 	_ = UpdateCmd.RegisterFlagCompletionFunc("goal", completion.Goals)
 	_ = UpdateCmd.RegisterFlagCompletionFunc("status",
 		completion.Static("later", "next", "started", "waiting", "completed"))
+	_ = UpdateCmd.RegisterFlagCompletionFunc("priority",
+		completion.Static("lowest", "low", "normal", "high", "highest"))
 	_ = UpdateCmd.RegisterFlagCompletionFunc("motivation",
 		completion.Static("must", "should", "want"))
-	_ = UpdateCmd.RegisterFlagCompletionFunc("eisenhower",
-		completion.Static("1", "2", "3", "4"))
+}
+
+func runUpdate(cmd *cobra.Command, args []string) error {
+	id, err := validate.Reference(args[0])
+	if err != nil {
+		return err
+	}
+
+	apiClient, err := client.New()
+	if err != nil {
+		return err
+	}
+
+	builder := apiClient.NewTaskUpdate(id)
+
+	if err := applyUpdateName(cmd, builder); err != nil {
+		return err
+	}
+
+	if err := applyUpdateAreaAndGoal(cmd, builder); err != nil {
+		return err
+	}
+
+	if err := applyUpdateFlags(cmd, builder); err != nil {
+		return err
+	}
+
+	task, err := builder.Update(cmd.Context())
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Updated task: "+task.ID))
+
+	return nil
+}
+
+func applyUpdateName(cmd *cobra.Command, builder *lunatask.TaskUpdateBuilder) error {
+	name, _ := cmd.Flags().GetString("name")
+	if name == "" {
+		return nil
+	}
+
+	resolved, err := resolveName(name)
+	if err != nil {
+		return err
+	}
+
+	builder.Name(resolved)
+
+	return nil
+}
+
+func applyUpdateAreaAndGoal(cmd *cobra.Command, builder *lunatask.TaskUpdateBuilder) error {
+	areaKey, _ := cmd.Flags().GetString("area")
+	goalKey, _ := cmd.Flags().GetString("goal")
+
+	if areaKey == "" && goalKey == "" {
+		return nil
+	}
+
+	if areaKey == "" && goalKey != "" {
+		fmt.Fprintln(cmd.ErrOrStderr(), ui.Warning.Render("Goal specified without area; ignoring"))
+
+		return nil
+	}
+
+	cfg, err := config.Load()
+	if err != nil {
+		return err
+	}
+
+	area := cfg.AreaByKey(areaKey)
+	if area == nil {
+		return fmt.Errorf("%w: %s", ErrUnknownArea, areaKey)
+	}
+
+	builder.InArea(area.ID)
+
+	if goalKey == "" {
+		return nil
+	}
+
+	goal := area.GoalByKey(goalKey)
+	if goal == nil {
+		return fmt.Errorf("%w: %s", ErrUnknownGoal, goalKey)
+	}
+
+	builder.InGoal(goal.ID)
+
+	return nil
+}
+
+func applyUpdateFlags(cmd *cobra.Command, builder *lunatask.TaskUpdateBuilder) error {
+	if status, _ := cmd.Flags().GetString("status"); status != "" {
+		builder.WithStatus(lunatask.TaskStatus(status))
+	}
+
+	if note, _ := cmd.Flags().GetString("note"); note != "" {
+		resolved, err := resolveNote(note)
+		if err != nil {
+			return err
+		}
+
+		builder.WithNote(resolved)
+	}
+
+	if priority, _ := cmd.Flags().GetString("priority"); priority != "" {
+		p, err := lunatask.ParsePriority(priority)
+		if err != nil {
+			return err
+		}
+
+		builder.Priority(p)
+	}
+
+	if estimate, _ := cmd.Flags().GetInt("estimate"); estimate != 0 {
+		builder.WithEstimate(estimate)
+	}
+
+	if motivation, _ := cmd.Flags().GetString("motivation"); motivation != "" {
+		builder.WithMotivation(lunatask.Motivation(motivation))
+	}
+
+	applyUpdateEisenhower(cmd, builder)
+
+	return applyUpdateSchedule(cmd, builder)
+}
+
+func applyUpdateEisenhower(cmd *cobra.Command, builder *lunatask.TaskUpdateBuilder) {
+	if important, _ := cmd.Flags().GetBool("important"); important {
+		builder.Important()
+	} else if notImportant, _ := cmd.Flags().GetBool("not-important"); notImportant {
+		builder.NotImportant()
+	}
+
+	if urgent, _ := cmd.Flags().GetBool("urgent"); urgent {
+		builder.Urgent()
+	} else if notUrgent, _ := cmd.Flags().GetBool("not-urgent"); notUrgent {
+		builder.NotUrgent()
+	}
+}
+
+func applyUpdateSchedule(cmd *cobra.Command, builder *lunatask.TaskUpdateBuilder) error {
+	schedule, _ := cmd.Flags().GetString("schedule")
+	if schedule == "" {
+		return nil
+	}
+
+	date, err := dateutil.Parse(schedule)
+	if err != nil {
+		return err
+	}
+
+	builder.ScheduledOn(date)
+
+	return nil
 }