summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.tasks/race-test.jsonl11
-rw-r--r--.tasks/tasks.jsonl1
-rw-r--r--Omni/Task.hs32
-rw-r--r--Omni/Task/Core.hs61
4 files changed, 83 insertions, 22 deletions
diff --git a/.tasks/race-test.jsonl b/.tasks/race-test.jsonl
new file mode 100644
index 0000000..6a517fb
--- /dev/null
+++ b/.tasks/race-test.jsonl
@@ -0,0 +1,11 @@
+{"taskCreatedAt":"2025-11-22T11:31:38.184999267Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Parent Epic","taskType":"Epic","taskUpdatedAt":"2025-11-22T11:31:38.184999267Z"}
+{"taskCreatedAt":"2025-11-22T11:31:38.185327211Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P.1","taskNamespace":null,"taskParent":"t-rWcpxr83P","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 1","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:38.185327211Z"}
+{"taskCreatedAt":"2025-11-22T11:31:38.185727595Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P.2","taskNamespace":null,"taskParent":"t-rWcpxr83P","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 2","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:38.185727595Z"}
+{"taskCreatedAt":"2025-11-22T11:31:38.186141979Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P.3","taskNamespace":null,"taskParent":"t-rWcpxr83P","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 3","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:38.186141979Z"}
+{"taskCreatedAt":"2025-11-22T11:31:38.186656665Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P.4","taskNamespace":null,"taskParent":"t-rWcpxr83P","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 4","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:38.186656665Z"}
+{"taskCreatedAt":"2025-11-22T11:31:38.18720248Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P.5","taskNamespace":null,"taskParent":"t-rWcpxr83P","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 5","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:38.18720248Z"}
+{"taskCreatedAt":"2025-11-22T11:31:38.187861197Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P.6","taskNamespace":null,"taskParent":"t-rWcpxr83P","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 6","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:38.187861197Z"}
+{"taskCreatedAt":"2025-11-22T11:31:38.188661346Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P.7","taskNamespace":null,"taskParent":"t-rWcpxr83P","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 7","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:38.188661346Z"}
+{"taskCreatedAt":"2025-11-22T11:31:38.189755527Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P.8","taskNamespace":null,"taskParent":"t-rWcpxr83P","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 8","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:38.189755527Z"}
+{"taskCreatedAt":"2025-11-22T11:31:38.190654846Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P.9","taskNamespace":null,"taskParent":"t-rWcpxr83P","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 9","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:38.190654846Z"}
+{"taskCreatedAt":"2025-11-22T11:31:38.191575266Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpxr83P.10","taskNamespace":null,"taskParent":"t-rWcpxr83P","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 10","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:38.191575266Z"}
diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl
index c41960c..86534a0 100644
--- a/.tasks/tasks.jsonl
+++ b/.tasks/tasks.jsonl
@@ -170,6 +170,7 @@
{"taskCreatedAt":"2025-11-22T02:26:44.02456019Z","taskDependencies":[],"taskDescription":"Modify Omni/Agent/Git.hs to check for .git/rebase-merge or .git/rebase-apply before running git rebase --abort. This avoids blindly running abort commands.","taskId":"t-rWbPQPLps","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Detect in-progress rebase before aborting in Agent","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T02:27:45.377866012Z"}
{"taskCreatedAt":"2025-11-22T03:01:36.84628158Z","taskDependencies":[],"taskDescription":"Modify Omni/Agent/Worker.hs to check if the task branch already exists before trying to create it. If it exists, simply checkout the branch. This prevents 'fatal: a branch named ... already exists' errors when restarting the worker.","taskId":"t-rWbS8t1Wv","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Handle existing task branch in Worker Agent","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T03:02:31.746506652Z"}
{"taskCreatedAt":"2025-11-22T03:09:54.022974779Z","taskDependencies":[],"taskDescription":"Implement the 2-line status UI described in Omni/Agent/DESIGN.md (Section 4.3). It should reserve 2 lines at the bottom for Meta (Task ID, Time) and Activity (current thought/action), allowing history to scroll above. Use ANSI codes for cursor management.","taskId":"t-rWbSG78jq","taskNamespace":"Omni/Agent/Log.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement 2-line Agent Status UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T03:21:54.480763142Z"}
+{"taskCreatedAt":"2025-11-22T11:31:50.378377038Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpygi7d","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Test Lowercase","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:50.378377038Z"}
{"taskCreatedAt":"2025-11-22T11:34:17.854509264Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcpIf5ov","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"--help","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:39:43.304029721Z"}
{"taskCreatedAt":"2025-11-22T04:02:16.914288868Z","taskDependencies":[{"depId":"t-rWbMpcV4v","depType":"Blocks"},{"depId":"t-rWbMpxaBk","depType":"Blocks"},{"depId":"t-rWbS8t1Wv","depType":"Blocks"}],"taskDescription":"Update Omni/Agent/Worker.hs to spawn a background thread that tails '_/llm/amp.log' while the Amp agent is running. For each new line in the log: 1. Parse it (it's JSON). 2. Extract a user-friendly summary (e.g. 'Thinking...', 'Tool: Bash'). 3. Update the status bar activity line (AgentLog.updateActivity) with this summary. This provides real-time visibility into what the agent is doing.","taskId":"t-rWbW6OnUO","taskNamespace":"Omni/Agent/Worker.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Stream Amp logs to Agent status bar","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:05:14.217613978Z"}
{"taskCreatedAt":"2025-11-22T09:41:06.786529414Z","taskDependencies":[],"taskDescription":"Replace 'git rebase live' with 'git sync' (which maps to git-branchless sync) in Omni.Agent.Git.syncWithLive. This aligns with the branchless workflow and handles stack rebasing automatically.","taskId":"t-rWciiEsnZ","taskNamespace":"Omni/Agent/Git.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Use 'git sync' instead of 'git rebase' in Agent","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T09:42:37.875643446Z"}
diff --git a/Omni/Task.hs b/Omni/Task.hs
index 5008dd2..d5631f7 100644
--- a/Omni/Task.hs
+++ b/Omni/Task.hs
@@ -205,9 +205,9 @@ move args
| args `Cli.has` Cli.command "show" = do
tid <- getArgText args "id"
tasks <- loadTasks
- case filter (\t -> taskId t == tid) tasks of
- [] -> putText "Task not found"
- (task : _) ->
+ case findTask tid tasks of
+ Nothing -> putText "Task not found"
+ Just task ->
if isJsonMode args
then outputJson task
else showTaskDetailed task
@@ -414,7 +414,31 @@ unitTests =
Test.unit "namespace normalization handles .hs suffix" <| do
let ns = "Omni/Task.hs"
validNs = Namespace.fromHaskellModule ns
- Namespace.toPath validNs Test.@?= "Omni/Task.hs"
+ Namespace.toPath validNs Test.@?= "Omni/Task.hs",
+ Test.unit "generated IDs are lowercase" <| do
+ task <- createTask "Lowercase check" WorkTask Nothing Nothing P2 [] Nothing
+ let tid = taskId task
+ tid Test.@?= T.toLower tid
+ -- check it matches regex for base36 (t-[0-9a-z]+)
+ let isLowerBase36 = T.all (\c -> c `elem` ['0' .. '9'] ++ ['a' .. 'z'] || c == 't' || c == '-') tid
+ isLowerBase36 Test.@?= True,
+ Test.unit "dependencies are case insensitive" <| do
+ task1 <- createTask "Blocker" WorkTask Nothing Nothing P2 [] Nothing
+ let tid1 = taskId task1
+ -- Use uppercase ID for dependency
+ upperTid1 = T.toUpper tid1
+ dep = Dependency {depId = upperTid1, depType = Blocks}
+ task2 <- createTask "Blocked" WorkTask Nothing Nothing P2 [dep] Nothing
+
+ -- task1 is Open, so task2 should NOT be ready
+ ready <- getReadyTasks
+ (taskId task2 `notElem` map taskId ready) Test.@?= True
+
+ updateTaskStatus tid1 Done
+
+ -- task2 should now be ready because dependency check normalizes IDs
+ ready2 <- getReadyTasks
+ (taskId task2 `elem` map taskId ready2) Test.@?= True
]
-- | Test CLI argument parsing to ensure docopt string matches actual usage
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index 9e4d2b4..29aca85 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -96,11 +96,32 @@ 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)
+ where
+ -- Helper to check if IDs match
+ -- matchesId is already defined globally but we can use it directly
+ -- The lambda above is fine.
+ () = ()
+
+-- | 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
@@ -188,7 +209,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 +218,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 +241,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,7 +340,10 @@ 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
+ let parent' = fmap normalizeId parent
+ deps' = map normalizeDependency deps
+
+ tid <- case parent' of
Nothing -> generateId
Just pid -> do
tasks <- loadTasksInternal
@@ -327,14 +351,14 @@ createTask title taskType parent namespace priority deps description =
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
@@ -415,12 +439,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
@@ -815,7 +840,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