Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **ID Prefix Resolution**: `tw task show` now accepts unique ID prefixes (e.g., `task-abc` instead of full ID)
- Ambiguous prefixes display candidate IDs with clear error message
- Auto-prepends `task-` or `plan-` prefix when missing
- **Archived Plan Filtering**: `tw task list` now excludes archived plans by default
- New `--include-archived` flag to show tasks from archived plans
- JSON output includes `plan_status` field for each task
- **ID Utilities**: New `internal/util` package with ID helper functions
- `ShortID(id, n)` for consistent ID truncation
- `ResolveTaskID` and `ResolvePlanID` for prefix-based ID resolution
- Repository methods `FindTaskIDsByPrefix` and `FindPlanIDsByPrefix`

### Changed

- Standardized PlanID JSON field to `plan_id` (snake_case) across all types
- `planId` (camelCase) still accepted as deprecated alias with warning
- Affects Task model, MCP tool params (TaskToolParams, PlanToolParams, PolicyToolParams)
- Improved CLI ID display using consistent `util.ShortID` formatting
- Enhanced error messages with context wrapping (e.g., "failed to list tasks for plan X: ...")

### Fixed

- **Task Show ID Mismatch**: Fixed truncated 12-char display vs 13-char actual IDs causing "task not found" errors
- **Silent Error Suppression**: `tw task list` now properly propagates errors instead of silently returning empty results
- **Storage Layer**: Added `rows.Err()` checks to 24 database query functions to catch iteration errors
- **TaskStatus Formatting**: Complete coverage for all 8 status values with graceful "unknown" fallback

- **OpenCode Support**: Full integration with OpenCode AI assistant
- Bootstrap creates `opencode.json` at project root with MCP server configuration
- Commands directory `.opencode/commands/` with TaskWing slash commands (tw-next, tw-done, tw-brief, etc.)
Expand Down
7 changes: 3 additions & 4 deletions cmd/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/josephgoksu/TaskWing/internal/logger"
"github.com/josephgoksu/TaskWing/internal/task"
"github.com/josephgoksu/TaskWing/internal/ui"
"github.com/josephgoksu/TaskWing/internal/util"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/term"
Expand Down Expand Up @@ -656,10 +657,8 @@ func printStatus(plan *task.Plan) {
title = dimStyle.Render(title)
}

tid := t.ID
if len(tid) > 12 {
tid = tid[:12]
}
// Use ShortID for consistent task ID display
tid := util.ShortID(t.ID, util.TaskIDLength)
fmt.Printf(" %s %s %s\n", statusMarker, dimStyle.Render(tid), title)
}
fmt.Println()
Expand Down
93 changes: 71 additions & 22 deletions cmd/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/josephgoksu/TaskWing/internal/app"
"github.com/josephgoksu/TaskWing/internal/task"
"github.com/josephgoksu/TaskWing/internal/ui"
"github.com/josephgoksu/TaskWing/internal/util"
"github.com/spf13/cobra"
)

