← Back to task

Commit c3f48a93

commit c3f48a938af0b9a2f692fab2168cc549d629511c
Author: Ben Sima <ben@bensima.com>
Date:   Thu Nov 27 22:42:20 2025

    Add Draft status for tasks not ready for jr to pickup
    
    All changes are in place. The build and tests pass. The implementation
    i
    
    1. **Added `Draft` to Status enum** - First in sequence: `Draft |
    Open | 2. **Excluded Draft from getReadyTasks** - Uses `taskStatus
    t `elem` [Op 3. **CLI support** - `task update <id> draft` works,
    `task list --status 4. **Display coloring** - Draft shows in gray
    with `[.]` indicator in tr 5. **TaskStats** - Added `draftTasks`
    field and display in stats output 6. **Tests** - Added unit test for
    draft exclusion from ready tasks and
    
    Task-Id: t-166

diff --git a/Omni/Task.hs b/Omni/Task.hs
index 663cc023..c078a3e2 100644
--- a/Omni/Task.hs
+++ b/Omni/Task.hs
@@ -81,7 +81,7 @@ Options:
   --type=<type>                 Task type: epic, task, or human (default: task)
   --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, approved, done
+  --status=<status>             Filter by status: draft, open, in-progress, review, approved, 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
@@ -99,7 +99,7 @@ Options:
 Arguments:
   <title>                       Task title
   <id>                          Task ID
-  <status>                      Task status (open, in-progress, review, approved, done)
+  <status>                      Task status (draft, open, in-progress, review, approved, done)
   <file>                        JSONL file to import
 |]
 
@@ -208,11 +208,12 @@ move' args
         Just other -> panic <| "Invalid priority: " <> T.pack other <> ". Use: 0-4"
       maybeStatus <- case Cli.getArg args (Cli.longOption "status") of
         Nothing -> pure Nothing
+        Just "draft" -> pure <| Just Draft
         Just "open" -> pure <| Just Open
         Just "in-progress" -> pure <| Just InProgress
         Just "review" -> pure <| Just Review
         Just "done" -> pure <| Just Done
-        Just other -> panic <| "Invalid status: " <> T.pack other <> ". Use: open, in-progress, review, or done"
+        Just other -> panic <| "Invalid status: " <> T.pack other <> ". Use: draft, open, in-progress, review, or done"
       maybeNamespace <- case Cli.getArg args (Cli.longOption "namespace") of
         Nothing -> pure Nothing
         Just ns -> do
@@ -270,12 +271,13 @@ move' args
         Just p -> pure <| Just (T.pack p)
       maybeStatus <- case Cli.getArg args (Cli.longOption "status") of
         Nothing -> pure Nothing
+        Just "draft" -> pure <| Just Draft
         Just "open" -> pure <| Just Open
         Just "in-progress" -> pure <| Just InProgress
         Just "review" -> pure <| Just Review
         Just "approved" -> pure <| Just Approved
         Just "done" -> pure <| Just Done
-        Just other -> panic <| "Invalid status: " <> T.pack other <> ". Use: open, in-progress, review, approved, or done"
+        Just other -> panic <| "Invalid status: " <> T.pack other <> ". Use: draft, open, in-progress, review, approved, or done"
       maybeNamespace <- case Cli.getArg args (Cli.longOption "namespace") of
         Nothing -> pure Nothing
         Just ns -> do
@@ -323,12 +325,13 @@ move' args
         pure (map (\d -> Dependency {depId = d, depType = dtype}) ids)
 
       let newStatus = case statusStr of
+            "draft" -> Draft
             "open" -> Open
             "in-progress" -> InProgress
             "review" -> Review
             "approved" -> Approved
             "done" -> Done
-            _ -> panic "Invalid status. Use: open, in-progress, review, approved, or done"
+            _ -> panic "Invalid status. Use: draft, open, in-progress, review, approved, or done"
 
       -- Show verification checklist warning when marking Done without --verified
       when (newStatus == Done && not isVerified && not (isJsonMode args)) <| do
@@ -449,6 +452,11 @@ unitTests =
         task <- createTask "Human Task" HumanTask Nothing Nothing P2 [] Nothing
         ready <- getReadyTasks
         (taskId task `notElem` map taskId ready) Test.@?= True,
+      Test.unit "ready tasks exclude draft tasks" <| do
+        task <- createTask "Draft Task" WorkTask Nothing Nothing P2 [] Nothing
+        updateTaskStatus (taskId task) Draft []
+        ready <- getReadyTasks
+        (taskId task `notElem` map taskId ready) Test.@?= True,
       Test.unit "can create task with description" <| do
         task <- createTask "Test task" WorkTask Nothing Nothing P2 [] (Just "My description")
         taskDescription task Test.@?= Just "My description",
@@ -772,6 +780,13 @@ cliTests =
           Right args -> do
             args `Cli.has` Cli.command "list" Test.@?= True
             Cli.getArg args (Cli.longOption "status") Test.@?= Just "approved",
+      Test.unit "list with --status=draft filter" <| do
+        let result = Docopt.parseArgs help ["list", "--status=draft"]
+        case result of
+          Left err -> Test.assertFailure <| "Failed to parse 'list --status=draft': " <> show err
+          Right args -> do
+            args `Cli.has` Cli.command "list" Test.@?= True
+            Cli.getArg args (Cli.longOption "status") Test.@?= Just "draft",
       Test.unit "ready command" <| do
         let result = Docopt.parseArgs help ["ready"]
         case result of
@@ -800,6 +815,14 @@ cliTests =
             args `Cli.has` Cli.command "update" Test.@?= True
             Cli.getArg args (Cli.argument "id") Test.@?= Just "t-abc123"
             Cli.getArg args (Cli.argument "status") Test.@?= Just "approved",
+      Test.unit "update command with draft" <| do
+        let result = Docopt.parseArgs help ["update", "t-abc123", "draft"]
+        case result of
+          Left err -> Test.assertFailure <| "Failed to parse 'update ... draft': " <> show err
+          Right args -> do
+            args `Cli.has` Cli.command "update" Test.@?= True
+            Cli.getArg args (Cli.argument "id") Test.@?= Just "t-abc123"
+            Cli.getArg args (Cli.argument "status") Test.@?= Just "draft",
       Test.unit "update with --json flag" <| do
         let result = Docopt.parseArgs help ["update", "t-abc123", "done", "--json"]
         case result of
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index d15ed8e0..61a2e839 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -44,7 +44,7 @@ data Task = Task
 data TaskType = Epic | WorkTask | HumanTask
   deriving (Show, Eq, Read, Generic)
 
-data Status = Open | InProgress | Review | Approved | Done
+data Status = Draft | Open | InProgress | Review | Approved | Done
   deriving (Show, Eq, Read, Generic)
 
 -- Priority levels (matching beads convention)
@@ -692,7 +692,7 @@ getReadyTasks :: IO [Task]
 getReadyTasks = do
   allTasks <- loadTasks
   retryContexts <- getAllRetryContexts
-  let openTasks = filter (\t -> taskStatus t == Open || taskStatus t == InProgress) allTasks
+  let openTasks = filter (\t -> taskStatus t `elem` [Open, InProgress]) allTasks
       doneIds = map taskId <| filter (\t -> taskStatus t == Done) allTasks
 
       parentIds = mapMaybe taskParent allTasks
@@ -815,6 +815,7 @@ showTaskTree maybeId = do
                   completed = length <| filter (\t -> taskStatus t == Done) children
                in "[" <> T.pack (show completed) <> "/" <> T.pack (show total) <> "]"
             _ -> case taskStatus task of
+              Draft -> "[.]"
               Open -> "[ ]"
               InProgress -> "[~]"
               Review -> "[?]"
@@ -824,6 +825,7 @@ showTaskTree maybeId = do
           coloredStatusStr = case taskType task of
             Epic -> magenta statusStr
             _ -> case taskStatus task of
+              Draft -> gray statusStr
               Open -> bold statusStr
               InProgress -> yellow statusStr
               Review -> magenta statusStr
@@ -881,6 +883,7 @@ printTask t = do
       coloredStatus =
         let s = "[" <> T.pack (show (taskStatus t)) <> "]"
          in case taskStatus t of
+              Draft -> gray s
               Open -> bold s
               InProgress -> yellow s
               Review -> magenta s
@@ -987,6 +990,7 @@ saveTaskToJsonl path task = do
 
 data TaskStats = TaskStats
   { totalTasks :: Int,
+    draftTasks :: Int,
     openTasks :: Int,
     inProgressTasks :: Int,
     reviewTasks :: Int,
@@ -1021,6 +1025,7 @@ getTaskStats maybeEpicId = do
 
       tasks = targetTasks
       total = length tasks
+      draft = length <| filter (\t -> taskStatus t == Draft) 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
@@ -1028,7 +1033,7 @@ getTaskStats maybeEpicId = do
       done = length <| filter (\t -> taskStatus t == Done) tasks
       epics = length <| filter (\t -> taskType t == Epic) tasks
       readyCount' = readyCount
-      blockedCount = total - readyCount' - done
+      blockedCount = total - readyCount' - done - draft
       byPriority =
         [ (P0, length <| filter (\t -> taskPriority t == P0) tasks),
           (P1, length <| filter (\t -> taskPriority t == P1) tasks),
@@ -1042,6 +1047,7 @@ getTaskStats maybeEpicId = do
   pure
     TaskStats
       { totalTasks = total,
+        draftTasks = draft,
         openTasks = open,
         inProgressTasks = inProg,
         reviewTasks = review,
@@ -1068,6 +1074,7 @@ showTaskStats maybeEpicId = do
     Just epicId -> putText <| "Task Statistics for Epic " <> epicId
   putText ""
   putText <| "Total tasks:       " <> T.pack (show (totalTasks stats))
+  putText <| "  Draft:           " <> T.pack (show (draftTasks stats))
   putText <| "  Open:            " <> T.pack (show (openTasks stats))
   putText <| "  In Progress:     " <> T.pack (show (inProgressTasks stats))
   putText <| "  Review:          " <> T.pack (show (reviewTasks stats))