feat(area): add workflow support for LLM guidance

Amolith created

Areas now store their Lunatask workflow type (kanban, now_later, etc.).
The init wizard prompts for workflow selection with descriptions. MCP
area resource exposes workflow metadata including valid statuses, and
which fields (motivation, eisenhower, scheduling, priority) are relevant
for each workflow.

Assisted-by: Claude Sonnet 4 via Crush

Change summary

cmd/init/areas.go                       | 31 ++++++++++++++++++
cmd/mcp/server.go                       |  9 ++--
go.mod                                  |  2 
go.sum                                  |  4 +-
internal/completion/completion.go       | 13 +++++++
internal/config/config.go               | 10 +++--
internal/mcp/resources/areas/handler.go | 46 +++++++++++++++++++++-----
internal/mcp/shared/types.go            | 11 ++++--
8 files changed, 101 insertions(+), 25 deletions(-)

Detailed changes

cmd/init/areas.go 🔗

@@ -8,6 +8,7 @@ import (
 	"errors"
 	"fmt"
 
+	"git.secluded.site/go-lunatask"
 	"github.com/charmbracelet/huh"
 
 	"git.secluded.site/lune/internal/config"
@@ -137,6 +138,8 @@ func editArea(existing *config.Area, cfg *config.Config) (*config.Area, error) {
 	area := config.Area{}
 	if existing != nil {
 		area = *existing
+	} else {
+		area.Workflow = lunatask.WorkflowNowLater
 	}
 
 	err := runItemForm(&area.Name, &area.Key, &area.ID, itemFormConfig{
@@ -150,9 +153,37 @@ func editArea(existing *config.Area, cfg *config.Config) (*config.Area, error) {
 		return nil, err
 	}
 
+	if err := selectWorkflow(&area.Workflow); err != nil {
+		return nil, err
+	}
+
 	return &area, nil
 }
 
+func selectWorkflow(selected *lunatask.Workflow) error {
+	options := make([]huh.Option[lunatask.Workflow], 0, len(lunatask.Workflows()))
+	for _, w := range lunatask.Workflows() {
+		label := fmt.Sprintf("%s - %s", w, w.Description())
+		options = append(options, huh.NewOption(label, w))
+	}
+
+	err := huh.NewSelect[lunatask.Workflow]().
+		Title("Workflow").
+		Description("Determines which task fields are relevant for this area").
+		Options(options...).
+		Value(selected).
+		Run()
+	if err != nil {
+		if errors.Is(err, huh.ErrUserAborted) {
+			return errQuit
+		}
+
+		return err
+	}
+
+	return nil
+}
+
 func validateAreaKey(cfg *config.Config, existing *config.Area) func(string) error {
 	return func(input string) error {
 		if err := validateKeyFormat(input); err != nil {

cmd/mcp/server.go 🔗

@@ -56,10 +56,11 @@ func toAreaProviders(cfgAreas []config.Area) []shared.AreaProvider {
 
 	for _, area := range cfgAreas {
 		providers = append(providers, shared.AreaProvider{
-			ID:    area.ID,
-			Name:  area.Name,
-			Key:   area.Key,
-			Goals: shared.ToGoalProviders(area.Goals),
+			ID:       area.ID,
+			Name:     area.Name,
+			Key:      area.Key,
+			Workflow: area.Workflow,
+			Goals:    shared.ToGoalProviders(area.Goals),
 		})
 	}
 

go.mod 🔗

@@ -7,7 +7,7 @@ module git.secluded.site/lune
 go 1.25.5
 
 require (
-	git.secluded.site/go-lunatask v0.1.0-rc9.1
+	git.secluded.site/go-lunatask v0.1.0-rc9.2
 	github.com/BurntSushi/toml v1.6.0
 	github.com/charmbracelet/fang v0.4.4
 	github.com/charmbracelet/huh v0.8.0

go.sum 🔗

@@ -2,8 +2,8 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXy
 al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
-git.secluded.site/go-lunatask v0.1.0-rc9.1 h1:6dJcP3P+2QraPQ/wfPjCWaXv2mr1B4lMvBuQCNZd1t8=
-git.secluded.site/go-lunatask v0.1.0-rc9.1/go.mod h1:rxps7BBqF+BkY8VN5E7J9zSOzSbtZ1hDmLEOHxjTHZQ=
+git.secluded.site/go-lunatask v0.1.0-rc9.2 h1:fk5fCGdHmKpwz5HPy/n/LURBNweJoRN2xSay36VgA7g=
+git.secluded.site/go-lunatask v0.1.0-rc9.2/go.mod h1:rxps7BBqF+BkY8VN5E7J9zSOzSbtZ1hDmLEOHxjTHZQ=
 github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
 github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
 github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=

internal/completion/completion.go 🔗

@@ -6,6 +6,7 @@
 package completion
 
 import (
+	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/config"
 	"github.com/spf13/cobra"
 )
@@ -90,3 +91,15 @@ func Relationships(_ *cobra.Command, _ []string, _ string) ([]string, cobra.Shel
 		"almost-strangers",
 	}, cobra.ShellCompDirectiveNoFileComp
 }
+
+// Workflows returns workflow options for shell completion.
+func Workflows(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+	workflows := lunatask.Workflows()
+	keys := make([]string, len(workflows))
+
+	for i, w := range workflows {
+		keys[i] = string(w)
+	}
+
+	return keys, cobra.ShellCompDirectiveNoFileComp
+}

internal/config/config.go 🔗

@@ -11,6 +11,7 @@ import (
 	"os"
 	"path/filepath"
 
+	"git.secluded.site/go-lunatask"
 	"github.com/BurntSushi/toml"
 )
 
@@ -130,10 +131,11 @@ type Defaults struct {
 //
 //nolint:recvcheck // Value receivers for Keyed interface; pointer receiver for GoalByKey is intentional.
 type Area struct {
-	ID    string `json:"id"    toml:"id"`
-	Name  string `json:"name"  toml:"name"`
-	Key   string `json:"key"   toml:"key"`
-	Goals []Goal `json:"goals" toml:"goals"`
+	ID       string            `json:"id"       toml:"id"`
+	Name     string            `json:"name"     toml:"name"`
+	Key      string            `json:"key"      toml:"key"`
+	Workflow lunatask.Workflow `json:"workflow" toml:"workflow"`
+	Goals    []Goal            `json:"goals"    toml:"goals"`
 }
 
 // Goal represents a goal within an area.

internal/mcp/resources/areas/handler.go 🔗

@@ -24,9 +24,16 @@ Each area represents a life domain (e.g., Work, Personal, Health) and contains:
 - id: UUID to use when creating tasks in this area
 - name: Human-readable area name
 - key: Short alias for CLI usage
-- goals: List of goals within the area, each with id, name, and key
+- workflow: Task management style (determines which fields are relevant)
+- workflow_description: Human-readable explanation of the workflow
+- valid_statuses: Task statuses valid for this workflow
+- uses_motivation: Whether motivation field (must/should/want) is relevant
+- uses_eisenhower: Whether eisenhower matrix is relevant
+- uses_scheduling: Whether date scheduling is relevant
+- uses_priority: Whether priority field is relevant
+- goals: List of goals within the area
 
-Use this resource to discover valid area and goal IDs before creating or updating tasks.`
+Use workflow information to determine which task fields to set for each area.`
 
 // Handler handles area resource requests.
 type Handler struct {
@@ -40,10 +47,17 @@ func NewHandler(areas []shared.AreaProvider) *Handler {
 
 // areaInfo represents an area in the resource response.
 type areaInfo struct {
-	ID    string     `json:"id"`
-	Name  string     `json:"name"`
-	Key   string     `json:"key"`
-	Goals []goalInfo `json:"goals"`
+	ID                  string     `json:"id"`
+	Name                string     `json:"name"`
+	Key                 string     `json:"key"`
+	Workflow            string     `json:"workflow"`
+	WorkflowDescription string     `json:"workflow_description"`
+	ValidStatuses       []string   `json:"valid_statuses"`
+	UsesMotivation      bool       `json:"uses_motivation"`
+	UsesEisenhower      bool       `json:"uses_eisenhower"`
+	UsesScheduling      bool       `json:"uses_scheduling"`
+	UsesPriority        bool       `json:"uses_priority"`
+	Goals               []goalInfo `json:"goals"`
 }
 
 // goalInfo represents a goal in the resource response.
@@ -70,11 +84,23 @@ func (h *Handler) HandleRead(
 			})
 		}
 
+		validStatuses := make([]string, 0, len(area.Workflow.ValidStatuses()))
+		for _, s := range area.Workflow.ValidStatuses() {
+			validStatuses = append(validStatuses, string(s))
+		}
+
 		areasInfo = append(areasInfo, areaInfo{
-			ID:    area.ID,
-			Name:  area.Name,
-			Key:   area.Key,
-			Goals: goals,
+			ID:                  area.ID,
+			Name:                area.Name,
+			Key:                 area.Key,
+			Workflow:            string(area.Workflow),
+			WorkflowDescription: area.Workflow.Description(),
+			ValidStatuses:       validStatuses,
+			UsesMotivation:      area.Workflow.UsesMotivation(),
+			UsesEisenhower:      area.Workflow.UsesEisenhower(),
+			UsesScheduling:      area.Workflow.UsesScheduling(),
+			UsesPriority:        area.Workflow.UsesPriority(),
+			Goals:               goals,
 		})
 	}
 

internal/mcp/shared/types.go 🔗

@@ -5,6 +5,8 @@
 // Package shared provides common types and utilities for MCP resources and tools.
 package shared
 
+import "git.secluded.site/go-lunatask"
+
 // Keyed is an interface for types that have ID, Name, and Key fields.
 type Keyed interface {
 	GetID() string
@@ -14,10 +16,11 @@ type Keyed interface {
 
 // AreaProvider represents an area with its goals for MCP resources.
 type AreaProvider struct {
-	ID    string
-	Name  string
-	Key   string
-	Goals []GoalProvider
+	ID       string
+	Name     string
+	Key      string
+	Workflow lunatask.Workflow
+	Goals    []GoalProvider
 }
 
 // GoalProvider represents a goal within an area.