Expand All @@ -24,14 +25,18 @@ var taskListCmd = &cobra.Command{
Short: "List all tasks",
Long: `List all tasks, grouped by plan.

By default, tasks from archived plans are excluded. Use --include-archived to show them.

Filter options:
--plan Filter by plan ID (prefix match)
--status Filter by status (pending, in_progress, completed, failed)
--priority Filter by priority threshold (show tasks with priority <= value)
--scope Filter by scope/tag
--plan Filter by plan ID (prefix match)
--status Filter by status (pending, in_progress, completed, failed)
--priority Filter by priority threshold (show tasks with priority <= value)
--scope Filter by scope/tag
--include-archived Include tasks from archived plans

Examples:
taskwing task list # All tasks
taskwing task list # All tasks (excludes archived plans)
taskwing task list --include-archived # Include archived plan tasks
taskwing task list --status pending # Only pending tasks
taskwing task list --priority 50 # High priority tasks only
taskwing task list --scope api # Tasks in api scope`,
Expand All @@ -41,7 +46,7 @@ Examples:
func runTaskList(cmd *cobra.Command, args []string) error {
repo, err := openRepo()
if err != nil {
return err
return fmt.Errorf("failed to open repository: %w", err)
}
defer func() { _ = repo.Close() }()

Expand All @@ -50,10 +55,11 @@ func runTaskList(cmd *cobra.Command, args []string) error {
statusFilter, _ := cmd.Flags().GetString("status")
priorityFilter, _ := cmd.Flags().GetInt("priority")
scopeFilter, _ := cmd.Flags().GetString("scope")
includeArchived, _ := cmd.Flags().GetBool("include-archived")

plans, err := repo.ListPlans()
if err != nil {
return err
return fmt.Errorf("failed to list plans: %w", err)
}

if len(plans) == 0 {
Expand All @@ -66,17 +72,25 @@ func runTaskList(cmd *cobra.Command, args []string) error {

// Collect and filter tasks
type taskWithPlan struct {
Task task.Task
PlanID string
Goal string
Task task.Task
PlanID string
PlanStatus task.PlanStatus
Goal string
}
var allTasks []taskWithPlan

for _, p := range plans {
// Skip archived plans by default unless --include-archived is set
if !includeArchived && p.Status == task.PlanStatusArchived {
continue
}
if planFilter != "" && !strings.HasPrefix(p.ID, planFilter) {
continue
}
tasks, _ := repo.ListTasks(p.ID)
tasks, err := repo.ListTasks(p.ID)
if err != nil {
return fmt.Errorf("failed to list tasks for plan %s: %w", p.ID, err)
}
for _, t := range tasks {
// Apply filters
if statusFilter != "" && string(t.Status) != statusFilter {
Expand All @@ -98,7 +112,7 @@ func runTaskList(cmd *cobra.Command, args []string) error {
continue
}
}
allTasks = append(allTasks, taskWithPlan{Task: t, PlanID: p.ID, Goal: p.Goal})
allTasks = append(allTasks, taskWithPlan{Task: t, PlanID: p.ID, PlanStatus: p.Status, Goal: p.Goal})
}
}

Expand All @@ -107,6 +121,7 @@ func runTaskList(cmd *cobra.Command, args []string) error {
type taskJSON struct {
ID string `json:"id"`
PlanID string `json:"plan_id"`
PlanStatus string `json:"plan_status"`
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
Expand All @@ -124,6 +139,7 @@ func runTaskList(cmd *cobra.Command, args []string) error {
jsonTasks = append(jsonTasks, taskJSON{
ID: t.ID,
PlanID: tp.PlanID,
PlanStatus: string(tp.PlanStatus),
Title: t.Title,
Description: t.Description,
Status: string(t.Status),
Expand Down Expand Up @@ -196,11 +212,9 @@ func runTaskList(cmd *cobra.Command, args []string) error {
for _, tp := range tasks {
t := tp.Task

// ID - Cyan and bold
tid := t.ID
if len(tid) > 12 {
tid = tid[:12]
}
// ID - Cyan and bold, use ShortID for consistent display
// Full task IDs are 13 chars (task-xxxxxxxx), display fits in 14-char column
tid := util.ShortID(t.ID, util.TaskIDLength)
idStr := idStyle.Render(tid)

// Status - Color coded
Expand Down Expand Up @@ -237,18 +251,37 @@ func runTaskList(cmd *cobra.Command, args []string) error {
}

// formatTaskStatus returns a color-coded status string.
// Covers all known TaskStatus values with an "unknown" fallback for unexpected values.
func formatTaskStatus(status task.TaskStatus) string {
switch status {
case task.StatusCompleted, "done":
// Green - successfully completed
return lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render("[done] ")
case task.StatusInProgress:
// Orange/bold - actively working
return lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true).Render("[active] ")
case task.StatusFailed:
// Red - execution or verification failed
return lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render("[failed] ")
case task.StatusVerifying:
// Blue - running validation
return lipgloss.NewStyle().Foreground(lipgloss.Color("33")).Render("[verify] ")
default:
case task.StatusPending:
// Gray - ready to be picked up
return lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Render("[pending] ")
case task.StatusDraft:
// Dim gray - initial creation, not ready
return lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("[draft] ")
case task.StatusBlocked:
// Red/dim - waiting on dependencies
return lipgloss.NewStyle().Foreground(lipgloss.Color("124")).Render("[blocked] ")
case task.StatusReady:
// Green/dim - dependencies met, ready for execution
return lipgloss.NewStyle().Foreground(lipgloss.Color("28")).Render("[ready] ")
default:
// Unknown status - neutral gray with label showing the actual value
// This ensures we never panic on unexpected values
return lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Render("[unknown] ")
}
}

Expand All @@ -271,18 +304,33 @@ func formatPriority(priority int) string {
var taskShowCmd = &cobra.Command{
Use: "show [task-id]",
Short: "Show a task",
Args: cobra.ExactArgs(1),
Long: `Show details for a specific task.

Accepts full task IDs or unique prefixes. If the prefix is ambiguous,
candidate IDs will be displayed.

Examples:
taskwing task show task-abc12345 # Full ID
taskwing task show task-abc # Unique prefix
taskwing task show abc # Prefix without 'task-' (auto-prepended)`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
taskID := args[0]
idOrPrefix := args[0]
repo, err := openRepo()
if err != nil {
return err
return fmt.Errorf("failed to open repository: %w", err)
}
defer func() { _ = repo.Close() }()

// Resolve prefix to full task ID
taskID, err := util.ResolveTaskID(cmd.Context(), repo, idOrPrefix)
if err != nil {
return fmt.Errorf("failed to resolve task ID: %w", err)
}

t, err := repo.GetTask(taskID)
if err != nil {
return err
return fmt.Errorf("failed to get task %s: %w", taskID, err)
}

if isJSON() {
Expand Down Expand Up @@ -814,6 +862,7 @@ func init() {
taskListCmd.Flags().StringP("status", "s", "", "Filter by status (pending, in_progress, completed, failed)")
taskListCmd.Flags().IntP("priority", "P", 0, "Filter by max priority (show tasks with priority <= value)")
taskListCmd.Flags().String("scope", "", "Filter by scope/tag")
taskListCmd.Flags().Bool("include-archived", false, "Include tasks from archived plans")

taskUpdateCmd.Flags().String("status", "", "Update the task status (draft, pending, in_progress, verifying, completed, failed)")
taskDeleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
Expand Down
Loading
Loading