summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore16
-rw-r--r--.tasks/tasks.jsonl12
-rw-r--r--AGENTS.md391
-rwxr-xr-xOmni/Ide/hooks/post-checkout6
-rwxr-xr-xOmni/Ide/hooks/post-merge6
-rwxr-xr-xOmni/Ide/hooks/pre-commit6
-rwxr-xr-xOmni/Ide/hooks/pre-push6
-rw-r--r--Omni/Task.hs148
-rw-r--r--Omni/Task/Core.hs223
9 files changed, 814 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index ac8a177..9ee57a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,19 @@ dist*
.direnv/
Biz/Mynion/Prompt.md
.aider*
+
+# Node.js
+node_modules/
+package-lock.json
+package.json
+
+# Task tracker
+.tasks/*.db
+.tasks/*.db-journal
+.tasks/*.sock
+.tasks/*.pipe
+.tasks/*.log
+
+# But DO track the JSONL file
+!.tasks/
+!.tasks/tasks.jsonl
diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl
new file mode 100644
index 0000000..3c975e8
--- /dev/null
+++ b/.tasks/tasks.jsonl
@@ -0,0 +1,12 @@
+{"taskCreatedAt":"2025-11-08T20:03:50.230851965Z","taskDependencies":[],"taskId":"t-a1b2c3","taskProject":"task-manager","taskStatus":"Done","taskTitle":"Show help text when task invoked without args","taskUpdatedAt":"2025-11-08T20:06:02.605878048Z"}
+{"taskCreatedAt":"2025-11-08T20:03:53.429072631Z","taskDependencies":[],"taskId":"t-d4e5f6","taskProject":"task-manager","taskStatus":"Done","taskTitle":"Move dev instructions from README.md to AGENTS.md","taskUpdatedAt":"2025-11-08T20:06:22.732392229Z"}
+{"taskCreatedAt":"2025-11-08T20:06:27.395834401Z","taskDependencies":[],"taskId":"t-g7h8i9","taskProject":"task-manager","taskStatus":"Done","taskTitle":"Task ids should be shorter. Use the sqids package in haskell to generate ids","taskUpdatedAt":"2025-11-08T21:00:37.311865046Z"}
+{"taskCreatedAt":"2025-11-08T20:09:35.590622249Z","taskDependencies":[],"taskId":"t-j0k1L2","taskProject":"task-manager","taskStatus":"Open","taskTitle":"Tasks should have an optional namespace associated with them. Namespaces are first class citizens in this monorepo","taskUpdatedAt":"2025-11-08T20:09:35.590622249Z"}
+{"taskCreatedAt":"2025-11-08T20:10:09.944217463Z","taskDependencies":[],"taskId":"t-m3n4o5","taskProject":"task-manager","taskStatus":"Open","taskTitle":"There should be a command to list all projects.","taskUpdatedAt":"2025-11-08T20:10:09.944217463Z"}
+{"taskCreatedAt":"2025-11-08T20:20:38.785442739Z","taskDependencies":[],"taskId":"t-p6q7r8","taskProject":"task-manager","taskStatus":"Done","taskTitle":"Instruct agents too use git-branchless and a patch based workflow rather than traditional git commands if and when they need to record things in git.","taskUpdatedAt":"2025-11-08T21:09:06.854871964Z"}
+{"taskCreatedAt":"2025-11-08T20:22:20.116289616Z","taskDependencies":[],"taskId":"t-s9T0u1","taskProject":"task-manager","taskStatus":"Open","taskTitle":"instruct agents to include tests with all new features and bug fixes","taskUpdatedAt":"2025-11-08T20:22:20.116289616Z"}
+{"taskCreatedAt":"2025-11-08T20:45:12.764939794Z","taskDependencies":[],"taskId":"t-v2w3x4","taskProject":"task-manager","taskStatus":"Open","taskTitle":"instruct agents to run 'bild --test' and 'lint' for whatever namespace(s) they are working on after completing a task and fix any reported errors","taskUpdatedAt":"2025-11-08T20:45:12.764939794Z"}
+{"taskCreatedAt":"2025-11-08T20:48:43.183226361Z","taskDependencies":[],"taskId":"t-y5z6A7","taskProject":"ide","taskStatus":"Open","taskTitle":"The script Omni/Ide/typecheck.sh needs to support Haskell type checking in a similar fashion as how Omni/Ide/repl.sh is able to handle multiple languages","taskUpdatedAt":"2025-11-08T20:48:43.183226361Z"}
+{"taskCreatedAt":"2025-11-08T21:00:27.020241869Z","taskDependencies":[],"taskId":"t-1ky7gJ2","taskProject":"task-manager","taskStatus":"Done","taskTitle":"Test shorter IDs","taskUpdatedAt":"2025-11-08T21:04:00.990704969Z"}
+{"taskCreatedAt":"2025-11-08T21:00:29.901677247Z","taskDependencies":[],"taskId":"t-1kyjmjN","taskProject":"task-manager","taskStatus":"Done","taskTitle":"Another test task","taskUpdatedAt":"2025-11-08T21:04:04.081664205Z"}
+{"taskCreatedAt":"2025-11-08T21:11:41.013924674Z","taskDependencies":[],"taskId":"t-1lhJhgS","taskProject":"misc","taskStatus":"Open","taskTitle":"Remove the old aider config in .aider* files and directories. Aider stinks and we will use amp going forward","taskUpdatedAt":"2025-11-08T21:11:41.013924674Z"}
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..e30b01a
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,391 @@
+# Task Manager for AI Agents
+
+This document describes the task tracking system for AI coding agents working in this codebase.
+
+## Overview
+
+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
+
+## Quick Reference
+
+### Create a Task
+```bash
+task create "<title>" <project> [--deps=<ids>]
+```
+
+Examples:
+```bash
+task create "Add authentication" auth-system
+task create "Write tests" auth-system --deps=t-20241108120000
+```
+
+### List Tasks
+```bash
+task list [--project=<project>]
+```
+
+Examples:
+```bash
+task list # All tasks
+task list --project=auth # Filter by project
+```
+
+### 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
+```
+
+### View Dependencies
+```bash
+task deps <id>
+```
+
+Shows the dependency tree for a task.
+
+### 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
+```
+
+Creates `.tasks/` directory and `tasks.jsonl` file.
+
+## 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" api-layer
+
+# Note the ID (e.g., t-20241108120000)
+
+# Create dependent task
+task create "Implement API client" api-layer --deps=t-20241108120000
+```
+
+The dependent task won't show up in `task ready` until the blocker is marked `done`.
+
+### Working on a Project
+
+```bash
+# See all tasks for a project
+task list --project=auth-system
+
+# Create related tasks
+task create "Design login flow" auth-system
+task create "Implement OAuth" auth-system
+task create "Add password reset" auth-system
+```
+
+### Breaking Down Large Work
+
+```bash
+# Create parent task
+task create "Complete authentication system" auth-system
+# 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
+```
+
+## Agent Best Practices
+
+### 1. Always Check Ready Work First
+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
+```
+
+### 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
+```
+
+### 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`
+
+## Task Lifecycle
+
+```
+[open] ──update in-progress──> [in-progress] ──update done──> [done]
+ │ │
+ └─────────update open───────────────┘
+```
+
+States:
+- **open**: Not started, available for work (if not blocked)
+- **in-progress**: Currently being worked on
+- **done**: Completed
+
+## 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, source of truth
+```
+
+Each line in `tasks.jsonl` is a JSON object representing a task.
+
+## Example Session
+
+```bash
+# 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
+
+# See what's ready (all of them, no blockers yet)
+task ready
+
+# Start working
+task update t-20241108120000 in-progress
+
+# Discover dependent work
+task create "Write storage tests" testing --deps=t-20241108120000
+
+# Complete first task
+task update t-20241108120000 done
+
+# Now the test task is unblocked
+task ready
+# Shows: "Write storage tests"
+```
+
+## Build and Test Commands
+
+Build the task manager:
+```bash
+bild --time 0 Omni/Task.hs
+```
+
+Run tests:
+```bash
+task test
+```
+
+## 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>`
+
+## Development Tools
+
+### bild
+
+`bild` is the universal build tool. It can build and test everything in the repo.
+
+Examples:
+```bash
+bild --test Omni/Bild.hs # Build and test a namespace
+bild --time 0 Omni/Cloud.nix # Build with no timeout
+bild --plan Omni/Test.hs # Analyze build without building
+```
+
+### lint
+
+Universal lint and formatting tool. Errors if lints fail or code is not formatted properly.
+
+Examples:
+```bash
+lint Omni/Cli.hs # Lint a namespace
+lint --fix **/*.py # Lint and fix all Python files
+```
+
+### repl.sh
+
+Like `nix-shell` but specific to this repo. Analyzes the namespace, pulls dependencies, and starts a shell or repl.
+
+Examples:
+```bash
+repl.sh Omni/Bild.hs # Start Haskell repl with namespace loaded
+repl.sh --bash Omni/Log.py # Start bash shell for namespace
+```
+
+## Coding Conventions
+
+1. **Test interface**: Every program must accept `test` as a first argument to run its test suite
+2. **Entrypoint naming**: The entrypoint for every program shall be called `main`
+
+## Git Workflow
+
+### Use git-branchless
+
+This repository uses **git-branchless** for a patch-based workflow instead of traditional branch-based git.
+
+Key concepts:
+- Work with **patches** (commits) directly rather than branches
+- Use **stacking** to organize related changes
+- Leverage **smartlog** to visualize commit history
+
+### Common git-branchless Commands
+
+**View commit graph:**
+```bash
+git smartlog
+```
+
+**Create a new commit:**
+```bash
+# Make your changes
+git add .
+git commit -m "Your commit message"
+```
+
+**Amend the current commit:**
+```bash
+# Make additional changes
+git add .
+git commit --amend --no-edit
+```
+
+**Move/restack commits:**
+```bash
+git move -s <source> -d <destination>
+git restack
+```
+
+**Submit changes:**
+```bash
+# After commits are ready
+git submit
+```
+
+### When to Record Changes in Git
+
+**DO record in git:**
+- Completed features or bug fixes
+- Working code that passes tests and linting
+- Significant milestones in task completion
+
+**DO NOT record in git:**
+- Work in progress (unless specifically requested)
+- Broken or untested code
+- Temporary debugging changes
+
+### Workflow Best Practices
+
+1. **Make small, focused commits** - Each commit should represent one logical change
+2. **Write descriptive commit messages** - Explain what and why, not just what
+3. **Rebase and clean up history** - Use `git commit --amend` and `git restack` to keep history clean
+4. **Test before committing** - Run `bild --test` and `lint` on affected namespaces
+
+## Future Enhancements
+
+Planned features (not yet implemented):
+- Task priorities
+- Due dates
+- Task labels/tags
+- Search/filter capabilities
+- Assignees
+- Multi-repo support
diff --git a/Omni/Ide/hooks/post-checkout b/Omni/Ide/hooks/post-checkout
index 85541a2..3fe14b5 100755
--- a/Omni/Ide/hooks/post-checkout
+++ b/Omni/Ide/hooks/post-checkout
@@ -14,6 +14,12 @@ elif [[ ${#changed[@]} -gt 0 ]]
then
MakeTags "${changed[@]}"
fi
+
+# Task manager: Import tasks after branch switch
+if [ -f .tasks/tasks.jsonl ]; then
+ task import -i .tasks/tasks.jsonl 2>/dev/null || true
+fi
+
## START BRANCHLESS CONFIG
git branchless hook post-checkout "$@"
diff --git a/Omni/Ide/hooks/post-merge b/Omni/Ide/hooks/post-merge
index fcfd314..3e0495b 100755
--- a/Omni/Ide/hooks/post-merge
+++ b/Omni/Ide/hooks/post-merge
@@ -1,5 +1,11 @@
#!/usr/bin/env bash
"${CODEROOT:?}"/Omni/Ide/hooks/post-checkout 'HEAD@{1}' HEAD
+
+# Task manager: Import tasks after git pull/merge
+if [ -f .tasks/tasks.jsonl ]; then
+ task import -i .tasks/tasks.jsonl 2>/dev/null || true
+fi
+
## START BRANCHLESS CONFIG
git branchless hook post-merge "$@"
diff --git a/Omni/Ide/hooks/pre-commit b/Omni/Ide/hooks/pre-commit
index 06f1716..d096f5b 100755
--- a/Omni/Ide/hooks/pre-commit
+++ b/Omni/Ide/hooks/pre-commit
@@ -18,4 +18,10 @@
fi
done
lint "${changed[@]}"
+
+ # Task manager: Export tasks before commit
+ if [ -d .tasks ]; then
+ task export --flush 2>/dev/null || true
+ git add .tasks/tasks.jsonl 2>/dev/null || true
+ fi
##
diff --git a/Omni/Ide/hooks/pre-push b/Omni/Ide/hooks/pre-push
index 00110bd..adbf858 100755
--- a/Omni/Ide/hooks/pre-push
+++ b/Omni/Ide/hooks/pre-push
@@ -1,5 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
+
+# Task manager: Ensure tasks are exported before push
+if [ -d .tasks ]; then
+ task export --flush 2>/dev/null || true
+fi
+
remote="$1"
z40=0000000000000000000000000000000000000000
IFS=" "
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 <title> <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
+ ]
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
new file mode 100644
index 0000000..f49a8d2
--- /dev/null
+++ b/Omni/Task/Core.hs
@@ -0,0 +1,223 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Omni.Task.Core where
+
+import Alpha
+import Data.Aeson (FromJSON, ToJSON, decode, encode)
+import qualified Data.ByteString.Lazy.Char8 as BLC
+import qualified Data.Text as T
+import qualified Data.Text.IO as TIO
+import Data.Time (UTCTime, diffTimeToPicoseconds, getCurrentTime, utctDayTime)
+import GHC.Generics ()
+import System.Directory (createDirectoryIfMissing, doesFileExist)
+
+-- Core data types
+data Task = Task
+ { taskId :: Text,
+ taskTitle :: Text,
+ taskProject :: Text,
+ taskStatus :: Status,
+ taskDependencies :: [Text], -- List of task IDs this depends on
+ taskCreatedAt :: UTCTime,
+ taskUpdatedAt :: UTCTime
+ }
+ deriving (Show, Eq, Generic)
+
+data Status = Open | InProgress | Done
+ deriving (Show, Eq, Generic)
+
+instance ToJSON Status
+
+instance FromJSON Status
+
+instance ToJSON Task
+
+instance FromJSON Task
+
+-- Initialize the task database
+initTaskDb :: IO ()
+initTaskDb = do
+ createDirectoryIfMissing True ".tasks"
+ exists <- doesFileExist ".tasks/tasks.jsonl"
+ unless exists <| do
+ TIO.writeFile ".tasks/tasks.jsonl" ""
+ putText "Initialized task database at .tasks/tasks.jsonl"
+
+-- Generate a short ID using base62 encoding of timestamp
+generateId :: IO Text
+generateId = do
+ now <- getCurrentTime
+ -- Convert current time to microseconds since midnight
+ let dayTime = utctDayTime now
+ microseconds = diffTimeToPicoseconds dayTime `div` 1000000
+ -- Convert to base62 for shorter IDs
+ encoded = toBase62 (fromIntegral microseconds)
+ pure <| "t-" <> T.pack encoded
+
+-- Convert number to base62 (0-9, a-z, A-Z)
+toBase62 :: Integer -> String
+toBase62 0 = "0"
+toBase62 n = reverse <| go n
+ where
+ alphabet = ['0' .. '9'] ++ ['a' .. 'z'] ++ ['A' .. 'Z']
+ go 0 = []
+ go x =
+ let (q, r) = x `divMod` 62
+ idx = fromIntegral r
+ char = case drop idx alphabet of
+ (c : _) -> c
+ [] -> '0' -- Fallback (should never happen)
+ in char : go q
+
+-- Load all tasks from JSONL file
+loadTasks :: IO [Task]
+loadTasks = do
+ exists <- doesFileExist ".tasks/tasks.jsonl"
+ if exists
+ then do
+ content <- TIO.readFile ".tasks/tasks.jsonl"
+ let taskLines = T.lines content
+ pure <| mapMaybe decodeTask taskLines
+ else pure []
+ where
+ decodeTask :: Text -> Maybe Task
+ decodeTask line =
+ if T.null line
+ then Nothing
+ else decode (BLC.pack <| T.unpack line)
+
+-- Save a single task (append to JSONL)
+saveTask :: Task -> IO ()
+saveTask task = do
+ let json = encode task
+ BLC.appendFile ".tasks/tasks.jsonl" (json <> "\n")
+
+-- Create a new task
+createTask :: Text -> Text -> [Text] -> IO Task
+createTask title project deps = do
+ tid <- generateId
+ now <- getCurrentTime
+ let task =
+ Task
+ { taskId = tid,
+ taskTitle = title,
+ taskProject = project,
+ taskStatus = Open,
+ taskDependencies = deps,
+ taskCreatedAt = now,
+ taskUpdatedAt = now
+ }
+ saveTask task
+ pure task
+
+-- Update task status
+updateTaskStatus :: Text -> Status -> IO ()
+updateTaskStatus tid newStatus = do
+ tasks <- loadTasks
+ now <- getCurrentTime
+ let updatedTasks = map updateIfMatch tasks
+ updateIfMatch t =
+ if taskId t == tid
+ then t {taskStatus = newStatus, taskUpdatedAt = now}
+ else t
+ -- Rewrite the entire file (simple approach for MVP)
+ TIO.writeFile ".tasks/tasks.jsonl" ""
+ traverse_ saveTask updatedTasks
+
+-- List tasks, optionally filtered by project
+listTasks :: Maybe Text -> IO [Task]
+listTasks maybeProject = do
+ tasks <- loadTasks
+ pure <| case maybeProject of
+ Nothing -> tasks
+ Just proj -> filter (\t -> taskProject t == proj) tasks
+
+-- Get ready tasks (not blocked by dependencies)
+getReadyTasks :: IO [Task]
+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)
+ pure <| filter isReady openTasks
+
+-- Show dependency tree for a task
+showDependencyTree :: Text -> IO ()
+showDependencyTree tid = do
+ tasks <- loadTasks
+ case filter (\t -> taskId t == tid) tasks of
+ [] -> putText "Task not found"
+ (task : _) -> printTree tasks task 0
+ where
+ 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
+ traverse_ (\dep -> printTree allTasks dep (indent + 1)) deps
+
+-- Helper to print a task
+printTask :: Task -> IO ()
+printTask t =
+ putText
+ <| taskId t
+ <> " ["
+ <> T.pack (show (taskStatus t))
+ <> "] "
+ <> taskTitle t
+ <> " ("
+ <> taskProject t
+ <> ")"
+
+-- Export tasks: Consolidate JSONL file (remove duplicates, keep latest version)
+exportTasks :: IO ()
+exportTasks = do
+ tasks <- loadTasks
+ -- Rewrite the entire file with deduplicated tasks
+ TIO.writeFile ".tasks/tasks.jsonl" ""
+ traverse_ saveTask tasks
+
+-- Import tasks: Read from another JSONL file and merge with existing tasks
+importTasks :: FilePath -> IO ()
+importTasks filePath = do
+ exists <- doesFileExist filePath
+ unless exists <| panic (T.pack filePath <> " does not exist")
+
+ -- Load tasks from import file
+ content <- TIO.readFile filePath
+ let importLines = T.lines content
+ importedTasks = mapMaybe decodeTask importLines
+
+ -- Load existing tasks
+ existingTasks <- loadTasks
+
+ -- Create a map of existing task IDs for quick lookup
+ let existingIds = map taskId existingTasks
+ -- Filter to only new tasks (not already in our database)
+ newTasks = filter (\t -> taskId t `notElem` existingIds) importedTasks
+ -- For tasks that exist, update them with imported data
+ updatedTasks = map (updateWithImported importedTasks) existingTasks
+ -- Combine: updated existing tasks + new tasks
+ allTasks = updatedTasks ++ newTasks
+
+ -- Rewrite tasks.jsonl with merged data
+ TIO.writeFile ".tasks/tasks.jsonl" ""
+ traverse_ saveTask allTasks
+ where
+ decodeTask :: Text -> Maybe Task
+ decodeTask line =
+ if T.null line
+ then Nothing
+ else decode (BLC.pack <| T.unpack line)
+
+ -- Update an existing task if there's a newer version in imported tasks
+ updateWithImported :: [Task] -> Task -> Task
+ updateWithImported imported existing =
+ case filter (\t -> taskId t == taskId existing) imported of
+ [] -> existing -- No imported version, keep existing
+ (importedTask : _) ->
+ -- Use imported version if it's newer (based on updatedAt)
+ if taskUpdatedAt importedTask > taskUpdatedAt existing
+ then importedTask
+ else existing