From 04986e2fc5c8863672c2a84e644777505878318b Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Sun, 9 Nov 2025 07:37:25 -0500 Subject: Add enhanced dependency types to task manager Implement four dependency types based on beads patterns: - Blocks: Hard dependency, blocks ready work queue (default) - DiscoveredFrom: Work discovered during implementation (doesn't block) - ParentChild: Epic/task relationships (blocks ready work) - Related: Soft relationships (doesn't block) Key changes: - New Dependency data type with depId and depType fields - New DependencyType enum with four relationship types - Updated CLI with --dep-type and --discovered-from flags - Enhanced getReadyTasks to respect only blocking dependency types - Added comprehensive tests for all dependency behaviors - Updated AGENTS.md with usage examples and patterns The discovered-from pattern is especially important for AI agents to maintain context of work found during implementation while keeping it immediately available in the ready work queue. Amp-Thread-ID: https://ampcode.com/threads/T-178b273a-3ac7-416c-a964-db89bac3c8f7 Co-authored-by: Amp --- .tasks/tasks.jsonl | 14 ++++++- AGENTS.md | 121 ++++++++++++++++++++++++++++++++++++++--------------- Omni/Task.hs | 48 ++++++++++++++++++--- Omni/Task/Core.hs | 32 ++++++++++++-- 4 files changed, 169 insertions(+), 46 deletions(-) diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index a58fee2..ff2a394 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -1,2 +1,12 @@ -{"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"} +{"taskCreatedAt":"2025-11-09T12:28:48.574806406Z","taskDependencies":[],"taskId":"t-N2zIIk","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Test task","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:28:48.574806406Z"} +{"taskCreatedAt":"2025-11-09T12:28:48.591902984Z","taskDependencies":[],"taskId":"t-N2zNa5","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"First task","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:28:48.591902984Z"} +{"taskCreatedAt":"2025-11-09T12:28:48.592205598Z","taskDependencies":[{"depId":"t-N2zNa5","depType":"Blocks"}],"taskId":"t-N2zNeY","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Blocked task","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:28:48.592205598Z"} +{"taskCreatedAt":"2025-11-09T12:28:48.593123597Z","taskDependencies":[],"taskId":"t-N2zNtM","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Original task","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:28:48.593123597Z"} +{"taskCreatedAt":"2025-11-09T12:28:48.59342775Z","taskDependencies":[{"depId":"t-N2zNtM","depType":"DiscoveredFrom"}],"taskId":"t-N2zNyG","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Discovered work","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:28:48.59342775Z"} +{"taskCreatedAt":"2025-11-09T12:28:48.594193628Z","taskDependencies":[],"taskId":"t-N2zNL2","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Task A","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:28:48.594193628Z"} +{"taskCreatedAt":"2025-11-09T12:28:48.594472081Z","taskDependencies":[{"depId":"t-N2zNL2","depType":"Related"}],"taskId":"t-N2zNPx","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Task B","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:28:48.594472081Z"} +{"taskCreatedAt":"2025-11-09T12:30:08.108951426Z","taskDependencies":[],"taskId":"t-N7Xrb7","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Test task A","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:30:08.108951426Z"} +{"taskCreatedAt":"2025-11-09T12:30:08.2734999Z","taskDependencies":[{"depId":"t-MTdiPS","depType":"Blocks"}],"taskId":"t-N7Y7Z8","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Test task B with blocking dep","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:30:08.2734999Z"} +{"taskCreatedAt":"2025-11-09T12:30:08.459060403Z","taskDependencies":[{"depId":"t-MTdiPS","depType":"DiscoveredFrom"}],"taskId":"t-N7YUg2","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Discovered work","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:30:08.459060403Z"} +{"taskCreatedAt":"2025-11-09T12:30:45.984751589Z","taskDependencies":[],"taskId":"t-Nawmp8","taskNamespace":null,"taskParent":null,"taskStatus":"Done","taskTitle":"Blocking task example","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:30:54.668780601Z"} +{"taskCreatedAt":"2025-11-09T12:30:49.990290546Z","taskDependencies":[{"depId":"t-Nawmp8","depType":"Blocks"}],"taskId":"t-NaNaqA","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Dependent task","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T12:30:49.990290546Z"} diff --git a/AGENTS.md b/AGENTS.md index dde7e67..ab91f8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,16 +14,40 @@ The task manager is a dependency-aware issue tracker inspired by beads. It uses: ### Create a Task ```bash -task create "" <project> [--deps=<ids>] [--namespace=<ns>] +task create "<title>" [--type=<type>] [--parent=<id>] [--deps=<ids>] [--dep-type=<type>] [--discovered-from=<id>] [--namespace=<ns>] ``` Examples: ```bash -task create "Add authentication" auth-system -task create "Write tests" auth-system --deps=t-a1b2c3 -task create "Fix type errors" task-manager --namespace="Omni/Task" +# Create an epic (container for tasks) +task create "User Authentication System" --type=epic + +# Create a task within an epic +task create "Design auth API" --parent=t-abc123 + +# Create a task with blocking dependency +task create "Write tests" --deps=t-a1b2c3 --dep-type=blocks + +# Create work discovered during implementation (shortcut) +task create "Fix memory leak" --discovered-from=t-abc123 + +# Create related work (doesn't block) +task create "Update documentation" --deps=t-abc123 --dep-type=related + +# Associate with a namespace +task create "Fix type errors" --namespace="Omni/Task" ``` +**Task Types:** +- `epic` - Container for related tasks (replaces the old "project" concept) +- `task` - Individual work item (default) + +**Dependency Types:** +- `blocks` - Hard dependency, blocks ready work queue (default) +- `discovered-from` - Work discovered during other work, doesn't block +- `parent-child` - Epic/subtask relationship, blocks ready work +- `related` - Soft relationship, doesn't block + The `--namespace` option associates the task with a specific namespace in the monorepo (e.g., `Omni/Task`, `Biz/Cloud`). This helps organize tasks by the code they relate to. ### List Tasks @@ -118,39 +142,60 @@ When you discover work that depends on other work: ```bash # Create the blocking task first -task create "Design API" api-layer +task create "Design API" --type=task # Note the ID (e.g., t-20241108120000) -# Create dependent task -task create "Implement API client" api-layer --deps=t-20241108120000 +# Create dependent task with blocking dependency +task create "Implement API client" --deps=t-20241108120000 --dep-type=blocks ``` The dependent task won't show up in `task ready` until the blocker is marked `done`. -### Working on a Project +### Discovered Work Pattern + +When you find work during implementation, use the `--discovered-from` flag: + +```bash +# While working on t-abc123, you discover a bug +task create "Fix memory leak in parser" --discovered-from=t-abc123 + +# This is equivalent to: +task create "Fix memory leak in parser" --deps=t-abc123 --dep-type=discovered-from +``` + +The `discovered-from` dependency type maintains context but **doesn't block** the ready work queue. This allows AI agents to track what work was found during other work while still being able to work on it immediately. + +### Working with Epics ```bash -# See all tasks for a project -task list --project=auth-system +# Create an epic for a larger feature +task create "User Authentication System" --type=epic +# Note ID: t-abc123 -# Create related tasks -task create "Design login flow" auth-system -task create "Implement OAuth" auth-system -task create "Add password reset" auth-system +# Create child tasks within the epic +task create "Design login flow" --parent=t-abc123 +task create "Implement OAuth" --parent=t-abc123 +task create "Add password reset" --parent=t-abc123 + +# List all tasks in an epic +task list --parent=t-abc123 + +# List all epics +task list --type=epic ``` ### Breaking Down Large Work ```bash -# Create parent task -task create "Complete authentication system" auth-system +# Create parent epic +task create "Complete authentication system" --type=epic # Note ID: t-20241108120000 # Create subtasks that depend on planning -task create "Backend auth service" auth-system --deps=t-20241108120000 -task create "Frontend login UI" auth-system --deps=t-20241108120000 -task create "Integration tests" auth-system --deps=t-20241108120000 +task create "Backend auth service" --parent=t-20241108120000 +task create "Frontend login UI" --parent=t-20241108120000 +task create "Integration tests" --parent=t-20241108120000 ``` ## Agent Best Practices @@ -161,25 +206,26 @@ Before asking what to do, check `task ready` to see unblocked tasks. ### 2. Create Tasks for Discovered Work When you encounter work during implementation: ```bash -task create "Fix type error in auth module" auth-system -task create "Add missing test coverage" testing +task create "Fix type error in auth module" --discovered-from=t-abc123 +task create "Add missing test coverage" --discovered-from=t-abc123 ``` ### 3. Track Dependencies If work depends on other work, use `--deps`: ```bash # Can't write tests until implementation is done -task create "Test auth flow" testing --deps=t-20241108120000 +task create "Test auth flow" --deps=t-20241108120000 --dep-type=blocks ``` ### 4. Use Descriptive Titles Good: `"Add JWT token validation to auth middleware"` Bad: `"Fix auth"` -### 5. Keep Projects Organized -Use consistent project names: -- `auth-system` not `auth`, `authentication`, `auth-system-v2` -- `api-layer` not `api`, `API`, `backend-api` +### 5. Use Epics for Organization +Organize related work using epics: +- Create an epic for larger features: `task create "Feature Name" --type=epic` +- Add tasks to the epic using `--parent=<epic-id>` +- Use `--discovered-from` to track work found during implementation ## Task Lifecycle @@ -215,10 +261,14 @@ Each line in `tasks.jsonl` is a JSON object representing a task. # First time setup task init -# Create some work -task create "Design task manager schema" core-system -task create "Implement JSONL storage" core-system -task create "Add dependency tracking" core-system +# Create an epic for the work +task create "Task Manager Improvements" --type=epic +# Returns: t-abc123 + +# Create tasks within the epic +task create "Design task manager schema" --parent=t-abc123 +task create "Implement JSONL storage" --parent=t-abc123 +task create "Add dependency tracking" --parent=t-abc123 # See what's ready (all of them, no blockers yet) task ready @@ -226,15 +276,18 @@ task ready # Start working task update t-20241108120000 in-progress -# Discover dependent work -task create "Write storage tests" testing --deps=t-20241108120000 +# Discover work during implementation +task create "Fix edge case in ID generation" --discovered-from=t-20241108120000 + +# Discover dependent work with blocking +task create "Write storage tests" --deps=t-20241108120000 --dep-type=blocks # Complete first task task update t-20241108120000 done -# Now the test task is unblocked +# Now the test task is unblocked (discovered work was already unblocked) task ready -# Shows: "Write storage tests" +# Shows: "Write storage tests" and "Fix edge case in ID generation" ``` ## Build and Test Commands diff --git a/Omni/Task.hs b/Omni/Task.hs index 153f899..571c72e 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -33,7 +33,7 @@ task Usage: task init - task create <title> [--type=<type>] [--parent=<id>] [--deps=<ids>] [--namespace=<ns>] + task create <title> [--type=<type>] [--parent=<id>] [--deps=<ids>] [--dep-type=<type>] [--discovered-from=<id>] [--namespace=<ns>] task list [--type=<type>] [--parent=<id>] task ready task update <id> <status> @@ -59,6 +59,8 @@ Options: --type=<type> Task type: epic or task (default: task) --parent=<id> Parent epic ID --deps=<ids> Comma-separated list of dependency IDs + --dep-type=<type> Dependency type: blocks, discovered-from, parent-child, related (default: blocks) + --discovered-from=<id> Shortcut for --deps=<id> --dep-type=discovered-from --namespace=<ns> Optional namespace (e.g., Omni/Task, Biz/Cloud) --flush Force immediate export -i <file> Input file for import @@ -83,9 +85,26 @@ move args 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) + + -- Handle --discovered-from as shortcut + (depIds, depType) <- case Cli.getArg args (Cli.longOption "discovered-from") of + Just discoveredId -> pure ([T.pack discoveredId], DiscoveredFrom) + Nothing -> do + -- Parse regular --deps and --dep-type + ids <- case Cli.getArg args (Cli.longOption "deps") of + Nothing -> pure [] + Just depStr -> pure <| T.splitOn "," (T.pack depStr) + dtype <- case Cli.getArg args (Cli.longOption "dep-type") of + Nothing -> pure Blocks + Just "blocks" -> pure Blocks + Just "discovered-from" -> pure DiscoveredFrom + Just "parent-child" -> pure ParentChild + Just "related" -> pure Related + Just other -> panic <| "Invalid dependency type: " <> T.pack other <> ". Use: blocks, discovered-from, parent-child, or related" + pure (ids, dtype) + + let deps = map (\did -> Dependency {depId = did, depType = depType}) depIds + namespace <- case Cli.getArg args (Cli.longOption "namespace") of Nothing -> pure Nothing Just ns -> do @@ -162,8 +181,25 @@ unitTests = not (null tasks) Test.@?= True, Test.unit "ready tasks exclude blocked ones" <| do task1 <- createTask "First task" WorkTask Nothing Nothing [] - task2 <- createTask "Blocked task" WorkTask Nothing Nothing [taskId task1] + let blockingDep = Dependency {depId = taskId task1, depType = Blocks} + task2 <- createTask "Blocked task" WorkTask Nothing Nothing [blockingDep] + ready <- getReadyTasks + (taskId task1 `elem` map taskId ready) Test.@?= True + (taskId task2 `notElem` map taskId ready) Test.@?= True, + Test.unit "discovered-from dependencies don't block" <| do + task1 <- createTask "Original task" WorkTask Nothing Nothing [] + let discDep = Dependency {depId = taskId task1, depType = DiscoveredFrom} + task2 <- createTask "Discovered work" WorkTask Nothing Nothing [discDep] + ready <- getReadyTasks + -- Both should be ready since DiscoveredFrom doesn't block + (taskId task1 `elem` map taskId ready) Test.@?= True + (taskId task2 `elem` map taskId ready) Test.@?= True, + Test.unit "related dependencies don't block" <| do + task1 <- createTask "Task A" WorkTask Nothing Nothing [] + let relDep = Dependency {depId = taskId task1, depType = Related} + task2 <- createTask "Task B" WorkTask Nothing Nothing [relDep] ready <- getReadyTasks + -- Both should be ready since Related doesn't block (taskId task1 `elem` map taskId ready) Test.@?= True - (taskId task2 `notElem` map taskId ready) Test.@?= True + (taskId task2 `elem` map taskId ready) Test.@?= True ] diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 6d9856f..1137d8d 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -21,7 +21,7 @@ data Task = Task 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 + taskDependencies :: [Dependency], -- List of dependencies with types taskCreatedAt :: UTCTime, taskUpdatedAt :: UTCTime } @@ -33,6 +33,19 @@ data TaskType = Epic | WorkTask data Status = Open | InProgress | Done deriving (Show, Eq, Generic) +data Dependency = Dependency + { depId :: Text, -- ID of the task this depends on + depType :: DependencyType -- Type of dependency relationship + } + deriving (Show, Eq, Generic) + +data DependencyType + = Blocks -- Hard dependency, blocks ready work queue + | DiscoveredFrom -- Work discovered during other work + | ParentChild -- Epic/subtask relationship + | Related -- Soft relationship, doesn't block + deriving (Show, Eq, Generic) + instance ToJSON TaskType instance FromJSON TaskType @@ -41,6 +54,14 @@ instance ToJSON Status instance FromJSON Status +instance ToJSON DependencyType + +instance FromJSON DependencyType + +instance ToJSON Dependency + +instance FromJSON Dependency + instance ToJSON Task instance FromJSON Task @@ -104,7 +125,7 @@ saveTask task = do BLC.appendFile ".tasks/tasks.jsonl" (json <> "\n") -- Create a new task -createTask :: Text -> TaskType -> Maybe Text -> Maybe Text -> [Text] -> IO Task +createTask :: Text -> TaskType -> Maybe Text -> Maybe Text -> [Dependency] -> IO Task createTask title taskType parent namespace deps = do tid <- generateId now <- getCurrentTime @@ -158,7 +179,9 @@ getReadyTasks = do allTasks <- loadTasks let openTasks = filter (\t -> taskStatus t /= Done) allTasks doneIds = map taskId <| filter (\t -> taskStatus t == Done) allTasks - isReady task = all (`elem` doneIds) (taskDependencies task) + -- Only Blocks and ParentChild dependencies block ready work + blockingDepIds task = [depId dep | dep <- taskDependencies task, depType dep `elem` [Blocks, ParentChild]] + isReady task = all (`elem` doneIds) (blockingDepIds task) pure <| filter isReady openTasks -- Show dependency tree for a task @@ -172,7 +195,8 @@ showDependencyTree tid = do printTree :: [Task] -> Task -> Int -> IO () printTree allTasks task indent = do putText <| T.pack (replicate (indent * 2) ' ') <> taskId task <> ": " <> taskTitle task - let deps = filter (\t -> taskId t `elem` taskDependencies task) allTasks + let depIds = map depId (taskDependencies task) + deps = filter (\t -> taskId t `elem` depIds) allTasks traverse_ (\dep -> printTree allTasks dep (indent + 1)) deps -- Helper to print a task -- cgit v1.2.3