diff options
| -rw-r--r-- | .tasks/tasks-test.jsonl | 8 | ||||
| -rw-r--r-- | .tasks/tasks.jsonl | 2 | ||||
| -rw-r--r-- | Omni/Task.hs | 20 | ||||
| -rw-r--r-- | Omni/Task/Core.hs | 91 |
4 files changed, 112 insertions, 9 deletions
diff --git a/.tasks/tasks-test.jsonl b/.tasks/tasks-test.jsonl deleted file mode 100644 index 0b6b854..0000000 --- a/.tasks/tasks-test.jsonl +++ /dev/null @@ -1,8 +0,0 @@ -{"taskCreatedAt":"2025-11-14T04:30:59.204565135Z","taskDependencies":[],"taskId":"t-hKlXQS","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Test task","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T04:30:59.204565135Z"} -{"taskCreatedAt":"2025-11-14T04:30:59.217052707Z","taskDependencies":[],"taskId":"t-hKm16i","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Test task for list","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T04:30:59.217052707Z"} -{"taskCreatedAt":"2025-11-14T04:30:59.218106749Z","taskDependencies":[],"taskId":"t-hKm1nj","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"First task","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T04:30:59.218106749Z"} -{"taskCreatedAt":"2025-11-14T04:30:59.218343902Z","taskDependencies":[{"depId":"t-hKm1nj","depType":"Blocks"}],"taskId":"t-hKm1r8","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Blocked task","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T04:30:59.218343902Z"} -{"taskCreatedAt":"2025-11-14T04:30:59.219138111Z","taskDependencies":[],"taskId":"t-hKm1DW","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Original task","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T04:30:59.219138111Z"} -{"taskCreatedAt":"2025-11-14T04:30:59.219366383Z","taskDependencies":[{"depId":"t-hKm1DW","depType":"DiscoveredFrom"}],"taskId":"t-hKm1HD","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Discovered work","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T04:30:59.219366383Z"} -{"taskCreatedAt":"2025-11-14T04:30:59.220105422Z","taskDependencies":[],"taskId":"t-hKm1Ty","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Task A","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T04:30:59.220105422Z"} -{"taskCreatedAt":"2025-11-14T04:30:59.220248713Z","taskDependencies":[{"depId":"t-hKm1Ty","depType":"Related"}],"taskId":"t-hKm1VR","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Task B","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T04:30:59.220248713Z"} diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index 10715dd..4dfc413 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -15,7 +15,7 @@ {"taskCreatedAt":"2025-11-09T13:05:06.746734115Z","taskDependencies":[],"taskId":"t-PpZ6JC","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Add child_counters storage","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.746734115Z"} {"taskCreatedAt":"2025-11-09T13:05:06.774903465Z","taskDependencies":[],"taskId":"t-PpZe3X","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Update createTask to auto-generate child IDs","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.774903465Z"} {"taskCreatedAt":"2025-11-09T13:05:06.802295008Z","taskDependencies":[],"taskId":"t-PpZlbL","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement task tree visualization command","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:47:12.411364105Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.829842253Z","taskDependencies":[],"taskId":"t-PpZsm4","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Implement task stats command","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.829842253Z"} +{"taskCreatedAt":"2025-11-09T13:05:06.829842253Z","taskDependencies":[],"taskId":"t-PpZsm4","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement task stats command","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T19:05:37.772094417Z"} {"taskCreatedAt":"2025-11-09T13:05:06.85771202Z","taskDependencies":[],"taskId":"t-PpZzBA","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Implement epic progress tracking","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.85771202Z"} {"taskCreatedAt":"2025-11-09T13:05:06.88583862Z","taskDependencies":[],"taskId":"t-PpZGVf","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add filtering by type and parent (list improvements)","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:17:51.373969453Z"} {"taskCreatedAt":"2025-11-09T13:05:18.344932105Z","taskDependencies":[],"taskId":"t-PqLLXk","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement epic and task types","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:18.406381682Z"} diff --git a/Omni/Task.hs b/Omni/Task.hs index 6d2da71..bbb78bb 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -45,6 +45,7 @@ Usage: task update <id> <status> [--json] task deps <id> [--json] task tree [<id>] [--json] + task stats [--json] task export [--flush] task import -i <file> task sync @@ -60,6 +61,7 @@ Commands: update Update task status deps Show dependency tree tree Show task tree (epics with children, or all epics if no ID given) + stats Show task statistics export Export and consolidate tasks to JSONL import Import tasks from JSONL file sync Export and commit tasks to git (does NOT push) @@ -227,6 +229,12 @@ move args tree <- getTaskTree maybeId outputJson tree else showTaskTree maybeId + | args `Cli.has` Cli.command "stats" = do + if isJsonMode args + then do + stats <- getTaskStats + outputJson stats + else showTaskStats | args `Cli.has` Cli.command "export" = do exportTasks putText "Exported and consolidated tasks to .tasks/tasks.jsonl" @@ -454,6 +462,18 @@ cliTests = case result of Left err -> Test.assertFailure <| "Failed to parse 'sync': " <> show err Right args -> args `Cli.has` Cli.command "sync" Test.@?= True, + Test.unit "stats command" <| do + let result = Docopt.parseArgs help ["stats"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'stats': " <> show err + Right args -> args `Cli.has` Cli.command "stats" Test.@?= True, + Test.unit "stats with --json flag" <| do + let result = Docopt.parseArgs help ["stats", "--json"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'stats --json': " <> show err + Right args -> do + args `Cli.has` Cli.command "stats" Test.@?= True + args `Cli.has` Cli.longOption "json" Test.@?= True, Test.unit "create with flags in different order" <| do let result = Docopt.parseArgs help ["create", "Test", "--json", "--priority=1", "--namespace=Omni/Task"] case result of diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index f463040..e9da38e 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -437,6 +437,97 @@ exportTasks = do TIO.writeFile tasksFile "" traverse_ saveTask tasks +-- Task statistics +data TaskStats = TaskStats + { totalTasks :: Int, + openTasks :: Int, + inProgressTasks :: Int, + doneTasks :: Int, + totalEpics :: Int, + readyTasks :: Int, + blockedTasks :: Int, + tasksByPriority :: [(Priority, Int)], + tasksByNamespace :: [(Text, Int)] + } + deriving (Show, Eq, Generic) + +instance ToJSON TaskStats + +instance FromJSON TaskStats + +-- Get task statistics +getTaskStats :: IO TaskStats +getTaskStats = do + tasks <- loadTasks + ready <- getReadyTasks + let total = length tasks + open = length <| filter (\t -> taskStatus t == Open) tasks + inProg = length <| filter (\t -> taskStatus t == InProgress) tasks + done = length <| filter (\t -> taskStatus t == Done) tasks + epics = length <| filter (\t -> taskType t == Epic) tasks + readyCount = length ready + blockedCount = total - readyCount - done + -- Count tasks by priority + byPriority = + [ (P0, length <| filter (\t -> taskPriority t == P0) tasks), + (P1, length <| filter (\t -> taskPriority t == P1) tasks), + (P2, length <| filter (\t -> taskPriority t == P2) tasks), + (P3, length <| filter (\t -> taskPriority t == P3) tasks), + (P4, length <| filter (\t -> taskPriority t == P4) tasks) + ] + -- Count tasks by namespace + namespaces = mapMaybe taskNamespace tasks + uniqueNs = List.nub namespaces + byNamespace = map (\ns -> (ns, length <| filter (\t -> taskNamespace t == Just ns) tasks)) uniqueNs + pure + TaskStats + { totalTasks = total, + openTasks = open, + inProgressTasks = inProg, + doneTasks = done, + totalEpics = epics, + readyTasks = readyCount, + blockedTasks = blockedCount, + tasksByPriority = byPriority, + tasksByNamespace = byNamespace + } + +-- Show task statistics (human-readable) +showTaskStats :: IO () +showTaskStats = do + stats <- getTaskStats + putText "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + putText "Task Statistics" + putText "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + putText <| "Total tasks: " <> T.pack (show (totalTasks stats)) + putText <| " Open: " <> T.pack (show (openTasks stats)) + putText <| " In Progress: " <> T.pack (show (inProgressTasks stats)) + putText <| " Done: " <> T.pack (show (doneTasks stats)) + putText "" + putText <| "Epics: " <> T.pack (show (totalEpics stats)) + putText "" + putText <| "Ready to work: " <> T.pack (show (readyTasks stats)) + putText <| "Blocked: " <> T.pack (show (blockedTasks stats)) + putText "" + putText "By Priority:" + traverse_ printPriority (tasksByPriority stats) + unless (null (tasksByNamespace stats)) <| do + putText "" + putText "By Namespace:" + traverse_ printNamespace (tasksByNamespace stats) + putText "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + where + printPriority (p, count) = + let label = case p of + P0 -> "P0 (Critical)" + P1 -> "P1 (High)" + P2 -> "P2 (Medium)" + P3 -> "P3 (Low)" + P4 -> "P4 (Backlog)" + in putText <| " " <> T.pack (show count) <> " " <> label + printNamespace (ns, count) = + putText <| " " <> T.pack (show count) <> " " <> ns + -- Import tasks: Read from another JSONL file and merge with existing tasks importTasks :: FilePath -> IO () importTasks filePath = do |
