← Back to task

Commit 901018e6

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