← Back to task

Commit 375cf189

commit 375cf189a94dd9c191ed17c066a8cf0c56bd3e7c
Author: Ben Sima <ben@bensima.com>
Date:   Fri Nov 28 01:50:40 2025

    Implement jr facts list/show/add/delete CLI commands
    
    The build and tests pass. The hlint suggestions have been fixed:
    1. Changed `Text.pack <$>` to `Text.pack </` on line 528 2. Replaced
    the case expression with `maybe Fact.getAllFacts Fact.getFac 3. Changed
    `Text.pack <$>` to `Text.pack </` on line 555
    
    Task-Id: t-158.3

diff --git a/Omni/Jr.hs b/Omni/Jr.hs
index 9e33ab5e..69c395be 100755
--- a/Omni/Jr.hs
+++ b/Omni/Jr.hs
@@ -12,11 +12,14 @@
 module Omni.Jr where
 
 import Alpha
+import qualified Data.Aeson as Aeson
+import qualified Data.ByteString.Lazy.Char8 as BLC
 import qualified Data.List as List
 import qualified Data.Text as Text
 import qualified Omni.Agent.Core as AgentCore
 import qualified Omni.Agent.Worker as AgentWorker
 import qualified Omni.Cli as Cli
+import qualified Omni.Fact as Fact
 import qualified Omni.Jr.Web as Web
 import qualified Omni.Task as Task
 import qualified Omni.Task.Core as TaskCore
@@ -52,6 +55,10 @@ Usage:
   jr web [--port=PORT]
   jr review [<task-id>] [--auto]
   jr loop [--delay=SECONDS]
+  jr facts list [--project=PROJECT] [--json]
+  jr facts show <fact-id> [--json]
+  jr facts add <project> <content> [--files=FILES] [--task=TASK] [--confidence=CONF] [--json]
+  jr facts delete <fact-id> [--json]
   jr test
   jr (-h | --help)
 
@@ -61,12 +68,18 @@ Commands:
   web           Start the web UI server
   review        Review a completed task (show diff, accept/reject)
   loop          Run autonomous work+review loop
+  facts         Manage knowledge base facts
 
 Options:
-  -h --help        Show this help
-  --port=PORT      Port for web server [default: 8080]
-  --auto           Auto-review: accept if tests pass, reject if they fail
-  --delay=SECONDS  Delay between loop iterations [default: 5]
+  -h --help           Show this help
+  --port=PORT         Port for web server [default: 8080]
+  --auto              Auto-review: accept if tests pass, reject if they fail
+  --delay=SECONDS     Delay between loop iterations [default: 5]
+  --project=PROJECT   Filter facts by project
+  --files=FILES       Comma-separated list of related files
+  --task=TASK         Source task ID
+  --confidence=CONF   Confidence level 0.0-1.0 [default: 0.8]
+  --json              Output in JSON format
 |]
 
 move :: Cli.Arguments -> IO ()
@@ -115,6 +128,7 @@ move args
             Just d -> fromMaybe 5 (readMaybe d)
             Nothing -> 5
       runLoop delay
+  | args `Cli.has` Cli.command "facts" = handleFacts args
   | otherwise = putText (str <| Docopt.usage help)
 
 -- | Run the autonomous loop: work -> review -> repeat
@@ -507,6 +521,79 @@ checkEpicCompletion task =
   where
     hasParent pid t = maybe False (TaskCore.matchesId pid) (TaskCore.taskParent t)
 
+-- | Handle facts subcommands
+handleFacts :: Cli.Arguments -> IO ()
+handleFacts args
+  | args `Cli.has` Cli.command "list" = do
+      let maybeProject = Text.pack </ Cli.getArg args (Cli.longOption "project")
+          jsonMode = args `Cli.has` Cli.longOption "json"
+      facts <- maybe Fact.getAllFacts Fact.getFactsByProject maybeProject
+      if jsonMode
+        then BLC.putStrLn (Aeson.encode facts)
+        else traverse_ printFact facts
+  | args `Cli.has` Cli.command "show" = do
+      let jsonMode = args `Cli.has` Cli.longOption "json"
+      case Cli.getArg args (Cli.argument "fact-id") of
+        Nothing -> putText "fact-id required"
+        Just fidStr -> case readMaybe fidStr of
+          Nothing -> putText "Invalid fact ID (must be integer)"
+          Just fid -> do
+            maybeFact <- Fact.getFact fid
+            case maybeFact of
+              Nothing -> putText "Fact not found"
+              Just fact ->
+                if jsonMode
+                  then BLC.putStrLn (Aeson.encode fact)
+                  else printFactDetailed fact
+  | args `Cli.has` Cli.command "add" = do
+      let jsonMode = args `Cli.has` Cli.longOption "json"
+      case (Cli.getArg args (Cli.argument "project"), Cli.getArg args (Cli.argument "content")) of
+        (Just proj, Just content) -> do
+          let files = case Cli.getArg args (Cli.longOption "files") of
+                Just f -> Text.splitOn "," (Text.pack f)
+                Nothing -> []
+              sourceTask = Text.pack </ Cli.getArg args (Cli.longOption "task")
+              confidence = case Cli.getArg args (Cli.longOption "confidence") of
+                Just c -> fromMaybe 0.8 (readMaybe c)
+                Nothing -> 0.8
+          factId <- Fact.createFact (Text.pack proj) (Text.pack content) files sourceTask confidence
+          if jsonMode
+            then BLC.putStrLn (Aeson.encode (Aeson.object ["id" Aeson..= factId, "success" Aeson..= True]))
+            else putText ("Created fact: " <> tshow factId)
+        _ -> putText "project and content required"
+  | args `Cli.has` Cli.command "delete" = do
+      let jsonMode = args `Cli.has` Cli.longOption "json"
+      case Cli.getArg args (Cli.argument "fact-id") of
+        Nothing -> putText "fact-id required"
+        Just fidStr -> case readMaybe fidStr of
+          Nothing -> putText "Invalid fact ID (must be integer)"
+          Just fid -> do
+            Fact.deleteFact fid
+            if jsonMode
+              then BLC.putStrLn (Aeson.encode (Aeson.object ["success" Aeson..= True, "message" Aeson..= ("Deleted fact " <> tshow fid)]))
+              else putText ("Deleted fact: " <> tshow fid)
+  | otherwise = putText "Unknown facts subcommand. Use: list, show, add, or delete"
+
+-- | Print a fact in a compact format
+printFact :: TaskCore.Fact -> IO ()
+printFact fact = do
+  let fid = maybe "?" tshow (TaskCore.factId fact)
+      proj = TaskCore.factProject fact
+      content = Text.take 60 (TaskCore.factContent fact)
+      suffix = if Text.length (TaskCore.factContent fact) > 60 then "..." else ""
+  putText (fid <> "\t" <> proj <> "\t" <> content <> suffix)
+
+-- | Print a fact in detailed format
+printFactDetailed :: TaskCore.Fact -> IO ()
+printFactDetailed fact = do
+  putText ("ID:         " <> maybe "?" tshow (TaskCore.factId fact))
+  putText ("Project:    " <> TaskCore.factProject fact)
+  putText ("Content:    " <> TaskCore.factContent fact)
+  putText ("Files:      " <> Text.intercalate ", " (TaskCore.factRelatedFiles fact))
+  putText ("Source:     " <> fromMaybe "-" (TaskCore.factSourceTask fact))
+  putText ("Confidence: " <> tshow (TaskCore.factConfidence fact))
+  putText ("Created:    " <> tshow (TaskCore.factCreatedAt fact))
+
 test :: Test.Tree
 test =
   Test.group
@@ -535,5 +622,63 @@ test =
           Left err -> Test.assertFailure <| "Failed to parse 'work t-123': " <> show err
           Right args -> do
             args `Cli.has` Cli.command "work" Test.@?= True
-            Cli.getArg args (Cli.argument "task-id") Test.@?= Just "t-123"
+            Cli.getArg args (Cli.argument "task-id") Test.@?= Just "t-123",
+      Test.unit "can parse facts list command" <| do
+        let result = Docopt.parseArgs help ["facts", "list"]
+        case result of
+          Left err -> Test.assertFailure <| "Failed to parse 'facts list': " <> show err
+          Right args -> do
+            args `Cli.has` Cli.command "facts" Test.@?= True
+            args `Cli.has` Cli.command "list" Test.@?= True,
+      Test.unit "can parse facts list with --project" <| do
+        let result = Docopt.parseArgs help ["facts", "list", "--project=myproj"]
+        case result of
+          Left err -> Test.assertFailure <| "Failed to parse 'facts list --project': " <> show err
+          Right args -> do
+            args `Cli.has` Cli.command "facts" Test.@?= True
+            args `Cli.has` Cli.command "list" Test.@?= True
+            Cli.getArg args (Cli.longOption "project") Test.@?= Just "myproj",
+      Test.unit "can parse facts list with --json" <| do
+        let result = Docopt.parseArgs help ["facts", "list", "--json"]
+        case result of
+          Left err -> Test.assertFailure <| "Failed to parse 'facts list --json': " <> show err
+          Right args -> do
+            args `Cli.has` Cli.command "facts" Test.@?= True
+            args `Cli.has` Cli.command "list" Test.@?= True
+            args `Cli.has` Cli.longOption "json" Test.@?= True,
+      Test.unit "can parse facts show command" <| do
+        let result = Docopt.parseArgs help ["facts", "show", "42"]
+        case result of
+          Left err -> Test.assertFailure <| "Failed to parse 'facts show 42': " <> show err
+          Right args -> do
+            args `Cli.has` Cli.command "facts" Test.@?= True
+            args `Cli.has` Cli.command "show" Test.@?= True
+            Cli.getArg args (Cli.argument "fact-id") Test.@?= Just "42",
+      Test.unit "can parse facts add command" <| do
+        let result = Docopt.parseArgs help ["facts", "add", "myproj", "This is a fact"]
+        case result of
+          Left err -> Test.assertFailure <| "Failed to parse 'facts add': " <> show err
+          Right args -> do
+            args `Cli.has` Cli.command "facts" Test.@?= True
+            args `Cli.has` Cli.command "add" Test.@?= True
+            Cli.getArg args (Cli.argument "project") Test.@?= Just "myproj"
+            Cli.getArg args (Cli.argument "content") Test.@?= Just "This is a fact",
+      Test.unit "can parse facts add with options" <| do
+        let result = Docopt.parseArgs help ["facts", "add", "myproj", "fact", "--files=a.hs,b.hs", "--task=t-123", "--confidence=0.9"]
+        case result of
+          Left err -> Test.assertFailure <| "Failed to parse 'facts add' with options: " <> show err
+          Right args -> do
+            args `Cli.has` Cli.command "facts" Test.@?= True
+            args `Cli.has` Cli.command "add" Test.@?= True
+            Cli.getArg args (Cli.longOption "files") Test.@?= Just "a.hs,b.hs"
+            Cli.getArg args (Cli.longOption "task") Test.@?= Just "t-123"
+            Cli.getArg args (Cli.longOption "confidence") Test.@?= Just "0.9",
+      Test.unit "can parse facts delete command" <| do
+        let result = Docopt.parseArgs help ["facts", "delete", "42"]
+        case result of
+          Left err -> Test.assertFailure <| "Failed to parse 'facts delete 42': " <> show err
+          Right args -> do
+            args `Cli.has` Cli.command "facts" Test.@?= True
+            args `Cli.has` Cli.command "delete" Test.@?= True
+            Cli.getArg args (Cli.argument "fact-id") Test.@?= Just "42"
     ]