summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2025-11-09 07:37:25 -0500
committerBen Sima <ben@bsima.me>2025-11-09 07:37:25 -0500
commit04986e2fc5c8863672c2a84e644777505878318b (patch)
treef95b6ad65d076ca6b420c571f52bf7e3f6469998
parent1d98b463ac79905b542d45d632e5135ea90af585 (diff)
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 <amp@ampcode.com>
-rw-r--r--.tasks/tasks.jsonl14
-rw-r--r--AGENTS.md121
-rw-r--r--Omni/Task.hs48
-rw-r--r--Omni/Task/Core.hs32
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 "<title>" <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