diff options
Diffstat (limited to 'Omni/Task')
| -rw-r--r-- | Omni/Task/Core.hs | 106 | ||||
| -rw-r--r-- | Omni/Task/README.md | 415 |
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.** |
