From 372fae128e149a8234efdaa2c33704a4a803d225 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 24 Dec 2025 09:18:46 -0700 Subject: [PATCH] feat(area): add workflow support for LLM guidance 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 --- 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(-) diff --git a/cmd/init/areas.go b/cmd/init/areas.go index d6e034eb664e5339fd93622550db13de36911ce9..15cde476e239d4dce4b58fa1e004a9ff7c912a53 100644 --- a/cmd/init/areas.go +++ b/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 { diff --git a/cmd/mcp/server.go b/cmd/mcp/server.go index e860c8a91d7bc569ae376b2d757d51d6f16f918a..3f6d96aaa8fe8e3a558d4b5cabc954e7fbbbf2be 100644 --- a/cmd/mcp/server.go +++ b/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), }) } diff --git a/go.mod b/go.mod index d01b0c01f77ccacec6cc39b3e99a6b67d11b9045..41f7a2ca92cf80800943af74e7093ebd98f041dc 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 0d5d2d714b6f3353fb7f4b9ada9604c2053e432b..b647a66f962867b3391f7606dc0cc83156bd7f14 100644 --- a/go.sum +++ b/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= diff --git a/internal/completion/completion.go b/internal/completion/completion.go index 9bab7ba7bd07121e33e0c7448e04b00fb94e12ee..99938cb5e3d06f052822abb546682b262ed22947 100644 --- a/internal/completion/completion.go +++ b/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 +} diff --git a/internal/config/config.go b/internal/config/config.go index 88c7be3b5182333cc93d8c839e8d5bc3f06b77d7..e5b636bc64b637924d1075347f86fe311c0f8b0f 100644 --- a/internal/config/config.go +++ b/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. diff --git a/internal/mcp/resources/areas/handler.go b/internal/mcp/resources/areas/handler.go index 0d09bdf6dec4025a0c90d8ba356d9c4948db6f0a..9017302e4d5d7ccb65740077ff96c376ea45be6d 100644 --- a/internal/mcp/resources/areas/handler.go +++ b/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, }) } diff --git a/internal/mcp/shared/types.go b/internal/mcp/shared/types.go index d509d09f44365bcd60d78701e9e704e7752036b4..ee8c12d6424548ec96768ad80c16ba7677a74be1 100644 --- a/internal/mcp/shared/types.go +++ b/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.