summaryrefslogtreecommitdiff
path: root/Omni/Task
diff options
context:
space:
mode:
Diffstat (limited to 'Omni/Task')
-rw-r--r--Omni/Task/Core.hs106
-rw-r--r--Omni/Task/README.md415
2 files changed, 498 insertions, 23 deletions
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index c52f9bf..ebf5390 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -96,12 +96,28 @@ instance FromJSON Task
-- | Case-insensitive ID comparison
matchesId :: Text -> Text -> Bool
-matchesId id1 id2 = T.toLower id1 == T.toLower id2
+matchesId id1 id2 = normalizeId id1 == normalizeId id2
+
+-- | Normalize ID to lowercase
+normalizeId :: Text -> Text
+normalizeId = T.toLower
-- | Find a task by ID (case-insensitive)
findTask :: Text -> [Task] -> Maybe Task
findTask tid = List.find (\t -> matchesId (taskId t) tid)
+-- | Normalize task IDs (self, parent, dependencies) to lowercase
+normalizeTask :: Task -> Task
+normalizeTask t =
+ t
+ { taskId = normalizeId (taskId t),
+ taskParent = fmap normalizeId (taskParent t),
+ taskDependencies = map normalizeDependency (taskDependencies t)
+ }
+
+normalizeDependency :: Dependency -> Dependency
+normalizeDependency d = d {depId = normalizeId (depId d)}
+
instance ToJSON TaskProgress
instance FromJSON TaskProgress
@@ -176,7 +192,7 @@ withTaskReadLock action =
action
)
--- Generate a short ID using base62 encoding of timestamp
+-- Generate a short ID using base36 encoding of timestamp (lowercase)
generateId :: IO Text
generateId = do
now <- getCurrentTime
@@ -188,7 +204,7 @@ generateId = do
-- Combine MJD and micros to ensure uniqueness across days.
-- Multiplier 10^11 (100,000 seconds) is safe for any day length.
totalMicros = (mjd * 100000000000) + micros
- encoded = toBase62 totalMicros
+ encoded = toBase36 totalMicros
pure <| "t-" <> T.pack encoded
-- Generate a child ID based on parent ID (e.g. "t-abc.1", "t-abc.1.2")
@@ -197,7 +213,7 @@ generateChildId :: Text -> IO Text
generateChildId parentId =
withTaskReadLock <| do
tasks <- loadTasksInternal
- pure <| computeNextChildId tasks parentId
+ pure <| computeNextChildId tasks (normalizeId parentId)
computeNextChildId :: [Task] -> Text -> Text
computeNextChildId tasks parentId =
@@ -220,15 +236,15 @@ getSuffix parent childId =
else Nothing
else Nothing
--- Convert number to base62 (0-9, a-z, A-Z)
-toBase62 :: Integer -> String
-toBase62 0 = "0"
-toBase62 n = reverse <| go n
+-- Convert number to base36 (0-9, a-z)
+toBase36 :: Integer -> String
+toBase36 0 = "0"
+toBase36 n = reverse <| go n
where
- alphabet = ['0' .. '9'] ++ ['a' .. 'z'] ++ ['A' .. 'Z']
+ alphabet = ['0' .. '9'] ++ ['a' .. 'z']
go 0 = []
go x =
- let (q, r) = x `divMod` 62
+ let (q, r) = x `divMod` 36
idx = fromIntegral r
char = case drop idx alphabet of
(c : _) -> c
@@ -319,22 +335,25 @@ saveTaskInternal task = do
createTask :: Text -> TaskType -> Maybe Text -> Maybe Text -> Priority -> [Dependency] -> Maybe Text -> IO Task
createTask title taskType parent namespace priority deps description =
withTaskWriteLock <| do
- tid <- case parent of
- Nothing -> generateId
+ let parent' = fmap normalizeId parent
+ deps' = map normalizeDependency deps
+
+ tid <- case parent' of
+ Nothing -> generateUniqueId
Just pid -> do
tasks <- loadTasksInternal
pure <| computeNextChildId tasks pid
now <- getCurrentTime
let task =
Task
- { taskId = tid,
+ { taskId = normalizeId tid,
taskTitle = title,
taskType = taskType,
- taskParent = parent,
+ taskParent = parent',
taskNamespace = namespace,
taskStatus = Open,
taskPriority = priority,
- taskDependencies = deps,
+ taskDependencies = deps',
taskDescription = description,
taskCreatedAt = now,
taskUpdatedAt = now
@@ -342,22 +361,62 @@ createTask title taskType parent namespace priority deps description =
saveTaskInternal task
pure task
+-- Generate a unique ID (checking against existing tasks)
+generateUniqueId :: IO Text
+generateUniqueId = do
+ tasks <- loadTasksInternal
+ go tasks
+ where
+ go tasks = do
+ tid <- generateId
+ case findTask tid tasks of
+ Nothing -> pure tid
+ Just _ -> go tasks -- Retry if collision (case-insensitive)
+
-- Update task status
-updateTaskStatus :: Text -> Status -> IO ()
-updateTaskStatus tid newStatus =
+updateTaskStatus :: Text -> Status -> [Dependency] -> IO ()
+updateTaskStatus tid newStatus newDeps =
withTaskWriteLock <| do
tasks <- loadTasksInternal
now <- getCurrentTime
let updatedTasks = map updateIfMatch tasks
updateIfMatch t =
if matchesId (taskId t) tid
- then t {taskStatus = newStatus, taskUpdatedAt = now}
+ then t {taskStatus = newStatus, taskUpdatedAt = now, taskDependencies = if null newDeps then taskDependencies t else newDeps}
else t
-- Rewrite the entire file (simple approach for MVP)
tasksFile <- getTasksFilePath
TIO.writeFile tasksFile ""
traverse_ saveTaskInternal updatedTasks
+-- Edit a task by applying a modification function
+editTask :: Text -> (Task -> Task) -> IO Task
+editTask tid modifyFn =
+ withTaskWriteLock <| do
+ tasks <- loadTasksInternal
+ now <- getCurrentTime
+
+ -- Find the task first to ensure it exists
+ case findTask tid tasks of
+ Nothing -> panic "Task not found"
+ Just original -> do
+ let modified = modifyFn original
+ -- Only update timestamp if something actually changed
+ -- But for simplicity, we always update it if the user explicitly ran 'edit'
+ finalTask = modified {taskUpdatedAt = now}
+
+ updateIfMatch t =
+ if matchesId (taskId t) tid
+ then finalTask
+ else t
+ updatedTasks = map updateIfMatch tasks
+
+ -- Rewrite the entire file
+ tasksFile <- getTasksFilePath
+ TIO.writeFile tasksFile ""
+ traverse_ saveTaskInternal updatedTasks
+ pure finalTask
+
-- List tasks, optionally filtered by type, parent, status, or namespace
listTasks :: Maybe TaskType -> Maybe Text -> Maybe Status -> Maybe Text -> IO [Task]
listTasks maybeType maybeParent maybeStatus maybeNamespace = do
@@ -415,12 +474,13 @@ getDependencyTree tid = do
-- Get task progress
getTaskProgress :: Text -> IO TaskProgress
-getTaskProgress tid = do
+getTaskProgress tidRaw = do
+ let tid = normalizeId tidRaw
tasks <- loadTasks
-- Verify task exists (optional, but good for error handling)
- case filter (\t -> taskId t == tid) tasks of
- [] -> panic "Task not found"
- _ -> do
+ case findTask tid tasks of
+ Nothing -> panic "Task not found"
+ Just _ -> do
let children = filter (\child -> taskParent child == Just tid) tasks
total = length children
completed = length <| filter (\child -> taskStatus child == Done) children
@@ -822,7 +882,7 @@ importTasks filePath =
-- Load tasks from import file
content <- TIO.readFile filePath
let importLines = T.lines content
- importedTasks = mapMaybe decodeTask importLines
+ importedTasks = map normalizeTask (mapMaybe decodeTask importLines)
-- Load existing tasks
existingTasks <- loadTasksInternal
diff --git a/Omni/Task/README.md b/Omni/Task/README.md
new file mode 100644
index 0000000..8e8670e
--- /dev/null
+++ b/Omni/Task/README.md
@@ -0,0 +1,415 @@
+# Task Manager for AI Agents
+
+The task manager is a dependency-aware issue tracker inspired by beads. It uses:
+- **Storage**: Local JSONL file (`.tasks/tasks.jsonl`)
+- **Sync**: Git-tracked (automatically synced across machines)
+- **Dependencies**: Tasks can block other tasks
+- **Ready work detection**: Automatically finds unblocked tasks
+
+**IMPORTANT**: You MUST use `task` for ALL issue tracking. NEVER use markdown TODOs, todo_write, task lists, or any other tracking methods.
+
+## Human Setup vs Agent Usage
+
+**If you see "database not found" or similar errors:**
+```bash
+task init --quiet # Non-interactive, auto-setup, no prompts
+```
+
+**Why `--quiet`?** The regular `task init` may have interactive prompts. The `--quiet` flag makes it fully non-interactive and safe for agent-driven setup.
+
+**If `task init --quiet` fails:** Ask the human to run `task init` manually, then continue.
+
+## Create a Task
+```bash
+task create "<title>" [--type=<type>] [--parent=<id>] [--deps=<ids>] [--dep-type=<type>] [--discovered-from=<id>] [--namespace=<ns>]
+```
+
+Examples:
+```bash
+# 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
+- `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
+```bash
+task list [options] # Flags can be in any order
+```
+
+Examples:
+```bash
+task list # All tasks
+task list --type=epic # All epics
+task list --parent=t-abc123 # All tasks in an epic
+task list --status=open # All open tasks
+task list --status=done # All completed tasks
+task list --namespace="Omni/Task" # All tasks for a namespace
+task list --parent=t-abc123 --status=open # Combine filters: open tasks in epic
+```
+
+## Get Ready Work
+```bash
+task ready
+```
+
+Shows all tasks that are:
+- Not closed
+- Not blocked by incomplete dependencies
+
+## Update Task Status
+```bash
+task update <id> <status>
+```
+
+Status values: `open`, `in-progress`, `done`
+
+Examples:
+```bash
+task update t-20241108120000 in-progress
+task update t-20241108120000 done
+```
+
+**Note**: Task updates modify `.tasks/tasks.jsonl` but don't auto-commit. The pre-commit hook will automatically export and stage task changes on your next `git commit`.
+
+## View Dependencies
+```bash
+task deps <id>
+```
+
+Shows the dependency tree for a task.
+
+## View Task Tree
+```bash
+task tree [<id>]
+```
+
+Shows task hierarchy with visual status indicators:
+- `[ ]` - Open
+- `[~]` - In Progress
+- `[✓]` - Done
+
+Examples:
+```bash
+task tree # Show all epics with their children
+task tree t-abc123 # Show specific epic/task with its children
+```
+
+## Export Tasks
+```bash
+task export [--flush]
+```
+
+Consolidates and exports tasks to `.tasks/tasks.jsonl`, removing duplicates. The `--flush` flag forces immediate export (used by git hooks).
+
+## Import Tasks
+```bash
+task import -i <file>
+```
+
+Imports tasks from a JSONL file, merging with existing tasks. Newer tasks (based on `updatedAt` timestamp) take precedence.
+
+Examples:
+```bash
+task import -i .tasks/tasks.jsonl
+task import -i /path/to/backup.jsonl
+```
+
+## Initialize (First Time)
+```bash
+task init --quiet # Non-interactive (recommended for agents)
+# OR
+task init # Interactive (for humans)
+```
+
+Creates `.tasks/` directory and `tasks.jsonl` file.
+
+**Agents MUST use `--quiet` flag** to avoid interactive prompts.
+
+## Common Workflows
+
+### Starting New Work
+
+1. **Find what's ready to work on:**
+ ```bash
+ task ready
+ ```
+
+2. **Pick a task and mark it in progress:**
+ ```bash
+ task update t-20241108120000 in-progress
+ ```
+
+3. **When done, mark it complete:**
+ ```bash
+ task update t-20241108120000 done
+ ```
+
+### Creating Dependent Tasks
+
+When you discover work that depends on other work:
+
+```bash
+# Create the blocking task first
+task create "Design API" --type=task
+
+# Note the ID (e.g., 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`.
+
+### 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
+# Create an epic for a larger feature
+task create "User Authentication System" --type=epic
+# Note ID: t-abc123
+
+# 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
+```
+
+## Agent Best Practices
+
+### 1. ALWAYS Check Ready Work First
+Before asking what to do, you MUST check `task ready --json` to see unblocked tasks.
+
+### 2. ALWAYS Create Tasks for Discovered Work
+When you encounter work during implementation, you MUST create linked tasks:
+```bash
+task create "Fix type error in auth module" --discovered-from=t-abc123 --json
+task create "Add missing test coverage" --discovered-from=t-abc123 --json
+```
+
+**Bug Discovery Pattern**
+
+When you discover a bug or unexpected behavior:
+```bash
+# CORRECT: Immediately file a task
+task create "Command X fails when Y" --discovered-from=<current-task-id> --json
+
+# WRONG: Ignoring it and moving on
+# WRONG: Leaving a TODO comment
+# WRONG: Mentioning it but not filing a task
+```
+
+**Examples of bugs you MUST file:**
+- "Expected `--flag value` to work but only `--flag=value` works"
+- "Documentation says X but actual behavior is Y"
+- "Combining two flags causes parsing error"
+- "Feature is missing that would be useful"
+
+**CRITICAL: File bugs immediately when you discover them:**
+- If a command doesn't work as documented → create a task
+- If a command doesn't work as you expected → create a task
+- If behavior is inconsistent or confusing → create a task
+- If documentation is wrong or misleading → create a task
+- If you find yourself working around a limitation → create a task
+
+**NEVER leave TODO comments in code.** Create a task instead.
+
+**NEVER ignore bugs or unexpected behavior.** File a task for it immediately.
+
+### 3. Forbidden Patterns
+
+**Markdown checklist (NEVER do this):**
+```markdown
+❌ Wrong:
+- [ ] Refactor auth module
+- [ ] Add tests
+- [ ] Update docs
+
+✅ Correct:
+task create "Refactor auth module" -p 2 --json
+task create "Add tests for auth" -p 2 --json
+task create "Update auth docs" -p 3 --json
+```
+
+**todo_write tool (NEVER do this):**
+```
+❌ Wrong: todo_write({todos: [{content: "Fix bug", ...}]})
+✅ Correct: task create "Fix bug in parser" -p 1 --json
+```
+
+**Inline code comments (NEVER do this):**
+```python
+❌ Wrong:
+# TODO: write tests for this function
+# FIXME: handle edge case
+
+✅ Correct:
+# Create task instead:
+task create "Write tests for parse_config" -p 2 --namespace="Omni/Config" --json
+task create "Handle edge case in parser" -p 1 --discovered-from=<current-id> --json
+```
+
+### 4. Track Dependencies
+If work depends on other work, use `--deps`:
+```bash
+# Can't write tests until implementation is done
+task create "Test auth flow" --deps=t-20241108120000 --dep-type=blocks --json
+```
+
+### 5. Use Descriptive Titles
+Good: `"Add JWT token validation to auth middleware"`
+Bad: `"Fix auth"`
+
+### 6. Use Epics for Organization
+Organize related work using epics:
+- Create an epic for larger features: `task create "Feature Name" --type=epic --json`
+- Add tasks to the epic using `--parent=<epic-id>`
+- Use `--discovered-from` to track work found during implementation
+
+### 7. ALWAYS Store AI Planning Docs in `_/llm` Directory
+AI assistants often create planning and design documents during development:
+- PLAN.md, DESIGN.md, TESTING_GUIDE.md, tmp, and similar files
+- **You MUST use a dedicated directory for these ephemeral files**
+- Store ALL AI-generated planning/design docs in `_/llm`
+- The `_` directory is ignored by git and all of our temporary files related to the omnirepo go there
+- NEVER commit planning docs to the repo root
+
+## Dependency Rules
+
+- A task is **blocked** if any of its dependencies are not `done`
+- A task is **ready** if all its dependencies are `done` (or it has no dependencies)
+- `task ready` only shows tasks with status `open` or `in-progress` that are not blocked
+
+## File Structure
+
+```
+.tasks/
+├── tasks.jsonl # Git-tracked, production database
+├── tasks-test.jsonl # Test database (not tracked, auto-created)
+
+Omni/Ide/hooks/
+├── pre-commit # Exports tasks before commit (auto-stages tasks.jsonl)
+├── post-checkout # Imports tasks after branch switch
+└── ... # Other git hooks
+```
+
+Each line in `tasks.jsonl` is a JSON object representing a task.
+
+**Git Hooks**: This repository uses hooks from `Omni/Ide/hooks/` (configured via `core.hooksPath`). Do NOT add hooks to `.git/hooks/` - they won't be version controlled and may cause confusion.
+
+## Testing and Development
+
+**CRITICAL**: When manually testing task functionality (like tree visualization, flag ordering, etc.), you MUST use the test database:
+
+```bash
+# Set test mode to protect production database
+export TASK_TEST_MODE=1
+
+# Now all task operations use .tasks/tasks-test.jsonl
+task create "Test task" --type=task
+task list
+task tree
+
+# Unset when done
+unset TASK_TEST_MODE
+```
+
+**The test suite automatically uses test mode** - you don't need to set it manually when running `task test` or `bild --test Omni/Task.hs`.
+
+**NEVER run manual tests against the production database** (`.tasks/tasks.jsonl`). This pollutes it with test data that must be manually cleaned up. Always use `TASK_TEST_MODE=1` for experimentation.
+
+## Integration with Git
+
+The `.tasks/tasks.jsonl` file is git-tracked. When you:
+- Create/update tasks locally
+- Commit and push
+- Other machines/agents get the updates on `git pull`
+
+**Important**: Add to `.gitignore`:
+```
+.tasks/*.db
+.tasks/*.db-journal
+.tasks/*.sock
+```
+
+But **do** track:
+```
+!.tasks/
+!.tasks/tasks.jsonl
+```
+
+## Troubleshooting
+
+### "Task not found"
+- Check the task ID is correct with `task list`
+- Ensure you've run `task init`
+
+### "Database not initialized"
+Run: `task init`
+
+### Dependencies not working
+- Verify dependency IDs exist: `task list`
+- Check dependency tree: `task deps <id>`
+
+## Reinforcement: Critical Rules
+
+Remember these non-negotiable rules:
+
+- ✅ Use `task` for ALL task tracking (with `--json` flag)
+- ✅ Link discovered work with `--discovered-from` dependencies
+- ✅ File bugs IMMEDIATELY when you discover unexpected behavior
+- ✅ Check `task ready --json` before asking "what should I work on?"
+- ✅ Store AI planning docs in `_/llm` directory
+- ✅ Run `task sync` at end of every session (commits locally, does NOT push)
+- ❌ NEVER use `todo_write` tool
+- ❌ NEVER create markdown TODO lists or task checklists
+- ❌ NEVER put TODOs or FIXMEs in code comments
+- ❌ NEVER use external issue trackers
+- ❌ NEVER duplicate tracking systems
+- ❌ NEVER clutter repo root with planning documents
+
+**If you find yourself about to use todo_write or create a markdown checklist, STOP and use `task create` instead.**