summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.tasks/tasks.jsonl2
-rw-r--r--Omni/Task.hs17
-rw-r--r--Omni/Task/Core.hs45
3 files changed, 48 insertions, 16 deletions
diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl
index a411197..e593e4c 100644
--- a/.tasks/tasks.jsonl
+++ b/.tasks/tasks.jsonl
@@ -153,8 +153,8 @@
{"taskCreatedAt":"2025-11-21T03:12:57.890285833Z","taskDependencies":[],"taskId":"t-rWa7IYOrq","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Parent Epic","taskType":"Epic","taskUpdatedAt":"2025-11-21T03:12:57.890285833Z"}
{"taskCreatedAt":"2025-11-21T03:13:01.031231982Z","taskDependencies":[],"taskId":"t-rWa7IYOrq.1","taskNamespace":null,"taskParent":"t-rWa7IYOrq","taskPriority":"P2","taskStatus":"Review","taskTitle":"Child Task","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:09:24.025629665Z"}
{"taskCreatedAt":"2025-11-21T04:09:41.699239296Z","taskDependencies":[],"taskId":"t-rWabrkQDQ","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix task ready to exclude Review tasks","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T04:10:49.102675623Z"}
-{"taskCreatedAt":"2025-11-21T04:30:05.792313193Z","taskDependencies":[],"taskId":"t-rWacMb1av","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Review","taskTitle":"Make task IDs case-insensitive","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:18:06.371310379Z"}
{"taskCreatedAt":"2025-11-13T19:38:07.804316976Z","taskDependencies":[],"taskId":"t-1f9QP23","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"General Code Quality Refactor","taskType":"Epic","taskUpdatedAt":"2025-11-13T19:38:07.804316976Z"}
{"taskCreatedAt":"2025-11-20T21:41:20.029426381Z","taskDependencies":[],"taskId":"t-1ne7Qtj","taskNamespace":"Network/Wai/Middleware/Braid.hs","taskParent":"t-1f9QP23","taskPriority":"P2","taskStatus":"Review","taskTitle":"Implement Braid keep-alive mechanism","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:23:00.872672096Z"}
{"taskCreatedAt":"2025-11-20T21:41:20.048368004Z","taskDependencies":[],"taskId":"t-1ne7VoO","taskNamespace":"Biz/Que/Host.hs","taskParent":"t-1f9QP23","taskPriority":"P2","taskStatus":"Review","taskTitle":"Revive authkey authentication in Que/Host","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:35:29.184786905Z"}
{"taskCreatedAt":"2025-11-20T21:41:20.067644599Z","taskDependencies":[],"taskId":"t-1ne80pJ","taskNamespace":"Biz/Dragons.hs","taskParent":"t-1f9QP23","taskPriority":"P2","taskStatus":"Done","taskTitle":"Store generated JWK in persistent file","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:54:17.655700806Z"}
+{"taskCreatedAt":"2025-11-21T04:30:05.792313193Z","taskDependencies":[],"taskId":"t-rWacMb1av","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Review","taskTitle":"Make task IDs case-insensitive","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:18:06.371310379Z"}
diff --git a/Omni/Task.hs b/Omni/Task.hs
index 19fc005..f3baf15 100644
--- a/Omni/Task.hs
+++ b/Omni/Task.hs
@@ -46,7 +46,7 @@ Usage:
task deps <id> [--json]
task tree [<id>] [--json]
task progress <id> [--json]
- task stats [--json]
+ task stats [--epic=<id>] [--json]
task export [--flush]
task import -i <file>
task sync
@@ -75,6 +75,7 @@ Options:
--parent=<id> Parent epic ID
--priority=<p> Priority: 0-4 (0=critical, 4=backlog, default: 2)
--status=<status> Filter by status: open, in-progress, review, done
+ --epic=<id> Filter stats by epic (recursive)
--deps=<ids> Comma-separated list of dependency IDs
--dep-type=<type> Dependency type: blocks, discovered-from, parent-child, related (default: blocks)
--discovered-from=<id> Shortcut for --deps=<id> --dep-type=discovered-from
@@ -241,11 +242,14 @@ move args
outputJson progress
else showTaskProgress tid
| args `Cli.has` Cli.command "stats" = do
+ maybeEpic <- case Cli.getArg args (Cli.longOption "epic") of
+ Nothing -> pure Nothing
+ Just e -> pure <| Just (T.pack e)
if isJsonMode args
then do
- stats <- getTaskStats
+ stats <- getTaskStats maybeEpic
outputJson stats
- else showTaskStats
+ else showTaskStats maybeEpic
| args `Cli.has` Cli.command "export" = do
exportTasks
putText "Exported and consolidated tasks to .tasks/tasks.jsonl"
@@ -526,6 +530,13 @@ cliTests =
Right args -> do
args `Cli.has` Cli.command "stats" Test.@?= True
args `Cli.has` Cli.longOption "json" Test.@?= True,
+ Test.unit "stats with --epic flag" <| do
+ let result = Docopt.parseArgs help ["stats", "--epic=t-abc123"]
+ case result of
+ Left err -> Test.assertFailure <| "Failed to parse 'stats --epic': " <> show err
+ Right args -> do
+ args `Cli.has` Cli.command "stats" Test.@?= True
+ Cli.getArg args (Cli.longOption "epic") Test.@?= Just "t-abc123",
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 17a5ecf..622e8de 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -556,18 +556,31 @@ instance ToJSON TaskStats
instance FromJSON TaskStats
-- Get task statistics
-getTaskStats :: IO TaskStats
-getTaskStats = do
- tasks <- loadTasks
- ready <- getReadyTasks
- let total = length tasks
+getTaskStats :: Maybe Text -> IO TaskStats
+getTaskStats maybeEpicId = do
+ allTasks <- loadTasks
+
+ targetTasks <- case maybeEpicId of
+ Nothing -> pure allTasks
+ Just epicId ->
+ case filter (\t -> taskId t == epicId) allTasks of
+ [] -> panic "Epic not found"
+ _ -> pure <| getAllDescendants allTasks epicId
+
+ globalReady <- getReadyTasks
+ let readyIds = map taskId globalReady
+ -- Filter ready tasks to only include those in our target set
+ readyCount = length <| filter (\t -> taskId t `elem` readyIds) targetTasks
+
+ tasks = targetTasks
+ total = length tasks
open = length <| filter (\t -> taskStatus t == Open) tasks
inProg = length <| filter (\t -> taskStatus t == InProgress) tasks
review = length <| filter (\t -> taskStatus t == Review) tasks
done = length <| filter (\t -> taskStatus t == Done) tasks
epics = length <| filter (\t -> taskType t == Epic) tasks
- readyCount = length ready
- blockedCount = total - readyCount - done
+ readyCount' = readyCount
+ blockedCount = total - readyCount' - done
-- Count tasks by priority
byPriority =
[ (P0, length <| filter (\t -> taskPriority t == P0) tasks),
@@ -588,18 +601,26 @@ getTaskStats = do
reviewTasks = review,
doneTasks = done,
totalEpics = epics,
- readyTasks = readyCount,
+ readyTasks = readyCount',
blockedTasks = blockedCount,
tasksByPriority = byPriority,
tasksByNamespace = byNamespace
}
+-- Helper to get all descendants of a task (recursive)
+getAllDescendants :: [Task] -> Text -> [Task]
+getAllDescendants allTasks parentId =
+ let children = filter (\t -> taskParent t == Just parentId) allTasks
+ in children ++ concatMap (getAllDescendants allTasks <. taskId) children
+
-- Show task statistics (human-readable)
-showTaskStats :: IO ()
-showTaskStats = do
- stats <- getTaskStats
+showTaskStats :: Maybe Text -> IO ()
+showTaskStats maybeEpicId = do
+ stats <- getTaskStats maybeEpicId
putText ""
- putText "Task Statistics"
+ case maybeEpicId of
+ Nothing -> putText "Task Statistics"
+ Just epicId -> putText <| "Task Statistics for Epic " <> epicId
putText ""
putText <| "Total tasks: " <> T.pack (show (totalTasks stats))
putText <| " Open: " <> T.pack (show (openTasks stats))