From eab575ad7ce423f053c87c45225853dd51aa252f Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 14:09:57 -0500 Subject: task: implement stats command - Add 'task stats' command to show task statistics - Display total tasks, status breakdown (open/in-progress/done) - Show epic count, ready vs blocked tasks - Show task counts by priority (P0-P4) and namespace - Support both human-readable and JSON output (--json flag) - Add tests for stats command and stats --json - TaskStats data type with ToJSON/FromJSON instances All 31 tests passing. Amp-Thread-ID: https://ampcode.com/threads/T-4e6225cf-3e78-4538-963c-5377bbbccee8 Co-authored-by: Amp --- Omni/Task.hs | 20 ++++++++++++ Omni/Task/Core.hs | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) (limited to 'Omni') 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 [--json] task deps [--json] task tree [] [--json] + task stats [--json] task export [--flush] task import -i 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 -- cgit v1.2.3