diff options
| author | Ben Sima <ben@bsima.me> | 2025-11-09 07:18:18 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bsima.me> | 2025-11-09 07:18:18 -0500 |
| commit | 1d98b463ac79905b542d45d632e5135ea90af585 (patch) | |
| tree | 6db13ea019d1e0d7f809ef059921e0a99d5b1ad8 | |
| parent | 36a31210ff5682f3387a28893563f0c5bf1a4cc5 (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.jsonl | 5 | ||||
| -rw-r--r-- | Omni/Task.hs | 42 | ||||
| -rw-r--r-- | Omni/Task/Core.hs | 45 |
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 <> "]" |
