commit 901018e6489880f14f57ffe97aea7e0b00c1173a
Author: Ben Sima <ben@bensima.com>
Date: Wed Dec 31 12:18:20 2025
Fix task namespace parsing for non-.hs extensions
Add fromRelPath to parse paths like 'Omni/Ide/pi-review.sh' directly.
Add normalizeNamespace helper that tries fromRelPath first, falling
back to fromHaskellModule for module-style names like 'Omni.Task'.
Previously 'Omni/Ide/pi-review.sh' was mangled to 'Omni/Ide/pi-review/sh.hs'
because fromHaskellModule treated the dot as a module separator.
Task-Id: t-300
diff --git a/Omni/Namespace.hs b/Omni/Namespace.hs
index ddbd1f19..c0efa640 100644
--- a/Omni/Namespace.hs
+++ b/Omni/Namespace.hs
@@ -8,6 +8,7 @@ module Omni.Namespace
( Namespace (..),
Ext (..),
fromPath,
+ fromRelPath,
toPath,
toModule,
fromHaskellContent,
@@ -62,6 +63,11 @@ fromPath coderoot absPath =
+> List.stripPrefix "/"
+> Regex.match (Namespace </ rePath <* dot <*> reExt)
+-- | Parse a relative file path like "Omni/Ide/pi-review.sh" into a Namespace.
+-- Returns Nothing if the extension is not recognized.
+fromRelPath :: String -> Maybe Namespace
+fromRelPath = Regex.match (Namespace </ rePath <* dot <*> reExt)
+
toPath :: Namespace -> FilePath
toPath (Namespace parts ext) =
joinWith "/" parts <> toExt ext
diff --git a/Omni/Task.hs b/Omni/Task.hs
index b5cbb75d..5df28480 100644
--- a/Omni/Task.hs
+++ b/Omni/Task.hs
@@ -127,6 +127,15 @@ outputJson val = BLC.putStrLn <| Aeson.encode val
outputSuccess :: Text -> IO ()
outputSuccess msg = outputJson <| Aeson.object ["success" Aeson..= True, "message" Aeson..= msg]
+-- | Normalize a namespace string. Handles paths with extensions (.hs, .sh, .py, etc)
+-- or Haskell module names (Omni.Task). Returns the normalized path form.
+normalizeNamespace :: String -> Text
+normalizeNamespace ns = case Namespace.fromRelPath ns of
+ Just validNs -> T.pack <| Namespace.toPath validNs
+ Nothing ->
+ -- Fall back to treating as Haskell module name (e.g., "Omni.Task")
+ T.pack <| Namespace.toPath (Namespace.fromHaskellModule ns)
+
move :: Cli.Arguments -> IO ()
move args = do
-- Handle --db flag globally
@@ -191,11 +200,7 @@ move' args
namespace <- case Cli.getArg args (Cli.longOption "namespace") of
Nothing -> pure Nothing
- Just ns -> do
- -- Validate it's a proper namespace by parsing it
- let validNs = Namespace.fromHaskellModule ns
- nsPath = T.pack <| Namespace.toPath validNs
- pure <| Just nsPath
+ Just ns -> pure <| Just (normalizeNamespace ns)
description <- case Cli.getArg args (Cli.longOption "description") of
Nothing -> panic "--description is required for task create"
@@ -239,10 +244,7 @@ move' args
Just other -> panic <| "Invalid status: " <> T.pack other <> ". Use: draft, open, in-progress, review, or done"
maybeNamespace <- case Cli.getArg args (Cli.longOption "namespace") of
Nothing -> pure Nothing
- Just ns -> do
- let validNs = Namespace.fromHaskellModule ns
- nsPath = T.pack <| Namespace.toPath validNs
- pure <| Just nsPath
+ Just ns -> pure <| Just (normalizeNamespace ns)
maybeDesc <- pure <| fmap T.pack (Cli.getArg args (Cli.longOption "description"))
maybeDeps <- case Cli.getArg args (Cli.longOption "discovered-from") of
@@ -311,10 +313,7 @@ move' args
Just other -> panic <| "Invalid status: " <> T.pack other <> ". Use: draft, open, in-progress, review, approved, done, or needs-help"
maybeNamespace <- case Cli.getArg args (Cli.longOption "namespace") of
Nothing -> pure Nothing
- Just ns -> do
- let validNs = Namespace.fromHaskellModule ns
- nsPath = T.pack <| Namespace.toPath validNs
- pure <| Just nsPath
+ Just ns -> pure <| Just (normalizeNamespace ns)
tasks <- listTasks maybeType maybeParent maybeStatus maybeNamespace
if isJsonMode args
then outputJson tasks
@@ -769,8 +768,13 @@ unitTests =
Nothing -> Test.assertFailure "Could not find task with upper case ID",
Test.unit "namespace normalization handles .hs suffix" <| do
let ns = "Omni/Task.hs"
- validNs = Namespace.fromHaskellModule ns
- Namespace.toPath validNs Test.@?= "Omni/Task.hs",
+ normalizeNamespace ns Test.@?= "Omni/Task.hs",
+ Test.unit "namespace normalization handles .sh suffix" <| do
+ let ns = "Omni/Ide/pi-review.sh"
+ normalizeNamespace ns Test.@?= "Omni/Ide/pi-review.sh",
+ Test.unit "namespace normalization handles .py suffix" <| do
+ let ns = "Biz/Cloud/Api.py"
+ normalizeNamespace ns Test.@?= "Biz/Cloud/Api.py",
Test.unit "generated IDs are lowercase" <| do
task <- createTask "Lowercase check" WorkTask Nothing Nothing P2 Nothing [] "Lowercase description"
let tid = taskId task