summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2025-11-20 14:09:57 -0500
committerBen Sima <ben@bsima.me>2025-11-20 14:09:57 -0500
commiteab575ad7ce423f053c87c45225853dd51aa252f (patch)
tree55cc492c568ac27e3d2c7bdf869fa3e33cadc991
parenta5faf8c31f619142e6f43d688f52d87c4edff341 (diff)
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 <amp@ampcode.com>
-rw-r--r--.tasks/tasks-test.jsonl8
-rw-r--r--.tasks/tasks.jsonl2
-rw-r--r--Omni/Task.hs20
-rw-r--r--Omni/Task/Core.hs91
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