summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2025-11-09 07:18:18 -0500
committerBen Sima <ben@bsima.me>2025-11-09 07:18:18 -0500
commit1d98b463ac79905b542d45d632e5135ea90af585 (patch)
tree6db13ea019d1e0d7f809ef059921e0a99d5b1ad8
parent36a31210ff5682f3387a28893563f0c5bf1a4cc5 (diff)
Implement epic and task types to replace project field
Major refactoring of task data model: - Added TaskType enum (Epic | WorkTask) - Replaced taskProject with taskType and taskParent fields - Epics are containers for tasks (hierarchical organization) - Tasks can have optional parent epics - Updated createTask signature to accept type and parent - Updated CLI: --type=epic|task and --parent=<id> options - Updated list command to filter by type and parent - Updated printTask to display type and parent info - Fixed naming collision (WorkTask instead of Task constructor) Example usage: task create "Auth System" --type=epic task create "Design API" --type=task --parent=t-abc123 task list --type=epic task list --parent=t-abc123 Completed task: t-8WR5Zg Amp-Thread-ID: https://ampcode.com/threads/T-85f4ee29-a529-4f59-ac6f-6ffec75b6a56 Co-authored-by: Amp <amp@ampcode.com>
-rw-r--r--.tasks/tasks.jsonl5
-rw-r--r--Omni/Task.hs42
-rw-r--r--Omni/Task/Core.hs45
3 files changed, 61 insertions, 31 deletions
diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl
index d43d37a..a58fee2 100644
--- a/.tasks/tasks.jsonl
+++ b/.tasks/tasks.jsonl
@@ -1,3 +1,2 @@
-{"taskCreatedAt":"2025-11-08T21:41:26.038817033Z","taskDependencies":[],"taskId":"t-1nex3MX","taskNamespace":null,"taskProject":"test-project","taskStatus":"Open","taskTitle":"Test task","taskUpdatedAt":"2025-11-08T21:41:26.038817033Z"}
-{"taskCreatedAt":"2025-11-08T21:41:26.048075145Z","taskDependencies":[],"taskId":"t-1nex6ch","taskNamespace":null,"taskProject":"test","taskStatus":"Open","taskTitle":"First task","taskUpdatedAt":"2025-11-08T21:41:26.048075145Z"}
-{"taskCreatedAt":"2025-11-08T21:41:26.057905215Z","taskDependencies":["t-1nex6ch"],"taskId":"t-1nex8KQ","taskNamespace":null,"taskProject":"test","taskStatus":"Open","taskTitle":"Blocked task","taskUpdatedAt":"2025-11-08T21:41:26.057905215Z"}
+{"taskCreatedAt":"2025-11-09T12:17:09.286039287Z","taskDependencies":[],"taskId":"t-MhfzLf","taskNamespace":null,"taskParent":null,"taskStatus":"Done","taskTitle":"Authentication System","taskType":"Epic","taskUpdatedAt":"2025-11-09T12:17:29.632200648Z"}
+{"taskCreatedAt":"2025-11-09T12:17:14.313519244Z","taskDependencies":[],"taskId":"t-MhAFDD","taskNamespace":null,"taskParent":"t-MhfzLf","taskStatus":"Done","taskTitle":"Design auth API","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:17:29.659843447Z"}
diff --git a/Omni/Task.hs b/Omni/Task.hs
index 2f3ec6f..153f899 100644
--- a/Omni/Task.hs
+++ b/Omni/Task.hs
@@ -33,8 +33,8 @@ task
Usage:
task init
- task create <title> <project> [--deps=<ids>] [--namespace=<ns>]
- task list [--project=<project>]
+ task create <title> [--type=<type>] [--parent=<id>] [--deps=<ids>] [--namespace=<ns>]
+ task list [--type=<type>] [--parent=<id>]
task ready
task update <id> <status>
task deps <id>
@@ -45,7 +45,7 @@ Usage:
Commands:
init Initialize task database
- create Create a new task
+ create Create a new task or epic
list List all tasks
ready Show ready tasks (not blocked)
update Update task status
@@ -56,7 +56,8 @@ Commands:
Options:
-h --help Show this help
- --project=<project> Filter by project
+ --type=<type> Task type: epic or task (default: task)
+ --parent=<id> Parent epic ID
--deps=<ids> Comma-separated list of dependency IDs
--namespace=<ns> Optional namespace (e.g., Omni/Task, Biz/Cloud)
--flush Force immediate export
@@ -64,7 +65,6 @@ Options:
Arguments:
<title> Task title
- <project> Project name
<id> Task ID
<status> Task status (open, in-progress, done)
<file> JSONL file to import
@@ -75,7 +75,14 @@ move args
| args `Cli.has` Cli.command "init" = initTaskDb
| args `Cli.has` Cli.command "create" = do
title <- getArgText args "title"
- project <- getArgText args "project"
+ taskType <- case Cli.getArg args (Cli.longOption "type") of
+ Nothing -> pure WorkTask
+ Just "epic" -> pure Epic
+ Just "task" -> pure WorkTask
+ Just other -> panic <| "Invalid task type: " <> T.pack other <> ". Use: epic or task"
+ parent <- case Cli.getArg args (Cli.longOption "parent") of
+ Nothing -> pure Nothing
+ Just p -> pure <| Just (T.pack p)
deps <- case Cli.getArg args (Cli.longOption "deps") of
Nothing -> pure []
Just depStr -> pure <| T.splitOn "," (T.pack depStr)
@@ -86,13 +93,18 @@ move args
let validNs = Namespace.fromHaskellModule ns
nsPath = T.pack <| Namespace.toPath validNs
pure <| Just nsPath
- task <- createTask title project namespace deps
- putStrLn <| "Created task: " <> T.unpack (taskId task)
+ createdTask <- createTask title taskType parent namespace deps
+ putStrLn <| "Created task: " <> T.unpack (taskId createdTask)
| args `Cli.has` Cli.command "list" = do
- maybeProject <- case Cli.getArg args (Cli.longOption "project") of
+ maybeType <- case Cli.getArg args (Cli.longOption "type") of
+ Nothing -> pure Nothing
+ Just "epic" -> pure <| Just Epic
+ Just "task" -> pure <| Just WorkTask
+ Just other -> panic <| "Invalid task type: " <> T.pack other
+ maybeParent <- case Cli.getArg args (Cli.longOption "parent") of
Nothing -> pure Nothing
Just p -> pure <| Just (T.pack p)
- tasks <- listTasks maybeProject
+ tasks <- listTasks maybeType maybeParent
traverse_ printTask tasks
| args `Cli.has` Cli.command "ready" = do
tasks <- getReadyTasks
@@ -140,17 +152,17 @@ unitTests =
when exists <| removeFile ".tasks/tasks.jsonl"
initTaskDb
- task <- createTask "Test task" "test-project" Nothing []
+ task <- createTask "Test task" WorkTask Nothing Nothing []
taskTitle task Test.@?= "Test task"
- taskProject task Test.@?= "test-project"
+ taskType task Test.@?= WorkTask
taskStatus task Test.@?= Open
null (taskDependencies task) Test.@?= True,
Test.unit "can list tasks" <| do
- tasks <- listTasks Nothing
+ tasks <- listTasks Nothing Nothing
not (null tasks) Test.@?= True,
Test.unit "ready tasks exclude blocked ones" <| do
- task1 <- createTask "First task" "test" Nothing []
- task2 <- createTask "Blocked task" "test" Nothing [taskId task1]
+ task1 <- createTask "First task" WorkTask Nothing Nothing []
+ task2 <- createTask "Blocked task" WorkTask Nothing Nothing [taskId task1]
ready <- getReadyTasks
(taskId task1 `elem` map taskId ready) Test.@?= True
(taskId task2 `notElem` map taskId ready) Test.@?= True
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index f67c076..6d9856f 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -17,7 +17,8 @@ import System.Directory (createDirectoryIfMissing, doesFileExist)
data Task = Task
{ taskId :: Text,
taskTitle :: Text,
- taskProject :: Text,
+ taskType :: TaskType,
+ taskParent :: Maybe Text, -- Parent epic ID
taskNamespace :: Maybe Text, -- Optional namespace (e.g., "Omni/Task", "Biz/Cloud")
taskStatus :: Status,
taskDependencies :: [Text], -- List of task IDs this depends on
@@ -26,9 +27,16 @@ data Task = Task
}
deriving (Show, Eq, Generic)
+data TaskType = Epic | WorkTask
+ deriving (Show, Eq, Generic)
+
data Status = Open | InProgress | Done
deriving (Show, Eq, Generic)
+instance ToJSON TaskType
+
+instance FromJSON TaskType
+
instance ToJSON Status
instance FromJSON Status
@@ -96,15 +104,16 @@ saveTask task = do
BLC.appendFile ".tasks/tasks.jsonl" (json <> "\n")
-- Create a new task
-createTask :: Text -> Text -> Maybe Text -> [Text] -> IO Task
-createTask title project namespace deps = do
+createTask :: Text -> TaskType -> Maybe Text -> Maybe Text -> [Text] -> IO Task
+createTask title taskType parent namespace deps = do
tid <- generateId
now <- getCurrentTime
let task =
Task
{ taskId = tid,
taskTitle = title,
- taskProject = project,
+ taskType = taskType,
+ taskParent = parent,
taskNamespace = namespace,
taskStatus = Open,
taskDependencies = deps,
@@ -128,13 +137,20 @@ updateTaskStatus tid newStatus = do
TIO.writeFile ".tasks/tasks.jsonl" ""
traverse_ saveTask updatedTasks
--- List tasks, optionally filtered by project
-listTasks :: Maybe Text -> IO [Task]
-listTasks maybeProject = do
+-- List tasks, optionally filtered by type or parent
+listTasks :: Maybe TaskType -> Maybe Text -> IO [Task]
+listTasks maybeType maybeParent = do
tasks <- loadTasks
- pure <| case maybeProject of
- Nothing -> tasks
- Just proj -> filter (\t -> taskProject t == proj) tasks
+ let filtered =
+ tasks
+ |> filterByType maybeType
+ |> filterByParent maybeParent
+ pure filtered
+ where
+ filterByType Nothing ts = ts
+ filterByType (Just typ) ts = filter (\t -> taskType t == typ) ts
+ filterByParent Nothing ts = ts
+ filterByParent (Just pid) ts = filter (\t -> taskParent t == Just pid) ts
-- Get ready tasks (not blocked by dependencies)
getReadyTasks :: IO [Task]
@@ -165,14 +181,17 @@ printTask t =
putText
<| taskId t
<> " ["
+ <> T.pack (show (taskType t))
+ <> "] ["
<> T.pack (show (taskStatus t))
<> "] "
<> taskTitle t
- <> " ("
- <> taskProject t
- <> ")"
+ <> parentInfo
<> namespaceInfo
where
+ parentInfo = case taskParent t of
+ Nothing -> ""
+ Just p -> " (parent: " <> p <> ")"
namespaceInfo = case taskNamespace t of
Nothing -> ""
Just ns -> " [" <> ns <> "]"