@nbardy/oompa 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -0
- package/agentnet/src/agentnet/agent.clj +293 -0
- package/agentnet/src/agentnet/cli.clj +436 -0
- package/agentnet/src/agentnet/core.clj +211 -0
- package/agentnet/src/agentnet/merge.clj +355 -0
- package/agentnet/src/agentnet/notes.clj +123 -0
- package/agentnet/src/agentnet/orchestrator.clj +367 -0
- package/agentnet/src/agentnet/review.clj +263 -0
- package/agentnet/src/agentnet/schema.clj +141 -0
- package/agentnet/src/agentnet/tasks.clj +198 -0
- package/agentnet/src/agentnet/worker.clj +427 -0
- package/agentnet/src/agentnet/worktree.clj +342 -0
- package/bin/oompa.js +37 -0
- package/config/prompts/cto.md +45 -0
- package/config/prompts/engineer.md +44 -0
- package/config/prompts/executor.md +32 -0
- package/config/prompts/planner.md +35 -0
- package/config/prompts/reviewer.md +49 -0
- package/config/prompts/worker.md +31 -0
- package/oompa.example.json +18 -0
- package/package.json +45 -0
- package/swarm.bb +18 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
(ns agentnet.worker
|
|
2
|
+
"Self-directed worker execution.
|
|
3
|
+
|
|
4
|
+
Workers:
|
|
5
|
+
1. Claim tasks from tasks/pending/ (mv → current/)
|
|
6
|
+
2. Execute task in worktree
|
|
7
|
+
3. Commit changes
|
|
8
|
+
4. Reviewer checks work (if configured)
|
|
9
|
+
5. If approved → merge to main, complete task
|
|
10
|
+
6. If rejected → fix & retry → back to reviewer
|
|
11
|
+
7. Can create new tasks in pending/
|
|
12
|
+
8. Exit on __DONE__ signal
|
|
13
|
+
|
|
14
|
+
No separate orchestrator - workers self-organize."
|
|
15
|
+
(:require [agentnet.tasks :as tasks]
|
|
16
|
+
[agentnet.agent :as agent]
|
|
17
|
+
[agentnet.worktree :as worktree]
|
|
18
|
+
[babashka.process :as process]
|
|
19
|
+
[clojure.java.io :as io]
|
|
20
|
+
[clojure.string :as str]))
|
|
21
|
+
|
|
22
|
+
;; =============================================================================
|
|
23
|
+
;; codex-persist integration
|
|
24
|
+
;; =============================================================================
|
|
25
|
+
|
|
26
|
+
(def ^:private persist-cmd* (atom nil))
|
|
27
|
+
(def ^:private persist-missing-warned?* (atom false))
|
|
28
|
+
|
|
29
|
+
(defn- command-ok?
|
|
30
|
+
"Return true if command vector is executable (exit code ignored)."
|
|
31
|
+
[cmd]
|
|
32
|
+
(try
|
|
33
|
+
(do
|
|
34
|
+
(process/sh (vec cmd) {:out :string :err :string :continue true})
|
|
35
|
+
true)
|
|
36
|
+
(catch Exception _
|
|
37
|
+
false)))
|
|
38
|
+
|
|
39
|
+
(defn- resolve-codex-persist-cmd
|
|
40
|
+
"Resolve codex-persist command vector.
|
|
41
|
+
Order:
|
|
42
|
+
1) CODEX_PERSIST_BIN env var
|
|
43
|
+
2) codex-persist on PATH
|
|
44
|
+
3) node ~/git/codex-persist/dist/cli.js"
|
|
45
|
+
[]
|
|
46
|
+
(let [cached @persist-cmd*]
|
|
47
|
+
(if (some? cached)
|
|
48
|
+
cached
|
|
49
|
+
(let [env-bin (System/getenv "CODEX_PERSIST_BIN")
|
|
50
|
+
env-cmd (when (and env-bin (not (str/blank? env-bin)))
|
|
51
|
+
[env-bin])
|
|
52
|
+
path-cmd ["codex-persist"]
|
|
53
|
+
local-cli (str (System/getProperty "user.home") "/git/codex-persist/dist/cli.js")
|
|
54
|
+
local-cmd (when (.exists (io/file local-cli))
|
|
55
|
+
["node" local-cli])
|
|
56
|
+
cmd (cond
|
|
57
|
+
(and env-cmd (command-ok? env-cmd)) env-cmd
|
|
58
|
+
(command-ok? path-cmd) path-cmd
|
|
59
|
+
(and local-cmd (command-ok? local-cmd)) local-cmd
|
|
60
|
+
:else false)]
|
|
61
|
+
(reset! persist-cmd* cmd)
|
|
62
|
+
cmd))))
|
|
63
|
+
|
|
64
|
+
(defn- safe-assistant-content
|
|
65
|
+
"Pick a non-empty assistant message payload for persistence."
|
|
66
|
+
[result]
|
|
67
|
+
(let [out (or (:out result) "")
|
|
68
|
+
err (or (:err result) "")
|
|
69
|
+
exit-code (or (:exit result) -1)]
|
|
70
|
+
(cond
|
|
71
|
+
(not (str/blank? out)) out
|
|
72
|
+
(not (str/blank? err)) (str "[agent stderr] " err)
|
|
73
|
+
:else (str "[agent exit " exit-code "]"))))
|
|
74
|
+
|
|
75
|
+
(defn- persist-message!
|
|
76
|
+
"Write a single message to codex-persist; no-op if unavailable."
|
|
77
|
+
[worker-id session-id cwd role content]
|
|
78
|
+
(let [resolved (resolve-codex-persist-cmd)]
|
|
79
|
+
(if (and resolved (not= resolved false))
|
|
80
|
+
(let [persist-cmd resolved
|
|
81
|
+
payload (if (str/blank? content) "(empty)" content)
|
|
82
|
+
result (try
|
|
83
|
+
(process/sh (into persist-cmd ["write" session-id cwd role payload])
|
|
84
|
+
{:out :string :err :string})
|
|
85
|
+
(catch Exception e
|
|
86
|
+
{:exit -1 :out "" :err (.getMessage e)}))]
|
|
87
|
+
(when-not (zero? (:exit result))
|
|
88
|
+
(println (format "[%s] codex-persist write failed (%s)" worker-id role))))
|
|
89
|
+
(when (compare-and-set! persist-missing-warned?* false true)
|
|
90
|
+
(println "[oompa] codex-persist not found; set CODEX_PERSIST_BIN or install/link codex-persist")))))
|
|
91
|
+
|
|
92
|
+
;; =============================================================================
|
|
93
|
+
;; Worker State
|
|
94
|
+
;; =============================================================================
|
|
95
|
+
|
|
96
|
+
(defn create-worker
|
|
97
|
+
"Create a worker config"
|
|
98
|
+
[{:keys [id swarm-id harness model iterations custom-prompt review-harness review-model]}]
|
|
99
|
+
{:id id
|
|
100
|
+
:swarm-id swarm-id
|
|
101
|
+
:harness (or harness :codex)
|
|
102
|
+
:model model
|
|
103
|
+
:iterations (or iterations 10)
|
|
104
|
+
:custom-prompt custom-prompt
|
|
105
|
+
:review-harness review-harness
|
|
106
|
+
:review-model review-model
|
|
107
|
+
:completed 0
|
|
108
|
+
:status :idle})
|
|
109
|
+
|
|
110
|
+
;; =============================================================================
|
|
111
|
+
;; Task Execution
|
|
112
|
+
;; =============================================================================
|
|
113
|
+
|
|
114
|
+
(def ^:private max-review-retries 3)
|
|
115
|
+
|
|
116
|
+
(defn- build-context
|
|
117
|
+
"Build context for agent prompts"
|
|
118
|
+
[]
|
|
119
|
+
(let [pending (tasks/list-pending)
|
|
120
|
+
current (tasks/list-current)
|
|
121
|
+
complete (tasks/list-complete)]
|
|
122
|
+
{:pending_count (count pending)
|
|
123
|
+
:current_count (count current)
|
|
124
|
+
:complete_count (count complete)
|
|
125
|
+
:pending_tasks (str/join "\n" (map #(str "- " (:id %) ": " (:summary %)) pending))
|
|
126
|
+
:task_status (format "Pending: %d, In Progress: %d, Complete: %d"
|
|
127
|
+
(count pending) (count current) (count complete))}))
|
|
128
|
+
|
|
129
|
+
(defn- run-agent!
|
|
130
|
+
"Run agent with prompt, return {:output string, :done? bool, :exit int}"
|
|
131
|
+
[{:keys [id swarm-id harness model custom-prompt]} worktree-path context]
|
|
132
|
+
(let [;; Load prompt (check config/prompts/ as default)
|
|
133
|
+
prompt-content (or (agent/load-custom-prompt custom-prompt)
|
|
134
|
+
(agent/load-custom-prompt "config/prompts/worker.md")
|
|
135
|
+
"Goal: Match spec.md\nProcess: Create/claim tasks in tasks/{pending,current,complete}/*.edn\nMethod: Isolate changes to your worktree, commit and merge when complete")
|
|
136
|
+
|
|
137
|
+
;; Inject worktree and context
|
|
138
|
+
full-prompt (str "Worktree: " worktree-path "\n"
|
|
139
|
+
"Task Status: " (:task_status context) "\n\n"
|
|
140
|
+
prompt-content)
|
|
141
|
+
session-id (str/lower-case (str (java.util.UUID/randomUUID)))
|
|
142
|
+
swarm-id* (or swarm-id "unknown")
|
|
143
|
+
tagged-prompt (str "[oompa:" swarm-id* ":" id "] " full-prompt)
|
|
144
|
+
abs-worktree (.getAbsolutePath (io/file worktree-path))
|
|
145
|
+
|
|
146
|
+
;; Build command
|
|
147
|
+
cmd (case harness
|
|
148
|
+
:codex (cond-> ["codex" "exec" "--full-auto" "--skip-git-repo-check"
|
|
149
|
+
"-C" worktree-path "--sandbox" "workspace-write"]
|
|
150
|
+
model (into ["--model" model])
|
|
151
|
+
true (conj "--" full-prompt))
|
|
152
|
+
:claude (cond-> ["claude" "-p" "--dangerously-skip-permissions"
|
|
153
|
+
"--session-id" session-id]
|
|
154
|
+
model (into ["--model" model])))
|
|
155
|
+
|
|
156
|
+
_ (when (= harness :codex)
|
|
157
|
+
(persist-message! id session-id abs-worktree "user" tagged-prompt))
|
|
158
|
+
|
|
159
|
+
;; Run agent
|
|
160
|
+
result (try
|
|
161
|
+
(if (= harness :claude)
|
|
162
|
+
;; Claude reads from stdin
|
|
163
|
+
(process/sh cmd {:in tagged-prompt :out :string :err :string})
|
|
164
|
+
;; Codex takes prompt as arg
|
|
165
|
+
(process/sh cmd {:out :string :err :string}))
|
|
166
|
+
(catch Exception e
|
|
167
|
+
{:exit -1 :out "" :err (.getMessage e)}))]
|
|
168
|
+
|
|
169
|
+
(when (= harness :codex)
|
|
170
|
+
(persist-message! id session-id abs-worktree "assistant" (safe-assistant-content result)))
|
|
171
|
+
|
|
172
|
+
{:output (:out result)
|
|
173
|
+
:exit (:exit result)
|
|
174
|
+
:done? (agent/done-signal? (:out result))}))
|
|
175
|
+
|
|
176
|
+
(defn- run-reviewer!
|
|
177
|
+
"Run reviewer on worktree changes.
|
|
178
|
+
Returns {:verdict :approved|:needs-changes|:rejected, :comments [...]}"
|
|
179
|
+
[{:keys [review-harness review-model]} worktree-path]
|
|
180
|
+
(let [;; Get diff for context
|
|
181
|
+
diff-result (process/sh ["git" "diff" "main" "--stat"]
|
|
182
|
+
{:dir worktree-path :out :string :err :string})
|
|
183
|
+
diff-summary (:out diff-result)
|
|
184
|
+
|
|
185
|
+
;; Build review prompt
|
|
186
|
+
review-prompt (str "Review the changes in this worktree.\n\n"
|
|
187
|
+
"Diff summary:\n" diff-summary "\n\n"
|
|
188
|
+
"Check for:\n"
|
|
189
|
+
"- Code correctness\n"
|
|
190
|
+
"- Matches the intended task\n"
|
|
191
|
+
"- No obvious bugs or issues\n\n"
|
|
192
|
+
"Respond with:\n"
|
|
193
|
+
"- APPROVED if changes are good\n"
|
|
194
|
+
"- NEEDS_CHANGES with bullet points of issues\n"
|
|
195
|
+
"- REJECTED if fundamentally wrong")
|
|
196
|
+
|
|
197
|
+
;; Build command
|
|
198
|
+
cmd (case review-harness
|
|
199
|
+
:codex (cond-> ["codex" "exec" "--full-auto" "--skip-git-repo-check"
|
|
200
|
+
"-C" worktree-path "--sandbox" "workspace-write"]
|
|
201
|
+
review-model (into ["--model" review-model])
|
|
202
|
+
true (conj "--" review-prompt))
|
|
203
|
+
:claude (cond-> ["claude" "-p" "--dangerously-skip-permissions"]
|
|
204
|
+
review-model (into ["--model" review-model])))
|
|
205
|
+
|
|
206
|
+
;; Run reviewer
|
|
207
|
+
result (try
|
|
208
|
+
(if (= review-harness :claude)
|
|
209
|
+
(process/sh cmd {:in review-prompt :out :string :err :string})
|
|
210
|
+
(process/sh cmd {:out :string :err :string}))
|
|
211
|
+
(catch Exception e
|
|
212
|
+
{:exit -1 :out "" :err (.getMessage e)}))
|
|
213
|
+
|
|
214
|
+
output (:out result)]
|
|
215
|
+
|
|
216
|
+
{:verdict (cond
|
|
217
|
+
(re-find #"(?i)\bAPPROVED\b" output) :approved
|
|
218
|
+
(re-find #"(?i)\bREJECTED\b" output) :rejected
|
|
219
|
+
:else :needs-changes)
|
|
220
|
+
:comments (when (not= (:exit result) 0)
|
|
221
|
+
[(:err result)])
|
|
222
|
+
:output output}))
|
|
223
|
+
|
|
224
|
+
(defn- run-fix!
|
|
225
|
+
"Ask worker to fix issues based on reviewer feedback.
|
|
226
|
+
Returns {:output string, :exit int}"
|
|
227
|
+
[{:keys [harness model]} worktree-path feedback]
|
|
228
|
+
(let [fix-prompt (str "The reviewer found issues with your changes:\n\n"
|
|
229
|
+
feedback "\n\n"
|
|
230
|
+
"Please fix these issues in the worktree.")
|
|
231
|
+
|
|
232
|
+
cmd (case harness
|
|
233
|
+
:codex (cond-> ["codex" "exec" "--full-auto" "--skip-git-repo-check"
|
|
234
|
+
"-C" worktree-path "--sandbox" "workspace-write"]
|
|
235
|
+
model (into ["--model" model])
|
|
236
|
+
true (conj "--" fix-prompt))
|
|
237
|
+
:claude (cond-> ["claude" "-p" "--dangerously-skip-permissions"]
|
|
238
|
+
model (into ["--model" model])))
|
|
239
|
+
|
|
240
|
+
result (try
|
|
241
|
+
(if (= harness :claude)
|
|
242
|
+
(process/sh cmd {:in fix-prompt :out :string :err :string})
|
|
243
|
+
(process/sh cmd {:out :string :err :string}))
|
|
244
|
+
(catch Exception e
|
|
245
|
+
{:exit -1 :out "" :err (.getMessage e)}))]
|
|
246
|
+
|
|
247
|
+
{:output (:out result)
|
|
248
|
+
:exit (:exit result)}))
|
|
249
|
+
|
|
250
|
+
(defn- merge-to-main!
|
|
251
|
+
"Merge worktree changes to main branch"
|
|
252
|
+
[wt-path wt-id worker-id]
|
|
253
|
+
(println (format "[%s] Merging changes to main" worker-id))
|
|
254
|
+
(let [;; Commit in worktree if needed
|
|
255
|
+
_ (process/sh ["git" "add" "-A"] {:dir wt-path})
|
|
256
|
+
_ (process/sh ["git" "commit" "-m" (str "Work from " wt-id) "--allow-empty"]
|
|
257
|
+
{:dir wt-path})
|
|
258
|
+
;; Checkout main and merge
|
|
259
|
+
checkout-result (process/sh ["git" "checkout" "main"]
|
|
260
|
+
{:out :string :err :string})
|
|
261
|
+
merge-result (when (zero? (:exit checkout-result))
|
|
262
|
+
(process/sh ["git" "merge" wt-id "--no-edit"]
|
|
263
|
+
{:out :string :err :string}))]
|
|
264
|
+
(and (zero? (:exit checkout-result))
|
|
265
|
+
(zero? (:exit merge-result)))))
|
|
266
|
+
|
|
267
|
+
(defn- review-loop!
|
|
268
|
+
"Run review loop: reviewer checks → if issues, fix & retry → back to reviewer.
|
|
269
|
+
Returns {:approved? bool, :attempts int}"
|
|
270
|
+
[worker wt-path worker-id]
|
|
271
|
+
(if-not (and (:review-harness worker) (:review-model worker))
|
|
272
|
+
;; No reviewer configured, auto-approve
|
|
273
|
+
{:approved? true :attempts 0}
|
|
274
|
+
|
|
275
|
+
;; Run review loop
|
|
276
|
+
(loop [attempt 1]
|
|
277
|
+
(println (format "[%s] Review attempt %d/%d" worker-id attempt max-review-retries))
|
|
278
|
+
(let [{:keys [verdict output]} (run-reviewer! worker wt-path)]
|
|
279
|
+
(case verdict
|
|
280
|
+
:approved
|
|
281
|
+
(do
|
|
282
|
+
(println (format "[%s] Reviewer APPROVED" worker-id))
|
|
283
|
+
{:approved? true :attempts attempt})
|
|
284
|
+
|
|
285
|
+
:rejected
|
|
286
|
+
(do
|
|
287
|
+
(println (format "[%s] Reviewer REJECTED" worker-id))
|
|
288
|
+
{:approved? false :attempts attempt})
|
|
289
|
+
|
|
290
|
+
;; :needs-changes
|
|
291
|
+
(if (>= attempt max-review-retries)
|
|
292
|
+
(do
|
|
293
|
+
(println (format "[%s] Max review retries reached" worker-id))
|
|
294
|
+
{:approved? false :attempts attempt})
|
|
295
|
+
(do
|
|
296
|
+
(println (format "[%s] Reviewer requested changes, fixing..." worker-id))
|
|
297
|
+
(run-fix! worker wt-path output)
|
|
298
|
+
(recur (inc attempt)))))))))
|
|
299
|
+
|
|
300
|
+
(defn execute-iteration!
|
|
301
|
+
"Execute one iteration of work.
|
|
302
|
+
|
|
303
|
+
Flow:
|
|
304
|
+
1. Create worktree
|
|
305
|
+
2. Run agent
|
|
306
|
+
3. If reviewer configured: run review loop (fix → retry → reviewer)
|
|
307
|
+
4. If approved: merge to main
|
|
308
|
+
5. Cleanup worktree
|
|
309
|
+
|
|
310
|
+
Returns {:status :done|:continue|:error, :task task-or-nil}"
|
|
311
|
+
[worker iteration total-iterations]
|
|
312
|
+
(let [worker-id (:id worker)
|
|
313
|
+
wt-id (format ".w%s-i%d" worker-id iteration)
|
|
314
|
+
|
|
315
|
+
;; Create worktree
|
|
316
|
+
_ (println (format "[%s] Starting iteration %d/%d" worker-id iteration total-iterations))
|
|
317
|
+
wt-path (str (System/getProperty "user.dir") "/" wt-id)]
|
|
318
|
+
|
|
319
|
+
(try
|
|
320
|
+
;; Setup worktree
|
|
321
|
+
(process/sh ["git" "worktree" "add" wt-id "-b" wt-id])
|
|
322
|
+
|
|
323
|
+
;; Build context
|
|
324
|
+
(let [context (build-context)
|
|
325
|
+
|
|
326
|
+
;; Run agent
|
|
327
|
+
{:keys [output exit done?]} (run-agent! worker wt-path context)]
|
|
328
|
+
|
|
329
|
+
(cond
|
|
330
|
+
;; Agent signaled done
|
|
331
|
+
done?
|
|
332
|
+
(do
|
|
333
|
+
(println (format "[%s] Received __DONE__ signal" worker-id))
|
|
334
|
+
{:status :done})
|
|
335
|
+
|
|
336
|
+
;; Agent errored
|
|
337
|
+
(not (zero? exit))
|
|
338
|
+
(do
|
|
339
|
+
(println (format "[%s] Agent error (exit %d)" worker-id exit))
|
|
340
|
+
{:status :error :exit exit})
|
|
341
|
+
|
|
342
|
+
;; Success - run review loop before merge
|
|
343
|
+
:else
|
|
344
|
+
(let [{:keys [approved?]} (review-loop! worker wt-path worker-id)]
|
|
345
|
+
(if approved?
|
|
346
|
+
(do
|
|
347
|
+
(merge-to-main! wt-path wt-id worker-id)
|
|
348
|
+
(println (format "[%s] Iteration %d/%d complete" worker-id iteration total-iterations))
|
|
349
|
+
{:status :continue})
|
|
350
|
+
(do
|
|
351
|
+
(println (format "[%s] Iteration %d/%d rejected, discarding" worker-id iteration total-iterations))
|
|
352
|
+
{:status :continue})))))
|
|
353
|
+
|
|
354
|
+
(finally
|
|
355
|
+
;; Cleanup worktree
|
|
356
|
+
(process/sh ["git" "worktree" "remove" wt-id "--force"])))))
|
|
357
|
+
|
|
358
|
+
;; =============================================================================
|
|
359
|
+
;; Worker Loop
|
|
360
|
+
;; =============================================================================
|
|
361
|
+
|
|
362
|
+
(defn run-worker!
|
|
363
|
+
"Run worker loop until done or iterations exhausted.
|
|
364
|
+
|
|
365
|
+
Returns final worker state."
|
|
366
|
+
[worker]
|
|
367
|
+
(tasks/ensure-dirs!)
|
|
368
|
+
(let [{:keys [id iterations]} worker]
|
|
369
|
+
(println (format "[%s] Starting worker (%s:%s, %d iterations)"
|
|
370
|
+
id
|
|
371
|
+
(name (:harness worker))
|
|
372
|
+
(or (:model worker) "default")
|
|
373
|
+
iterations))
|
|
374
|
+
|
|
375
|
+
(loop [iter 1
|
|
376
|
+
completed 0]
|
|
377
|
+
(if (> iter iterations)
|
|
378
|
+
(do
|
|
379
|
+
(println (format "[%s] Completed %d iterations" id completed))
|
|
380
|
+
(assoc worker :completed completed :status :exhausted))
|
|
381
|
+
|
|
382
|
+
(let [{:keys [status]} (execute-iteration! worker iter iterations)]
|
|
383
|
+
(case status
|
|
384
|
+
:done
|
|
385
|
+
(do
|
|
386
|
+
(println (format "[%s] Worker done after %d/%d iterations" id iter iterations))
|
|
387
|
+
(assoc worker :completed iter :status :done))
|
|
388
|
+
|
|
389
|
+
:error
|
|
390
|
+
(do
|
|
391
|
+
(println (format "[%s] Worker error at iteration %d/%d, continuing..." id iter iterations))
|
|
392
|
+
(recur (inc iter) completed))
|
|
393
|
+
|
|
394
|
+
:continue
|
|
395
|
+
(recur (inc iter) (inc completed))))))))
|
|
396
|
+
|
|
397
|
+
;; =============================================================================
|
|
398
|
+
;; Multi-Worker Execution
|
|
399
|
+
;; =============================================================================
|
|
400
|
+
|
|
401
|
+
(defn run-workers!
|
|
402
|
+
"Run multiple workers in parallel.
|
|
403
|
+
|
|
404
|
+
Arguments:
|
|
405
|
+
workers - seq of worker configs
|
|
406
|
+
|
|
407
|
+
Returns seq of final worker states."
|
|
408
|
+
[workers]
|
|
409
|
+
(tasks/ensure-dirs!)
|
|
410
|
+
(println (format "Launching %d workers..." (count workers)))
|
|
411
|
+
|
|
412
|
+
(let [futures (doall
|
|
413
|
+
(map-indexed
|
|
414
|
+
(fn [idx worker]
|
|
415
|
+
(let [worker (assoc worker :id (or (:id worker) (str "w" idx)))]
|
|
416
|
+
(future (run-worker! worker))))
|
|
417
|
+
workers))]
|
|
418
|
+
|
|
419
|
+
(println "All workers launched. Waiting for completion...")
|
|
420
|
+
(let [results (mapv deref futures)]
|
|
421
|
+
(println "\nAll workers complete.")
|
|
422
|
+
(doseq [w results]
|
|
423
|
+
(println (format " [%s] %s - %d iterations"
|
|
424
|
+
(:id w)
|
|
425
|
+
(name (:status w))
|
|
426
|
+
(:completed w))))
|
|
427
|
+
results)))
|