1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE NoImplicitPrelude #-}
-- : out agent
-- : dep sqlite-simple
-- : dep temporary
module Omni.Agent where
import Alpha
import qualified Data.Text as Text
import qualified Data.Text.IO as TIO
import qualified Omni.Agent.Core as Core
import qualified Omni.Agent.Git as Git
import qualified Omni.Agent.Log as Log
import qualified Omni.Agent.Worker as Worker
import qualified Omni.Cli as Cli
import qualified Omni.Task.Core as TaskCore
import qualified Omni.Test as Test
import qualified System.Console.Docopt as Docopt
import qualified System.Directory as Directory
import qualified System.Environment as Env
import qualified System.Exit as Exit
import System.FilePath (takeFileName)
import qualified System.IO as IO
import qualified System.IO.Temp as Temp
main :: IO ()
main = Cli.main plan
plan :: Cli.Plan ()
plan =
Cli.Plan
{ Cli.help = help,
Cli.move = move,
Cli.test = test,
Cli.tidy = \_ -> pure ()
}
help :: Cli.Docopt
help =
[Cli.docopt|
agent
Usage:
agent start [<task-id>]
agent harvest [--path=<path>]
agent merge-driver <ours> <theirs>
agent test
agent --help
Options:
--path=<path> Path to the worker directory [default: .]
--help Show this help
|]
move :: Cli.Arguments -> IO ()
move args
| args `Cli.has` Cli.command "start" = do
-- Always run in current directory
let path = "."
-- Infer name from current directory
absPath <- Directory.getCurrentDirectory
let name = Text.pack (takeFileName absPath)
let worker =
Core.Worker
{ Core.workerName = name,
Core.workerPid = Nothing,
Core.workerStatus = Core.Idle,
Core.workerPath = path
}
let taskId = fmap Text.pack (Cli.getArg args (Cli.argument "task-id"))
Worker.start worker taskId
| args `Cli.has` Cli.command "harvest" = harvest args
| args `Cli.has` Cli.command "merge-driver" = mergeDriver args
| otherwise = putStrLn (Cli.usage help)
getArgOrExit :: Cli.Arguments -> Docopt.Option -> IO String
getArgOrExit args opt =
case Cli.getArg args opt of
Just val -> pure val
Nothing -> do
putText <| "Error: Missing required argument " <> Text.pack (show opt)
Exit.exitFailure
harvest :: Cli.Arguments -> IO ()
harvest args = do
let path = Cli.getArgWithDefault args "." (Cli.longOption "path")
putText "Harvesting task updates from workers..."
branches <- Git.listBranches path "omni-worker-*"
if null branches
then putText "No worker branches found."
else do
updated <- foldlM (processBranch path) False branches
when updated <| do
-- Consolidate
Directory.setCurrentDirectory path
-- Export to temporary file then commit? No, harvest implies we are merging back to source of truth.
-- But wait, tasks are now in SQLite.
-- If we are harvesting from branches, we are importing from JSONL in branches into our DB.
-- Then we probably want to export back to JSONL for git tracking?
-- The user said: "Remove git-tracked tasks.jsonl".
-- So harvest might need rethinking or just be disabled for now.
-- But strictly answering the compilation error: exportTasks takes an argument now.
-- Since we removed tasks.jsonl, maybe we shouldn't be exporting here at all if the goal is to just update the DB?
-- However, if harvest is about syncing multiple workers, it assumes a shared git repo.
-- If we removed git tracking of tasks, harvest via git branches makes less sense unless we still use JSONL for interchange.
-- For now, let's pass Nothing to exportTasks (stdout) which is probably not what we want, OR
-- if we want to save to DB, we don't need to call exportTasks because importTasks already updates the DB.
-- Let's remove the exportTasks call here as it seems redundant or incorrect given we removed the file.
-- But wait, if 'harvest' commits "task: harvest updates...", what is it committing?
-- The SQLite DB is likely .gitignore'd.
-- So harvest effectively does nothing useful for git if tasks are not in git.
-- I will comment out the commit and export for now to fix the build, assuming 'harvest' is legacy/deprecated with the removal of git-tracked tasks.
-- TaskCore.exportTasks Nothing
-- Commit if changed
-- Git.commit path "task: harvest updates from workers"
putText "Success: Task database updated."
processBranch :: FilePath -> Bool -> Text -> IO Bool
processBranch repo updated branch = do
putText <| "Checking " <> branch <> "..."
maybeContent <- Git.showFile repo branch ".tasks/tasks.jsonl"
case maybeContent of
Nothing -> do
putText <| " Warning: Could not read .tasks/tasks.jsonl from " <> branch
pure updated
Just content -> do
-- Write to temp file
Temp.withSystemTempFile "worker-tasks.jsonl" <| \tempPath h -> do
TIO.hPutStr h content
IO.hClose h
-- Import
-- We need to ensure we are in the repo directory for TaskCore to find .tasks/tasks.jsonl
Directory.setCurrentDirectory repo
TaskCore.importTasks tempPath
putText <| " Imported tasks from " <> branch
pure True
mergeDriver :: Cli.Arguments -> IO ()
mergeDriver args = do
ours <- getArgOrExit args (Cli.argument "ours")
theirs <- getArgOrExit args (Cli.argument "theirs")
-- Set TASK_DB_PATH to ours (the file git provided as the current version)
Env.setEnv "TASK_DB_PATH" ours
TaskCore.importTasks theirs
Exit.exitSuccess
test :: Test.Tree
test = Test.group "Omni.Agent" [unitTests, logTests]
logTests :: Test.Tree
logTests =
Test.group
"Log tests"
[ Test.unit "Log.emptyStatus" <| do
let s = Log.emptyStatus "worker-1"
Log.statusWorker s Test.@?= "worker-1"
Log.statusFiles s Test.@?= 0
]
unitTests :: Test.Tree
unitTests =
Test.group
"Unit tests"
[ Test.unit "can parse start command" <| do
let result = Docopt.parseArgs help ["start"]
case result of
Left err -> Test.assertFailure <| "Failed to parse 'start': " <> show err
Right args -> args `Cli.has` Cli.command "start" Test.@?= True,
Test.unit "can parse start command with task id" <| do
let result = Docopt.parseArgs help ["start", "t-123"]
case result of
Left err -> Test.assertFailure <| "Failed to parse 'start t-123': " <> show err
Right args -> do
args `Cli.has` Cli.command "start" Test.@?= True
Cli.getArg args (Cli.argument "task-id") Test.@?= Just "t-123",
Test.unit "can parse harvest command" <| do
let result = Docopt.parseArgs help ["harvest"]
case result of
Left err -> Test.assertFailure <| "Failed to parse 'harvest': " <> show err
Right args -> args `Cli.has` Cli.command "harvest" Test.@?= True
]
|