summaryrefslogtreecommitdiff
path: root/Omni/Task.hs
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2025-11-08 16:13:29 -0500
committerBen Sima <ben@bsima.me>2025-11-08 16:15:15 -0500
commit3bf1691f4e32235f84f5cff9d6e4a3fdb9a57ffc (patch)
treed33fccbf6b54164bd97320cad23c9109b3ef6516 /Omni/Task.hs
parentce6d313edbf5c545d16d88d28be867122b7c3d1b (diff)
Add task manager for AI agents
Implemented a dependency-aware task tracker inspired by beads: - Task CRUD operations (create, list, update, ready) - Dependency tracking and ready work detection - JSONL storage with git sync via hooks - Export/import for cross-machine synchronization - Short base62-encoded task IDs (e.g., t-1ky7gJ2) Added comprehensive AGENTS.md documentation: - Task manager usage and workflows - Development tools (bild, lint, repl.sh) - Git-branchless workflow guidelines - Coding conventions Integrated with git hooks for auto-sync: - post-merge/post-checkout: import tasks - pre-commit/pre-push: export tasks Also includes beads design analysis document for reference. Completed tasks: - t-a1b2c3: Show help text when invoked without args - t-d4e5f6: Move dev instructions to AGENTS.md - t-g7h8i9: Implement shorter task IDs - t-p6q7r8: Add git-branchless workflow docs https: //ampcode.com/threads/T-85f4ee29-a529-4f59-ac6f-6ffec75b6a56 Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-85f4ee29-a529-4f59-ac6f-6ffec75b6a56
Diffstat (limited to 'Omni/Task.hs')
-rw-r--r--Omni/Task.hs148
1 files changed, 148 insertions, 0 deletions
diff --git a/Omni/Task.hs b/Omni/Task.hs
new file mode 100644
index 0000000..be54d3d
--- /dev/null
+++ b/Omni/Task.hs
@@ -0,0 +1,148 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- : out task
+module Omni.Task where
+
+import Alpha
+import qualified Data.Text as T
+import qualified Omni.Cli as Cli
+import Omni.Task.Core
+import qualified Omni.Test as Test
+import System.Directory (doesFileExist, removeFile)
+
+main :: IO ()
+main = Cli.main plan
+
+plan :: Cli.Plan ()
+plan =
+ Cli.Plan
+ { help = help,
+ move = move,
+ test = test,
+ tidy = \_ -> pure ()
+ }
+
+help :: Cli.Docopt
+help =
+ [Cli.docopt|
+task
+
+Usage:
+ task init
+ task create <title> <project> [--deps=<ids>]
+ task list [--project=<project>]
+ task ready
+ task update <id> <status>
+ task deps <id>
+ task export [--flush]
+ task import -i <file>
+ task test
+ task (-h | --help)
+
+Commands:
+ init Initialize task database
+ create Create a new task
+ list List all tasks
+ ready Show ready tasks (not blocked)
+ update Update task status
+ deps Show dependency tree
+ export Export and consolidate tasks to JSONL
+ import Import tasks from JSONL file
+ test Run tests
+
+Options:
+ -h --help Show this help
+ --project=<project> Filter by project
+ --deps=<ids> Comma-separated list of dependency IDs
+ --flush Force immediate export
+ -i <file> Input file for import
+
+Arguments:
+ <title> Task title
+ <project> Project name
+ <id> Task ID
+ <status> Task status (open, in-progress, done)
+ <file> JSONL file to import
+|]
+
+move :: Cli.Arguments -> IO ()
+move args
+ | args `Cli.has` Cli.command "init" = initTaskDb
+ | args `Cli.has` Cli.command "create" = do
+ title <- getArgText args "title"
+ project <- getArgText args "project"
+ deps <- case Cli.getArg args (Cli.longOption "deps") of
+ Nothing -> pure []
+ Just depStr -> pure <| T.splitOn "," (T.pack depStr)
+ task <- createTask title project deps
+ putStrLn <| "Created task: " <> T.unpack (taskId task)
+ | args `Cli.has` Cli.command "list" = do
+ maybeProject <- case Cli.getArg args (Cli.longOption "project") of
+ Nothing -> pure Nothing
+ Just p -> pure <| Just (T.pack p)
+ tasks <- listTasks maybeProject
+ traverse_ printTask tasks
+ | args `Cli.has` Cli.command "ready" = do
+ tasks <- getReadyTasks
+ putText "Ready tasks:"
+ traverse_ printTask tasks
+ | args `Cli.has` Cli.command "update" = do
+ tid <- getArgText args "id"
+ statusStr <- getArgText args "status"
+ let newStatus = case statusStr of
+ "open" -> Open
+ "in-progress" -> InProgress
+ "done" -> Done
+ _ -> panic "Invalid status. Use: open, in-progress, or done"
+ updateTaskStatus tid newStatus
+ putStrLn <| "Updated task " <> T.unpack tid
+ | args `Cli.has` Cli.command "deps" = do
+ tid <- getArgText args "id"
+ showDependencyTree tid
+ | args `Cli.has` Cli.command "export" = do
+ exportTasks
+ putText "Exported and consolidated tasks to .tasks/tasks.jsonl"
+ | args `Cli.has` Cli.command "import" = do
+ file <- getArgText args "file"
+ importTasks (T.unpack file)
+ putText <| "Imported tasks from " <> file
+ | otherwise = putText (T.pack <| Cli.usage help)
+ where
+ getArgText :: Cli.Arguments -> String -> IO Text
+ getArgText argz name = do
+ maybeArg <- pure <| Cli.getArg argz (Cli.argument name)
+ case maybeArg of
+ Nothing -> panic (T.pack name <> " required")
+ Just val -> pure (T.pack val)
+
+test :: Test.Tree
+test = Test.group "Omni.Task" [unitTests]
+
+unitTests :: Test.Tree
+unitTests =
+ Test.group
+ "Unit tests"
+ [ Test.unit "can create task" <| do
+ -- Clean up before test
+ exists <- doesFileExist ".tasks/tasks.jsonl"
+ when exists <| removeFile ".tasks/tasks.jsonl"
+
+ initTaskDb
+ task <- createTask "Test task" "test-project" []
+ taskTitle task Test.@?= "Test task"
+ taskProject task Test.@?= "test-project"
+ taskStatus task Test.@?= Open
+ null (taskDependencies task) Test.@?= True,
+ Test.unit "can list tasks" <| do
+ tasks <- listTasks Nothing
+ not (null tasks) Test.@?= True,
+ Test.unit "ready tasks exclude blocked ones" <| do
+ task1 <- createTask "First task" "test" []
+ task2 <- createTask "Blocked task" "test" [taskId task1]
+ ready <- getReadyTasks
+ (taskId task1 `elem` map taskId ready) Test.@?= True
+ (taskId task2 `notElem` map taskId ready) Test.@?= True
+ ]