summaryrefslogtreecommitdiff
path: root/Omni
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2025-11-21 00:49:19 -0500
committerBen Sima <ben@bsima.me>2025-11-21 00:49:19 -0500
commit62728ac31f4a5a86678bf8830bf76b7ebf05436f (patch)
treeb06b431b0dbe8e858d339c6b0f9c1fbc86c85cd3 /Omni
parent5d8c049eadc14d0e782fededea4ac913a9dbbc78 (diff)
feat: implement t-1rcIwc8
Diffstat (limited to 'Omni')
-rw-r--r--Omni/Task.hs17
-rw-r--r--Omni/Task/Core.hs45
2 files changed, 47 insertions, 15 deletions
diff --git a/Omni/Task.hs b/Omni/Task.hs
index 24e528b..8151487 100644
--- a/Omni/Task.hs
+++ b/Omni/Task.hs
@@ -45,7 +45,7 @@ Usage:
task update <id> <status> [--json]
task deps <id> [--json]
task tree [<id>] [--json]
- task stats [--json]
+ task stats [--epic=<id>] [--json]
task export [--flush]
task import -i <file>
task sync
@@ -73,6 +73,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
@@ -232,11 +233,14 @@ move args
outputJson tree
else showTaskTree maybeId
| 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"
@@ -516,6 +520,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 e4f1086..2ff7302 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -518,18 +518,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),
@@ -550,18 +563,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 (\t -> getAllDescendants allTasks (taskId t)) 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))