From 3bf1691f4e32235f84f5cff9d6e4a3fdb9a57ffc Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Sat, 8 Nov 2025 16:13:29 -0500 Subject: 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-Thread-ID: https://ampcode.com/threads/T-85f4ee29-a529-4f59-ac6f-6ffec75b6a56 --- Omni/Task.hs | 148 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 Omni/Task.hs (limited to 'Omni/Task.hs') 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 <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 + ] -- cgit v1.2.3