@nbardy/oompa 0.7.0 → 0.7.2
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 +32 -6
- package/agentnet/src/agentnet/agent.clj +157 -14
- package/agentnet/src/agentnet/cli.clj +742 -148
- package/agentnet/src/agentnet/harness.clj +240 -0
- package/agentnet/src/agentnet/orchestrator.clj +2 -0
- package/agentnet/src/agentnet/runs.clj +78 -50
- package/agentnet/src/agentnet/schema.clj +9 -2
- package/agentnet/src/agentnet/tasks.clj +47 -0
- package/agentnet/src/agentnet/worker.clj +679 -395
- package/bin/test-models +1 -1
- package/config/prompts/_agent_scope_rules.md +7 -0
- package/config/prompts/_task_header.md +22 -47
- package/config/prompts/cto.md +2 -0
- package/config/prompts/engineer.md +2 -0
- package/config/prompts/executor.md +2 -0
- package/config/prompts/magicgenie-executor.md +15 -0
- package/config/prompts/magicgenie-planner.md +26 -0
- package/config/prompts/magicgenie-reviewer.md +44 -0
- package/config/prompts/planner.md +3 -1
- package/config/prompts/reviewer.md +2 -0
- package/config/prompts/worker.md +7 -4
- package/oompa.example.json +10 -2
- package/package.json +1 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"Command-line interface for AgentNet orchestrator.
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
|
-
./swarm.bb run # Run
|
|
6
|
-
./swarm.bb run --
|
|
5
|
+
./swarm.bb run # Run swarm from config (oompa.json)
|
|
6
|
+
./swarm.bb run --detach --config oompa.json # Run in background with startup validation
|
|
7
7
|
./swarm.bb loop 20 --harness claude # 20 iterations with Claude
|
|
8
8
|
./swarm.bb loop --workers claude:5 opencode:2 --iterations 20 # Mixed harnesses
|
|
9
9
|
./swarm.bb swarm oompa.json # Multi-model from config
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
[agentnet.worker :as worker]
|
|
17
17
|
[agentnet.tasks :as tasks]
|
|
18
18
|
[agentnet.agent :as agent]
|
|
19
|
+
[agentnet.harness :as harness]
|
|
19
20
|
[agentnet.runs :as runs]
|
|
20
21
|
[babashka.process :as process]
|
|
21
22
|
[clojure.string :as str]
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
(Integer/parseInt s)
|
|
32
33
|
(catch Exception _ default)))
|
|
33
34
|
|
|
34
|
-
(def ^:private harnesses
|
|
35
|
+
(def ^:private harnesses (harness/known-harnesses))
|
|
35
36
|
|
|
36
37
|
(defn- make-swarm-id
|
|
37
38
|
"Generate a short run-level swarm ID."
|
|
@@ -45,8 +46,8 @@
|
|
|
45
46
|
(let [[harness count-str] (str/split s #":" 2)
|
|
46
47
|
h (keyword harness)
|
|
47
48
|
cnt (parse-int count-str 0)]
|
|
48
|
-
(when-not (
|
|
49
|
-
(throw (ex-info (str "Unknown harness in worker spec: " s ".
|
|
49
|
+
(when-not (harness/valid-harness? h)
|
|
50
|
+
(throw (ex-info (str "Unknown harness in worker spec: " s ". Known: " (str/join ", " (map name (sort harnesses)))) {})))
|
|
50
51
|
(when (zero? cnt)
|
|
51
52
|
(throw (ex-info (str "Invalid count in worker spec: " s ". Use format 'harness:count'") {})))
|
|
52
53
|
{:harness h :count cnt}))
|
|
@@ -74,6 +75,10 @@
|
|
|
74
75
|
:harness :codex
|
|
75
76
|
:model nil
|
|
76
77
|
:dry-run false
|
|
78
|
+
:detach false
|
|
79
|
+
:all false
|
|
80
|
+
:config-file nil
|
|
81
|
+
:startup-timeout nil
|
|
77
82
|
:iterations 1
|
|
78
83
|
:worker-specs nil}
|
|
79
84
|
remaining args]
|
|
@@ -96,8 +101,8 @@
|
|
|
96
101
|
|
|
97
102
|
(= arg "--harness")
|
|
98
103
|
(let [h (keyword (second remaining))]
|
|
99
|
-
(when-not (
|
|
100
|
-
(throw (ex-info (str "Unknown harness: " (second remaining) ".
|
|
104
|
+
(when-not (harness/valid-harness? h)
|
|
105
|
+
(throw (ex-info (str "Unknown harness: " (second remaining) ". Known: " (str/join ", " (map name (sort harnesses)))) {})))
|
|
101
106
|
(recur (assoc opts :harness h)
|
|
102
107
|
(nnext remaining)))
|
|
103
108
|
|
|
@@ -105,6 +110,28 @@
|
|
|
105
110
|
(recur (assoc opts :model (second remaining))
|
|
106
111
|
(nnext remaining))
|
|
107
112
|
|
|
113
|
+
(= arg "--config")
|
|
114
|
+
(let [config-file (second remaining)]
|
|
115
|
+
(when (str/blank? config-file)
|
|
116
|
+
(throw (ex-info "--config requires a path" {:arg arg})))
|
|
117
|
+
(recur (assoc opts :config-file config-file)
|
|
118
|
+
(nnext remaining)))
|
|
119
|
+
|
|
120
|
+
(or (= arg "--detach") (= arg "--dettach"))
|
|
121
|
+
(recur (assoc opts :detach true)
|
|
122
|
+
(next remaining))
|
|
123
|
+
|
|
124
|
+
(= arg "--all")
|
|
125
|
+
(recur (assoc opts :all true)
|
|
126
|
+
(next remaining))
|
|
127
|
+
|
|
128
|
+
(= arg "--startup-timeout")
|
|
129
|
+
(let [seconds (parse-int (second remaining) nil)]
|
|
130
|
+
(when-not (and (number? seconds) (pos? seconds))
|
|
131
|
+
(throw (ex-info "--startup-timeout requires a positive integer (seconds)" {:arg arg})))
|
|
132
|
+
(recur (assoc opts :startup-timeout seconds)
|
|
133
|
+
(nnext remaining)))
|
|
134
|
+
|
|
108
135
|
;; Legacy flags (still supported)
|
|
109
136
|
(= arg "--claude")
|
|
110
137
|
(recur (assoc opts :harness :claude)
|
|
@@ -136,34 +163,69 @@
|
|
|
136
163
|
;; Commands
|
|
137
164
|
;; =============================================================================
|
|
138
165
|
|
|
139
|
-
(declare cmd-swarm parse-model-string)
|
|
166
|
+
(declare cmd-swarm parse-model-string pid-alive?)
|
|
140
167
|
|
|
141
168
|
(defn- check-git-clean!
|
|
142
|
-
"
|
|
143
|
-
and wasted worker iterations."
|
|
169
|
+
"Warn if git working tree is dirty. Dirty index may cause merge conflicts."
|
|
144
170
|
[]
|
|
145
171
|
(let [result (process/sh ["git" "status" "--porcelain"]
|
|
146
172
|
{:out :string :err :string})
|
|
147
173
|
output (str/trim (:out result))]
|
|
148
174
|
(when (and (zero? (:exit result)) (not (str/blank? output)))
|
|
149
|
-
(println "
|
|
175
|
+
(println "WARNING: Git working tree is dirty. You may experience merge conflicts.")
|
|
176
|
+
(println output))))
|
|
177
|
+
|
|
178
|
+
(defn- check-stale-worktrees!
|
|
179
|
+
"Abort if stale oompa worktrees or branches exist from a prior run.
|
|
180
|
+
Corrupted .git/worktrees/ entries poison git worktree add for ALL workers,
|
|
181
|
+
not just the worker whose entry is stale. (See swarm af32b180 — kimi-k2.5
|
|
182
|
+
w9 went 20/20 doing nothing because w10's corrupt commondir blocked it.)"
|
|
183
|
+
[]
|
|
184
|
+
;; Prune orphaned metadata first — cleans entries whose directories are gone
|
|
185
|
+
(let [prune-result (process/sh ["git" "worktree" "prune"] {:out :string :err :string})]
|
|
186
|
+
(when-not (zero? (:exit prune-result))
|
|
187
|
+
(println "WARNING: git worktree prune failed:")
|
|
188
|
+
(println (:err prune-result))))
|
|
189
|
+
(let [;; Find .ww* directories (oompa per-iteration worktree naming convention)
|
|
190
|
+
ls-result (process/sh ["find" "." "-maxdepth" "1" "-type" "d" "-name" ".ww*"]
|
|
191
|
+
{:out :string})
|
|
192
|
+
stale-dirs (when (zero? (:exit ls-result))
|
|
193
|
+
(->> (str/split-lines (:out ls-result))
|
|
194
|
+
(remove str/blank?)))
|
|
195
|
+
;; Find oompa/* branches
|
|
196
|
+
br-result (process/sh ["git" "branch" "--list" "oompa/*"]
|
|
197
|
+
{:out :string})
|
|
198
|
+
stale-branches (when (zero? (:exit br-result))
|
|
199
|
+
(->> (str/split-lines (:out br-result))
|
|
200
|
+
(map str/trim)
|
|
201
|
+
(remove str/blank?)))]
|
|
202
|
+
(when (or (seq stale-dirs) (seq stale-branches))
|
|
203
|
+
(println "ERROR: Stale oompa worktrees detected from a prior run.")
|
|
204
|
+
(println " Corrupt worktree metadata will cause worker failures.")
|
|
150
205
|
(println)
|
|
151
|
-
(
|
|
206
|
+
(when (seq stale-dirs)
|
|
207
|
+
(println (format " Stale directories (%d):" (count stale-dirs)))
|
|
208
|
+
(doseq [d stale-dirs] (println (str " " d))))
|
|
209
|
+
(when (seq stale-branches)
|
|
210
|
+
(println (format " Stale branches (%d):" (count stale-branches)))
|
|
211
|
+
(doseq [b stale-branches] (println (str " " b))))
|
|
212
|
+
(println)
|
|
213
|
+
(println "Clean up with:")
|
|
214
|
+
(println " git worktree prune; for d in .ww*/; do git worktree remove --force \"$d\" 2>/dev/null; done; git branch --list 'oompa/*' | xargs git branch -D 2>/dev/null; rm -rf .ww*")
|
|
152
215
|
(println)
|
|
153
|
-
(println "Run 'git stash' or 'git commit' first.")
|
|
154
216
|
(System/exit 1))))
|
|
155
217
|
|
|
156
218
|
(defn- probe-model
|
|
157
219
|
"Send 'say ok' to a model via its harness CLI. Returns true if model responds.
|
|
158
|
-
|
|
159
|
-
|
|
220
|
+
Uses harness/build-probe-cmd for the command.
|
|
221
|
+
For stdin-based harnesses (e.g. claude), delivers the probe prompt via stdin.
|
|
222
|
+
For close-stdin harnesses, uses /dev/null to prevent hang."
|
|
223
|
+
[harness-kw model]
|
|
160
224
|
(try
|
|
161
|
-
(let [cmd (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
null-in (io/input-stream (io/file "/dev/null"))
|
|
166
|
-
proc (process/process cmd {:out :string :err :string :in null-in})
|
|
225
|
+
(let [cmd (harness/build-probe-cmd harness-kw model)
|
|
226
|
+
probe-prompt "[_HIDE_TEST_] say ok"
|
|
227
|
+
stdin-val (harness/process-stdin harness-kw probe-prompt)
|
|
228
|
+
proc (process/process cmd {:out :string :err :string :in stdin-val})
|
|
167
229
|
result (deref proc 30000 :timeout)]
|
|
168
230
|
(if (= result :timeout)
|
|
169
231
|
(do (.destroyForcibly (:proc proc)) false)
|
|
@@ -172,14 +234,14 @@
|
|
|
172
234
|
|
|
173
235
|
(defn- validate-models!
|
|
174
236
|
"Probe each unique harness:model pair. Prints results and exits if any fail."
|
|
175
|
-
[worker-configs review-
|
|
237
|
+
[worker-configs review-models]
|
|
176
238
|
(let [;; Deduplicate by harness:model only (ignore reasoning level)
|
|
177
|
-
models (
|
|
239
|
+
models (into (->> worker-configs
|
|
178
240
|
(map (fn [wc]
|
|
179
241
|
(let [{:keys [harness model]} (parse-model-string (:model wc))]
|
|
180
242
|
{:harness harness :model model})))
|
|
181
243
|
set)
|
|
182
|
-
|
|
244
|
+
(map #(select-keys % [:harness :model]) review-models))
|
|
183
245
|
_ (println "Validating models...")
|
|
184
246
|
results (pmap (fn [{:keys [harness model]}]
|
|
185
247
|
(let [ok (probe-model harness model)]
|
|
@@ -199,36 +261,257 @@
|
|
|
199
261
|
(System/exit 1))
|
|
200
262
|
(println)))
|
|
201
263
|
|
|
202
|
-
(
|
|
203
|
-
|
|
264
|
+
(def ^:private default-detach-startup-timeout 20)
|
|
265
|
+
|
|
266
|
+
(defn- run-id []
|
|
267
|
+
(subs (str (java.util.UUID/randomUUID)) 0 8))
|
|
268
|
+
|
|
269
|
+
(defn- run-ts []
|
|
270
|
+
(.format (java.time.format.DateTimeFormatter/ofPattern "yyyyMMdd-HHmmss")
|
|
271
|
+
(java.time.LocalDateTime/now)))
|
|
272
|
+
|
|
273
|
+
(defn- default-config-file
|
|
274
|
+
[]
|
|
275
|
+
(cond
|
|
276
|
+
(.exists (io/file "oompa.json")) "oompa.json"
|
|
277
|
+
(.exists (io/file "oompa/oompa.json")) "oompa/oompa.json"
|
|
278
|
+
:else nil))
|
|
279
|
+
|
|
280
|
+
(defn- resolve-config-file
|
|
204
281
|
[opts args]
|
|
205
|
-
(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
282
|
+
(let [candidate (or (:config-file opts)
|
|
283
|
+
(first args)
|
|
284
|
+
(default-config-file))]
|
|
285
|
+
(when candidate
|
|
286
|
+
(.getCanonicalPath (io/file candidate)))))
|
|
287
|
+
|
|
288
|
+
(defn- prepare-log-file!
|
|
289
|
+
"Create oompa/logs and return absolute log path."
|
|
290
|
+
[rid]
|
|
291
|
+
(let [dir (if (.exists (io/file "oompa"))
|
|
292
|
+
(io/file "oompa" "logs")
|
|
293
|
+
(io/file "runs" "logs"))]
|
|
294
|
+
(.mkdirs dir)
|
|
295
|
+
(.getCanonicalPath (io/file dir (str (run-ts) "_" rid ".log")))))
|
|
296
|
+
|
|
297
|
+
(defn- read-file-safe
|
|
298
|
+
[path]
|
|
299
|
+
(try
|
|
300
|
+
(if (.exists (io/file path))
|
|
301
|
+
(slurp path)
|
|
302
|
+
"")
|
|
303
|
+
(catch Exception _
|
|
304
|
+
"")))
|
|
305
|
+
|
|
306
|
+
(defn- tail-lines
|
|
307
|
+
[text n]
|
|
308
|
+
(->> (str/split-lines (or text ""))
|
|
309
|
+
(take-last n)
|
|
310
|
+
(str/join "\n")))
|
|
311
|
+
|
|
312
|
+
(defn- extract-swarm-id
|
|
313
|
+
[text]
|
|
314
|
+
(some->> text
|
|
315
|
+
(re-find #"Swarm ID:\s*([0-9a-f]{8})")
|
|
316
|
+
second))
|
|
317
|
+
|
|
318
|
+
(defn- startup-diagnostic-lines
|
|
319
|
+
[text]
|
|
320
|
+
(->> (str/split-lines (or text ""))
|
|
321
|
+
(filter #(re-find #"ERROR:|FAIL|WARNING:" %))
|
|
322
|
+
(take-last 20)))
|
|
323
|
+
|
|
324
|
+
(defn- print-preflight-warnings!
|
|
325
|
+
[]
|
|
326
|
+
(let [agent-cli? (zero? (:exit (process/sh ["which" "agent-cli"]
|
|
327
|
+
{:out :string :err :string})))]
|
|
328
|
+
(when-not agent-cli?
|
|
329
|
+
(println "WARNING: 'agent-cli' is not on PATH.")
|
|
330
|
+
(println " Model validation may report false model-access failures.")))
|
|
331
|
+
(let [dirty (process/sh ["git" "status" "--porcelain"]
|
|
332
|
+
{:out :string :err :string})
|
|
333
|
+
lines (->> (:out dirty)
|
|
334
|
+
str/split-lines
|
|
335
|
+
(remove str/blank?))]
|
|
336
|
+
(when (seq lines)
|
|
337
|
+
(println (format "WARNING: Git working tree is dirty (%d changed paths)." (count lines)))
|
|
338
|
+
(println " Swarm startup may fail until changes are committed/stashed.")
|
|
339
|
+
(doseq [line (take 20 lines)]
|
|
340
|
+
(println line))
|
|
341
|
+
(when (> (count lines) 20)
|
|
342
|
+
(println (format "... (%d total changed paths)" (count lines)))))))
|
|
343
|
+
|
|
344
|
+
(defn- runtime-classpath-entry
|
|
345
|
+
"Best-effort classpath root for agentnet sources."
|
|
346
|
+
[]
|
|
347
|
+
(or
|
|
348
|
+
(some-> (System/getenv "OOMPA_PACKAGE_ROOT")
|
|
349
|
+
(io/file "agentnet" "src")
|
|
350
|
+
.getCanonicalPath)
|
|
351
|
+
(->> (str/split (or (System/getProperty "java.class.path") "")
|
|
352
|
+
(re-pattern (java.io.File/pathSeparator)))
|
|
353
|
+
(map str/trim)
|
|
354
|
+
(remove str/blank?)
|
|
355
|
+
(map io/file)
|
|
356
|
+
(filter #(.exists %))
|
|
357
|
+
(map #(.getCanonicalPath %))
|
|
358
|
+
(some #(when (str/ends-with? % (str "agentnet" java.io.File/separator "src"))
|
|
359
|
+
%)))
|
|
360
|
+
(.getCanonicalPath (io/file "agentnet" "src"))))
|
|
361
|
+
|
|
362
|
+
(defn- run-classpath
|
|
363
|
+
[]
|
|
364
|
+
(runtime-classpath-entry))
|
|
365
|
+
|
|
366
|
+
(defn- run-script-path
|
|
367
|
+
[]
|
|
368
|
+
(if-let [pkg-root (System/getenv "OOMPA_PACKAGE_ROOT")]
|
|
369
|
+
(.getCanonicalPath (io/file pkg-root "swarm.bb"))
|
|
370
|
+
(let [cp (io/file (runtime-classpath-entry))
|
|
371
|
+
;; cp = <repo>/agentnet/src -> <repo>/swarm.bb
|
|
372
|
+
repo-root (some-> cp .getParentFile .getParentFile)
|
|
373
|
+
candidate (when repo-root (io/file repo-root "swarm.bb"))]
|
|
374
|
+
(if (and candidate (.exists candidate))
|
|
375
|
+
(.getCanonicalPath candidate)
|
|
376
|
+
(.getCanonicalPath (io/file "swarm.bb"))))))
|
|
377
|
+
|
|
378
|
+
(defn- detached-cmd
|
|
379
|
+
[opts config-file]
|
|
380
|
+
(cond-> ["nohup" "bb" "--classpath" (run-classpath) (run-script-path) "swarm"]
|
|
381
|
+
(:dry-run opts) (conj "--dry-run")
|
|
382
|
+
true (conj config-file)))
|
|
383
|
+
|
|
384
|
+
(defn- spawn-detached!
|
|
385
|
+
[cmd log-file]
|
|
386
|
+
(let [log (io/file log-file)
|
|
387
|
+
pb (doto (ProcessBuilder. ^java.util.List cmd)
|
|
388
|
+
(.directory (io/file "."))
|
|
389
|
+
(.redirectInput (java.lang.ProcessBuilder$Redirect/from (io/file "/dev/null")))
|
|
390
|
+
(.redirectOutput (java.lang.ProcessBuilder$Redirect/appendTo log))
|
|
391
|
+
(.redirectError (java.lang.ProcessBuilder$Redirect/appendTo log)))
|
|
392
|
+
proc (.start pb)
|
|
393
|
+
pid (.pid proc)]
|
|
394
|
+
;; Give spawn a short window before validation checks liveness.
|
|
395
|
+
(Thread/sleep 100)
|
|
396
|
+
pid))
|
|
397
|
+
|
|
398
|
+
(defn- pid-alive?
|
|
399
|
+
[pid]
|
|
400
|
+
(zero? (:exit (process/sh ["kill" "-0" (str pid)]
|
|
401
|
+
{:out :string :err :string}))))
|
|
402
|
+
|
|
403
|
+
(defn- wait-for-startup!
|
|
404
|
+
[pid log-file timeout-sec]
|
|
405
|
+
(loop [waited 0]
|
|
406
|
+
(let [content (read-file-safe log-file)
|
|
407
|
+
started? (str/includes? content "Started event written to runs/")
|
|
408
|
+
alive? (pid-alive? pid)]
|
|
409
|
+
(cond
|
|
410
|
+
started?
|
|
411
|
+
{:status :started
|
|
412
|
+
:content content
|
|
413
|
+
:swarm-id (extract-swarm-id content)}
|
|
414
|
+
|
|
415
|
+
(not alive?)
|
|
416
|
+
{:status :failed
|
|
417
|
+
:content content}
|
|
418
|
+
|
|
419
|
+
(>= waited timeout-sec)
|
|
420
|
+
{:status :timeout
|
|
421
|
+
:content content}
|
|
422
|
+
|
|
423
|
+
:else
|
|
424
|
+
(do
|
|
425
|
+
(Thread/sleep 1000)
|
|
426
|
+
(recur (inc waited)))))))
|
|
427
|
+
|
|
428
|
+
(defn- cmd-run-detached
|
|
429
|
+
[opts config-file]
|
|
430
|
+
(print-preflight-warnings!)
|
|
431
|
+
(when-not (.exists (io/file config-file))
|
|
432
|
+
(println (format "Config not found: %s" config-file))
|
|
433
|
+
(System/exit 1))
|
|
434
|
+
(let [timeout-sec (or (:startup-timeout opts)
|
|
435
|
+
(parse-int (System/getenv "OOMPA_DETACH_STARTUP_TIMEOUT")
|
|
436
|
+
default-detach-startup-timeout))
|
|
437
|
+
rid (run-id)
|
|
438
|
+
log-file (prepare-log-file! rid)
|
|
439
|
+
cmd (detached-cmd opts config-file)
|
|
440
|
+
pid (spawn-detached! cmd log-file)]
|
|
441
|
+
(println (format "Config: %s" config-file))
|
|
442
|
+
(when (:dry-run opts)
|
|
443
|
+
(println "Merge mode: dry-run"))
|
|
444
|
+
(let [{:keys [status content swarm-id]} (wait-for-startup! pid log-file timeout-sec)]
|
|
445
|
+
(case status
|
|
446
|
+
:failed
|
|
447
|
+
(do
|
|
226
448
|
(println)
|
|
227
|
-
(
|
|
228
|
-
|
|
449
|
+
(println "ERROR: Detached swarm exited during startup validation.")
|
|
450
|
+
(println "Startup log excerpt:")
|
|
451
|
+
(println (tail-lines content 120))
|
|
452
|
+
(System/exit 1))
|
|
453
|
+
|
|
454
|
+
:timeout
|
|
229
455
|
(do
|
|
230
|
-
(println
|
|
231
|
-
(
|
|
456
|
+
(println)
|
|
457
|
+
(println (format "WARNING: Detached swarm still initializing after %ss." timeout-sec))
|
|
458
|
+
(println "Recent startup log lines:")
|
|
459
|
+
(println (tail-lines content 40)))
|
|
460
|
+
|
|
461
|
+
nil)
|
|
462
|
+
(let [diag (startup-diagnostic-lines content)]
|
|
463
|
+
(when (seq diag)
|
|
464
|
+
(println)
|
|
465
|
+
(println "Startup diagnostics:")
|
|
466
|
+
(doseq [line diag]
|
|
467
|
+
(println line))))
|
|
468
|
+
(println)
|
|
469
|
+
(println " ┌──────────────────────────────────────────────────────────────┐")
|
|
470
|
+
(println " │ OOMPA SWARM RUN (DETACHED) │")
|
|
471
|
+
(println (format " │ Run id: %-46s│" rid))
|
|
472
|
+
(println (format " │ PID: %-46s│" pid))
|
|
473
|
+
(println (format " │ Log file: %-46s│" log-file))
|
|
474
|
+
(println (format " │ Swarm ID: %-46s│" (or swarm-id "(pending)")))
|
|
475
|
+
(println " └──────────────────────────────────────────────────────────────┘")
|
|
476
|
+
(println))))
|
|
477
|
+
|
|
478
|
+
(defn- cmd-run-legacy
|
|
479
|
+
"Run orchestrator once from worker specs (legacy mode)."
|
|
480
|
+
[opts args]
|
|
481
|
+
(let [swarm-id (make-swarm-id)]
|
|
482
|
+
(if-let [specs (:worker-specs opts)]
|
|
483
|
+
;; Mixed worker specs: --workers claude:5 opencode:2
|
|
484
|
+
(let [workers (mapcat
|
|
485
|
+
(fn [spec]
|
|
486
|
+
(let [{:keys [harness count]} spec]
|
|
487
|
+
(map-indexed
|
|
488
|
+
(fn [idx _]
|
|
489
|
+
(worker/create-worker
|
|
490
|
+
{:id (format "%s-%d" (name harness) idx)
|
|
491
|
+
:swarm-id swarm-id
|
|
492
|
+
:harness harness
|
|
493
|
+
:model (:model opts)
|
|
494
|
+
:iterations 1}))
|
|
495
|
+
(range count))))
|
|
496
|
+
specs)]
|
|
497
|
+
(println (format "Running once with mixed workers (swarm %s):" swarm-id))
|
|
498
|
+
(doseq [spec specs]
|
|
499
|
+
(println (format " %dx %s" (:count spec) (name (:harness spec)))))
|
|
500
|
+
(println)
|
|
501
|
+
(worker/run-workers! workers))
|
|
502
|
+
;; Simple mode retired — use oompa.json or --workers harness:count
|
|
503
|
+
(do
|
|
504
|
+
(println "Simple mode is no longer supported. Use oompa.json or --workers harness:count.")
|
|
505
|
+
(System/exit 1)))))
|
|
506
|
+
|
|
507
|
+
(defn cmd-run
|
|
508
|
+
"Run swarm from config. Use --detach for background mode."
|
|
509
|
+
[opts args]
|
|
510
|
+
(if-let [config-file (resolve-config-file opts args)]
|
|
511
|
+
(if (:detach opts)
|
|
512
|
+
(cmd-run-detached opts config-file)
|
|
513
|
+
(cmd-swarm opts [config-file]))
|
|
514
|
+
(cmd-run-legacy opts args)))
|
|
232
515
|
|
|
233
516
|
(defn cmd-loop
|
|
234
517
|
"Run orchestrator N times"
|
|
@@ -257,14 +540,10 @@
|
|
|
257
540
|
(println (format " %dx %s" (:count spec) (name (:harness spec)))))
|
|
258
541
|
(println)
|
|
259
542
|
(worker/run-workers! workers))
|
|
260
|
-
;; Simple mode
|
|
261
|
-
(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
(println (format "Starting %d iterations with %s harness%s..."
|
|
265
|
-
iterations (name (:harness opts)) model-str))
|
|
266
|
-
(println (format "Swarm ID: %s" swarm-id))
|
|
267
|
-
(orchestrator/run-loop! iterations (assoc opts :swarm-id swarm-id))))))
|
|
543
|
+
;; Simple mode retired — use oompa.json or --workers harness:count
|
|
544
|
+
(do
|
|
545
|
+
(println "Simple mode is no longer supported. Use oompa.json or --workers harness:count.")
|
|
546
|
+
(System/exit 1)))))
|
|
268
547
|
|
|
269
548
|
(defn cmd-prompt
|
|
270
549
|
"Run ad-hoc prompt as single task"
|
|
@@ -284,47 +563,72 @@
|
|
|
284
563
|
(orchestrator/run-once! opts))))
|
|
285
564
|
|
|
286
565
|
(defn cmd-status
|
|
287
|
-
"Show
|
|
566
|
+
"Show running swarms."
|
|
288
567
|
[opts args]
|
|
289
568
|
(let [run-ids (runs/list-runs)]
|
|
290
569
|
(if (seq run-ids)
|
|
291
|
-
(let [
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
(
|
|
301
|
-
(if summary
|
|
570
|
+
(let [running (for [id run-ids
|
|
571
|
+
:let [started (runs/read-started id)
|
|
572
|
+
stopped (runs/read-stopped id)
|
|
573
|
+
pid (:pid started)]
|
|
574
|
+
:when (and started (not stopped) pid (pid-alive? pid))]
|
|
575
|
+
{:id id
|
|
576
|
+
:pid pid
|
|
577
|
+
:workers (count (:workers started))
|
|
578
|
+
:work-count (count (runs/list-cycles id))})]
|
|
579
|
+
(if (seq running)
|
|
302
580
|
(do
|
|
303
|
-
(println (format "
|
|
304
|
-
(
|
|
305
|
-
|
|
306
|
-
|
|
581
|
+
(println (format "Running Swarms: %d" (count running)))
|
|
582
|
+
(doseq [r running]
|
|
583
|
+
(println (format " Swarm: %s | PID: %s | Workers: %d | Work Count: %d"
|
|
584
|
+
(:id r) (:pid r) (:workers r) (:work-count r)))))
|
|
585
|
+
(println "No running swarms.")))
|
|
586
|
+
(println "No swarms found."))))
|
|
587
|
+
|
|
588
|
+
(defn cmd-info
|
|
589
|
+
"Show detailed information of a swarm run — reads event-sourced runs/{swarm-id}/ data."
|
|
590
|
+
[opts args]
|
|
591
|
+
(let [run-ids (runs/list-runs)]
|
|
592
|
+
(if (seq run-ids)
|
|
593
|
+
(let [target-ids (if (seq args) [(first args)] run-ids)]
|
|
594
|
+
(doseq [swarm-id target-ids]
|
|
595
|
+
(let [started (runs/read-started swarm-id)
|
|
596
|
+
stopped (runs/read-stopped swarm-id)
|
|
597
|
+
cycles (runs/list-cycles swarm-id)
|
|
598
|
+
reviews (runs/list-reviews swarm-id)]
|
|
599
|
+
(println "--------------------------------------------------")
|
|
600
|
+
(println (format "Swarm: %s" swarm-id))
|
|
601
|
+
(when started
|
|
602
|
+
(println (format " Started: %s" (:started-at started)))
|
|
603
|
+
(println (format " PID: %s" (or (:pid started) "N/A")))
|
|
604
|
+
(println (format " Config: %s" (or (:config-file started) "N/A")))
|
|
605
|
+
(println (format " Workers: %d" (count (:workers started)))))
|
|
307
606
|
(println)
|
|
308
|
-
(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
(:
|
|
312
|
-
(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
(
|
|
326
|
-
|
|
327
|
-
|
|
607
|
+
(if stopped
|
|
608
|
+
(println (format "Stopped: %s (reason: %s%s)"
|
|
609
|
+
(:stopped-at stopped)
|
|
610
|
+
(:reason stopped)
|
|
611
|
+
(if (:error stopped)
|
|
612
|
+
(str ", error: " (:error stopped))
|
|
613
|
+
"")))
|
|
614
|
+
(println " (still running — no stopped event yet)"))
|
|
615
|
+
(when (seq cycles)
|
|
616
|
+
(println)
|
|
617
|
+
(println (format "Cycles: %d total" (count cycles)))
|
|
618
|
+
(doseq [c cycles]
|
|
619
|
+
(println (format " %s-c%d: %s (%dms, claimed: %s)"
|
|
620
|
+
(:worker-id c) (:cycle c)
|
|
621
|
+
(:outcome c)
|
|
622
|
+
(or (:duration-ms c) 0)
|
|
623
|
+
(str/join ", " (or (:claimed-task-ids c) []))))))
|
|
624
|
+
(when (seq reviews)
|
|
625
|
+
(println)
|
|
626
|
+
(println (format "Reviews: %d total" (count reviews)))
|
|
627
|
+
(doseq [r reviews]
|
|
628
|
+
(println (format " %s-c%d-r%d: %s"
|
|
629
|
+
(:worker-id r) (:cycle r) (:round r)
|
|
630
|
+
(:verdict r)))))
|
|
631
|
+
(println))))
|
|
328
632
|
;; Fall back to legacy JSONL format
|
|
329
633
|
(let [runs-dir (io/file "runs")
|
|
330
634
|
files (when (.exists runs-dir)
|
|
@@ -344,6 +648,166 @@
|
|
|
344
648
|
(println (format "Total: %d tasks" (count entries))))))
|
|
345
649
|
(println "No runs found."))))))
|
|
346
650
|
|
|
651
|
+
(def ^:private error-outcomes
|
|
652
|
+
#{"error" "merge-failed" "rejected" "stuck"})
|
|
653
|
+
|
|
654
|
+
(def ^:private terminal-run-outcomes
|
|
655
|
+
#{"merged" "rejected" "error" "merge-failed" "sync-failed" "stuck" "no-changes"})
|
|
656
|
+
|
|
657
|
+
(defn- run-state
|
|
658
|
+
"Derive run lifecycle state from started/stopped events + PID liveness."
|
|
659
|
+
[started stopped]
|
|
660
|
+
(cond
|
|
661
|
+
(nil? started) "missing-started"
|
|
662
|
+
stopped (str "stopped/" (:reason stopped))
|
|
663
|
+
(pid-alive? (:pid started)) "running"
|
|
664
|
+
:else "stale"))
|
|
665
|
+
|
|
666
|
+
(defn- latest-cycles-by-worker
|
|
667
|
+
"Return map of worker-id -> latest cycle entry."
|
|
668
|
+
[cycles]
|
|
669
|
+
(reduce (fn [acc c]
|
|
670
|
+
(let [wid (:worker-id c)
|
|
671
|
+
prev (get acc wid)]
|
|
672
|
+
(if (or (nil? prev)
|
|
673
|
+
(> (or (:cycle c) 0) (or (:cycle prev) 0)))
|
|
674
|
+
(assoc acc wid c)
|
|
675
|
+
acc)))
|
|
676
|
+
{}
|
|
677
|
+
cycles))
|
|
678
|
+
|
|
679
|
+
(defn- worker-runtime
|
|
680
|
+
"Best-effort worker runtime classification for view output."
|
|
681
|
+
[worker latest-cycle worker-cycles run-state*]
|
|
682
|
+
(let [run-max (or (:runs worker) (:iterations worker) 0)
|
|
683
|
+
runs-done (count (filter #(terminal-run-outcomes (:outcome %)) worker-cycles))
|
|
684
|
+
outcome (or (:outcome latest-cycle) "-")]
|
|
685
|
+
(cond
|
|
686
|
+
(>= runs-done run-max) "completed"
|
|
687
|
+
(str/starts-with? run-state* "stopped/") "stopped"
|
|
688
|
+
(= run-state* "stale") "stale"
|
|
689
|
+
(nil? latest-cycle) "starting"
|
|
690
|
+
(= outcome "working") "working"
|
|
691
|
+
(= outcome "executor-done") "idle"
|
|
692
|
+
:else outcome)))
|
|
693
|
+
|
|
694
|
+
(defn- model-label
|
|
695
|
+
[{:keys [harness model reasoning]}]
|
|
696
|
+
(str harness ":" model (when reasoning (str ":" reasoning))))
|
|
697
|
+
|
|
698
|
+
(defn- run-metrics
|
|
699
|
+
"Summarize cycle metrics for a run."
|
|
700
|
+
[cycles]
|
|
701
|
+
(let [merged (count (filter #(= "merged" (:outcome %)) cycles))
|
|
702
|
+
failed (count (filter #(error-outcomes (:outcome %)) cycles))
|
|
703
|
+
claimed-all (->> cycles
|
|
704
|
+
(mapcat #(or (:claimed-task-ids %) []))
|
|
705
|
+
(remove str/blank?))
|
|
706
|
+
completed-ids (->> cycles
|
|
707
|
+
(filter #(= "merged" (:outcome %)))
|
|
708
|
+
(mapcat #(or (:claimed-task-ids %) []))
|
|
709
|
+
(remove str/blank?)
|
|
710
|
+
set)]
|
|
711
|
+
{:merged merged
|
|
712
|
+
:failed failed
|
|
713
|
+
:claimed (count (set claimed-all))
|
|
714
|
+
:completed (count completed-ids)}))
|
|
715
|
+
|
|
716
|
+
(defn- cmd-view-one
|
|
717
|
+
[swarm-id]
|
|
718
|
+
(if-let [started (runs/read-started swarm-id)]
|
|
719
|
+
(let [stopped (runs/read-stopped swarm-id)
|
|
720
|
+
cycles (or (runs/list-cycles swarm-id) [])
|
|
721
|
+
reviews (or (runs/list-reviews swarm-id) [])
|
|
722
|
+
workers (or (:workers started) [])
|
|
723
|
+
run-state* (run-state started stopped)
|
|
724
|
+
metrics (run-metrics cycles)
|
|
725
|
+
latest-by-worker (latest-cycles-by-worker cycles)
|
|
726
|
+
cycles-by-worker (group-by :worker-id cycles)]
|
|
727
|
+
(println (format "Swarm: %s" swarm-id))
|
|
728
|
+
(println (format "State: %s" run-state*))
|
|
729
|
+
(println (format "Started: %s" (:started-at started)))
|
|
730
|
+
(println (format "PID: %s" (or (:pid started) "N/A")))
|
|
731
|
+
(println (format "Config: %s" (or (:config-file started) "N/A")))
|
|
732
|
+
(when stopped
|
|
733
|
+
(println (format "Stopped: %s" (:stopped-at stopped))))
|
|
734
|
+
(println (format "Cycles: %d" (count cycles)))
|
|
735
|
+
(println (format "PRs: merged=%d failed=%d" (:merged metrics) (:failed metrics)))
|
|
736
|
+
(println (format "Tasks: claimed=%d completed=%d created=n/a"
|
|
737
|
+
(:claimed metrics) (:completed metrics)))
|
|
738
|
+
(println (format "Reviews: %d" (count reviews)))
|
|
739
|
+
(println)
|
|
740
|
+
(println "Workers:")
|
|
741
|
+
(println "ID | Runtime | Runs | Cycles | Last Outcome | Claimed | Model")
|
|
742
|
+
(println "----+-----------+--------+---------+----------------+---------+------------------------------")
|
|
743
|
+
(doseq [w (sort-by :id workers)]
|
|
744
|
+
(let [wid (:id w)
|
|
745
|
+
latest (get latest-by-worker wid)
|
|
746
|
+
worker-cycles (or (get cycles-by-worker wid) [])
|
|
747
|
+
run-max (or (:runs w) (:iterations w) 0)
|
|
748
|
+
runs-done (count (filter #(terminal-run-outcomes (:outcome %)) worker-cycles))
|
|
749
|
+
cycles-done (or (:cycle latest) 0)
|
|
750
|
+
runtime (worker-runtime w latest worker-cycles run-state*)
|
|
751
|
+
outcome (or (:outcome latest) "-")
|
|
752
|
+
claimed (count (or (:claimed-task-ids latest) []))]
|
|
753
|
+
(println (format "%-3s | %-9s | %4d/%-3d | %7d | %-14s | %-7d | %s"
|
|
754
|
+
wid runtime runs-done run-max cycles-done outcome claimed (model-label w))))))
|
|
755
|
+
(do
|
|
756
|
+
(println (format "Swarm not found: %s" swarm-id))
|
|
757
|
+
(System/exit 1))))
|
|
758
|
+
|
|
759
|
+
(defn cmd-list
|
|
760
|
+
"List recent swarms with liveness + activity metrics.
|
|
761
|
+
Default: 20 most recent. Use --all for full history."
|
|
762
|
+
[opts args]
|
|
763
|
+
(let [run-ids (or (runs/list-runs) [])]
|
|
764
|
+
(if-not (seq run-ids)
|
|
765
|
+
(println "No swarm runs found.")
|
|
766
|
+
(let [shown (if (:all opts) run-ids (take 20 run-ids))]
|
|
767
|
+
(println "Swarm Runs:")
|
|
768
|
+
(println "ID | State | PID | Workers | Active | Cycles | Merged | Failed | Done | Started")
|
|
769
|
+
(println "---------+------------------+--------+---------+--------+--------+--------+--------+------+-------------------------")
|
|
770
|
+
(doseq [rid shown]
|
|
771
|
+
(let [started (runs/read-started rid)
|
|
772
|
+
stopped (runs/read-stopped rid)
|
|
773
|
+
cycles (or (runs/list-cycles rid) [])
|
|
774
|
+
workers (or (:workers started) [])
|
|
775
|
+
metrics (run-metrics cycles)
|
|
776
|
+
latest-by-worker (latest-cycles-by-worker cycles)
|
|
777
|
+
cycles-by-worker (group-by :worker-id cycles)
|
|
778
|
+
state* (run-state started stopped)
|
|
779
|
+
active-count (if (= state* "running")
|
|
780
|
+
(count (filter (fn [w]
|
|
781
|
+
(let [wid (:id w)
|
|
782
|
+
run-max (or (:runs w) (:iterations w) 0)
|
|
783
|
+
runs-done (count (filter #(terminal-run-outcomes (:outcome %))
|
|
784
|
+
(or (get cycles-by-worker wid) [])))]
|
|
785
|
+
(< runs-done run-max)))
|
|
786
|
+
workers))
|
|
787
|
+
0)]
|
|
788
|
+
(println (format "%-8s | %-16s | %-6s | %7d | %6d | %6d | %6d | %6d | %4d | %s"
|
|
789
|
+
rid
|
|
790
|
+
state*
|
|
791
|
+
(or (:pid started) "-")
|
|
792
|
+
(count workers)
|
|
793
|
+
active-count
|
|
794
|
+
(count cycles)
|
|
795
|
+
(:merged metrics)
|
|
796
|
+
(:failed metrics)
|
|
797
|
+
(:completed metrics)
|
|
798
|
+
(or (:started-at started) "-")))))
|
|
799
|
+
(when (and (not (:all opts)) (> (count run-ids) 20))
|
|
800
|
+
(println (format "\nShowing 20 of %d runs. Use --all for full history." (count run-ids))))
|
|
801
|
+
(println)
|
|
802
|
+
(println "Use `oompa view <swarm-id>` for detailed single-swarm info.")))))
|
|
803
|
+
|
|
804
|
+
(defn cmd-view
|
|
805
|
+
"Show detailed runtime for one swarm (default: latest run)."
|
|
806
|
+
[opts args]
|
|
807
|
+
(if-let [swarm-id (or (first args) (first (runs/list-runs)))]
|
|
808
|
+
(cmd-view-one swarm-id)
|
|
809
|
+
(println "No swarm runs found.")))
|
|
810
|
+
|
|
347
811
|
(defn cmd-worktrees
|
|
348
812
|
"List worktree status"
|
|
349
813
|
[opts args]
|
|
@@ -382,21 +846,39 @@
|
|
|
382
846
|
"Check if agent backends are available"
|
|
383
847
|
[opts args]
|
|
384
848
|
(println "Checking agent backends...")
|
|
385
|
-
(doseq [
|
|
386
|
-
(let [available? (
|
|
849
|
+
(doseq [harness-kw (sort (harness/known-harnesses))]
|
|
850
|
+
(let [available? (harness/check-available harness-kw)]
|
|
387
851
|
(println (format " %s: %s"
|
|
388
|
-
(name
|
|
852
|
+
(name harness-kw)
|
|
389
853
|
(if available? "✓ available" "✗ not found"))))))
|
|
390
854
|
|
|
855
|
+
(def ^:private reasoning-variants
|
|
856
|
+
#{"minimal" "low" "medium" "high" "max" "xhigh"})
|
|
857
|
+
|
|
391
858
|
(defn- parse-model-string
|
|
392
859
|
"Parse model string into {:harness :model :reasoning}.
|
|
393
|
-
|
|
860
|
+
|
|
861
|
+
Supported formats:
|
|
862
|
+
- harness:model
|
|
863
|
+
- harness:model:reasoning (if reasoning is in reasoning-variants)
|
|
864
|
+
- model (defaults harness to :codex)
|
|
865
|
+
|
|
866
|
+
Note: model identifiers may contain ':' (for example openrouter/...:free).
|
|
867
|
+
Those suffixes are preserved in :model if not a known reasoning variant."
|
|
394
868
|
[s]
|
|
395
869
|
(if (and s (str/includes? s ":"))
|
|
396
|
-
(let [
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
870
|
+
(let [[harness-str rest*] (str/split s #":" 2)
|
|
871
|
+
harness (keyword harness-str)]
|
|
872
|
+
(if (harness/valid-harness? harness)
|
|
873
|
+
;; Check for reasoning suffix for any valid harness
|
|
874
|
+
(if-let [idx (str/last-index-of rest* ":")]
|
|
875
|
+
(let [model* (subs rest* 0 idx)
|
|
876
|
+
reasoning* (subs rest* (inc idx))]
|
|
877
|
+
(if (contains? reasoning-variants reasoning*)
|
|
878
|
+
{:harness harness :model model* :reasoning reasoning*}
|
|
879
|
+
{:harness harness :model rest*}))
|
|
880
|
+
{:harness harness :model rest*})
|
|
881
|
+
;; Not a known harness prefix, treat as raw model on default harness.
|
|
400
882
|
{:harness :codex :model s}))
|
|
401
883
|
{:harness :codex :model s}))
|
|
402
884
|
|
|
@@ -414,7 +896,7 @@
|
|
|
414
896
|
(println " \"workers\": [")
|
|
415
897
|
(println " {\"model\": \"codex:gpt-5.3-codex:medium\", \"prompt\": \"prompts/executor.md\", \"iterations\": 10, \"count\": 3, \"can_plan\": false},")
|
|
416
898
|
(println " {\"model\": \"claude:opus\", \"prompt\": [\"prompts/base.md\", \"prompts/planner.md\"], \"count\": 1},")
|
|
417
|
-
(println " {\"model\": \"
|
|
899
|
+
(println " {\"model\": \"gemini:gemini-3-pro-preview\", \"prompt\": [\"prompts/executor.md\"], \"count\": 1}")
|
|
418
900
|
(println " ]")
|
|
419
901
|
(println "}")
|
|
420
902
|
(println)
|
|
@@ -422,25 +904,21 @@
|
|
|
422
904
|
(System/exit 1))
|
|
423
905
|
;; Preflight: abort if git is dirty to prevent merge conflicts
|
|
424
906
|
(check-git-clean!)
|
|
907
|
+
;; Preflight: abort if stale worktrees from prior runs would poison git
|
|
908
|
+
(check-stale-worktrees!)
|
|
425
909
|
|
|
426
910
|
(let [config (json/parse-string (slurp f) true)
|
|
427
911
|
;; Parse reviewer config — supports both formats:
|
|
428
912
|
;; Legacy: {"review_model": "harness:model:reasoning"}
|
|
429
913
|
;; New: {"reviewer": {"model": "harness:model:reasoning", "prompt": ["path.md"]}}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
(assoc parsed :prompts prompts))
|
|
439
|
-
|
|
440
|
-
(:review_model config)
|
|
441
|
-
(parse-model-string (:review_model config))
|
|
442
|
-
|
|
443
|
-
:else nil)
|
|
914
|
+
generic-reviewers (cond
|
|
915
|
+
(:review_models config)
|
|
916
|
+
(mapv parse-model-string (:review_models config))
|
|
917
|
+
|
|
918
|
+
(:review_model config)
|
|
919
|
+
[(parse-model-string (:review_model config))]
|
|
920
|
+
|
|
921
|
+
:else [])
|
|
444
922
|
|
|
445
923
|
;; Parse planner config — optional dedicated planner
|
|
446
924
|
;; Runs in project root, no worktree/review/merge, respects max_pending backpressure
|
|
@@ -466,19 +944,35 @@
|
|
|
466
944
|
;; Convert to worker format
|
|
467
945
|
workers (map-indexed
|
|
468
946
|
(fn [idx wc]
|
|
469
|
-
(let [{:keys [harness model reasoning]} (parse-model-string (:model wc))
|
|
947
|
+
(let [{:keys [harness model reasoning]} (parse-model-string (:model wc))
|
|
948
|
+
;; Support per-worker reviewer override
|
|
949
|
+
worker-reviewer-config (:reviewer wc)
|
|
950
|
+
specific-reviewer (when worker-reviewer-config
|
|
951
|
+
(let [parsed (parse-model-string (:model worker-reviewer-config))
|
|
952
|
+
prompts (let [p (:prompt worker-reviewer-config)]
|
|
953
|
+
(cond (vector? p) p
|
|
954
|
+
(string? p) [p]
|
|
955
|
+
:else []))]
|
|
956
|
+
(assoc parsed :prompts prompts)))
|
|
957
|
+
all-reviewers (->> (concat (if specific-reviewer [specific-reviewer] []) generic-reviewers)
|
|
958
|
+
(map #(select-keys % [:harness :model :reasoning :prompts]))
|
|
959
|
+
(distinct)
|
|
960
|
+
(vec))]
|
|
470
961
|
(worker/create-worker
|
|
471
962
|
{:id (str "w" idx)
|
|
472
963
|
:swarm-id swarm-id
|
|
473
964
|
:harness harness
|
|
474
965
|
:model model
|
|
475
966
|
:reasoning reasoning
|
|
967
|
+
:runs (or (:runs wc) (:iterations wc) 10)
|
|
968
|
+
:max-cycles (or (:max_cycles wc) (:iterations wc) (:runs wc) 10)
|
|
476
969
|
:iterations (or (:iterations wc) 10)
|
|
477
970
|
:prompts (:prompt wc)
|
|
478
971
|
:can-plan (:can_plan wc)
|
|
479
|
-
:
|
|
480
|
-
:
|
|
481
|
-
:
|
|
972
|
+
:wait-between (:wait_between wc)
|
|
973
|
+
:max-wait-for-tasks (:max_wait_for_tasks wc)
|
|
974
|
+
:max-working-resumes (:max_working_resumes wc)
|
|
975
|
+
:reviewers all-reviewers})))
|
|
482
976
|
expanded-workers)]
|
|
483
977
|
|
|
484
978
|
(println (format "Swarm config from %s:" config-file))
|
|
@@ -491,22 +985,19 @@
|
|
|
491
985
|
(if (seq (:prompts planner-parsed))
|
|
492
986
|
(str ", prompts: " (str/join ", " (:prompts planner-parsed)))
|
|
493
987
|
""))))
|
|
494
|
-
(when
|
|
495
|
-
(println (format "
|
|
496
|
-
(name (:harness
|
|
497
|
-
(:model review-parsed)
|
|
498
|
-
(if (seq (:prompts review-parsed))
|
|
499
|
-
(str " (prompts: " (str/join ", " (:prompts review-parsed)) ")")
|
|
500
|
-
""))))
|
|
988
|
+
(when (seq generic-reviewers)
|
|
989
|
+
(println (format " Generic Reviewers: %s"
|
|
990
|
+
(str/join ", " (map #(str (name (:harness %)) ":" (:model %)) generic-reviewers)))))
|
|
501
991
|
(println (format " Workers: %d total" (count workers)))
|
|
502
992
|
(doseq [[idx wc] (map-indexed vector worker-configs)]
|
|
503
993
|
(let [{:keys [harness model reasoning]} (parse-model-string (:model wc))]
|
|
504
|
-
(println (format " - %dx %s:%s%s (%d
|
|
994
|
+
(println (format " - %dx %s:%s%s (%d runs, %d cycle cap%s)"
|
|
505
995
|
(or (:count wc) 1)
|
|
506
996
|
(name harness)
|
|
507
997
|
model
|
|
508
998
|
(if reasoning (str ":" reasoning) "")
|
|
509
|
-
(or (:iterations wc) 10)
|
|
999
|
+
(or (:runs wc) (:iterations wc) 10)
|
|
1000
|
+
(or (:max_cycles wc) (:iterations wc) (:runs wc) 10)
|
|
510
1001
|
(if (:prompt wc) (str ", " (:prompt wc)) "")))))
|
|
511
1002
|
(println)
|
|
512
1003
|
|
|
@@ -514,15 +1005,15 @@
|
|
|
514
1005
|
;; Include planner model in validation if configured
|
|
515
1006
|
(validate-models! (cond-> worker-configs
|
|
516
1007
|
planner-config (conj planner-config))
|
|
517
|
-
|
|
1008
|
+
generic-reviewers)
|
|
518
1009
|
|
|
519
|
-
;; Write
|
|
520
|
-
(runs/write-
|
|
1010
|
+
;; Write started event to runs/{swarm-id}/started.json
|
|
1011
|
+
(runs/write-started! swarm-id
|
|
521
1012
|
{:workers workers
|
|
522
1013
|
:planner-config planner-parsed
|
|
523
|
-
:reviewer-
|
|
1014
|
+
:reviewer-configs generic-reviewers
|
|
524
1015
|
:config-file config-file})
|
|
525
|
-
(println (format "\
|
|
1016
|
+
(println (format "\nStarted event written to runs/%s/started.json" swarm-id))
|
|
526
1017
|
|
|
527
1018
|
;; Run planner if configured — synchronously before workers
|
|
528
1019
|
(when planner-parsed
|
|
@@ -555,6 +1046,69 @@
|
|
|
555
1046
|
(doseq [t (tasks/list-current)]
|
|
556
1047
|
(println (format " - %s: %s" (:id t) (:summary t)))))))
|
|
557
1048
|
|
|
1049
|
+
(defn- find-latest-swarm-id
|
|
1050
|
+
"Find the most recent swarm ID from runs/ directory."
|
|
1051
|
+
[]
|
|
1052
|
+
(first (runs/list-runs)))
|
|
1053
|
+
|
|
1054
|
+
(defn- read-swarm-pid
|
|
1055
|
+
"Read PID from started.json for a swarm. Returns nil if not found."
|
|
1056
|
+
[swarm-id]
|
|
1057
|
+
(when-let [started (runs/read-started swarm-id)]
|
|
1058
|
+
(:pid started)))
|
|
1059
|
+
|
|
1060
|
+
(defn- pid-alive?
|
|
1061
|
+
"Check if a process is alive via kill -0."
|
|
1062
|
+
[pid]
|
|
1063
|
+
(try
|
|
1064
|
+
(zero? (:exit (process/sh ["kill" "-0" (str pid)]
|
|
1065
|
+
{:out :string :err :string})))
|
|
1066
|
+
(catch Exception _ false)))
|
|
1067
|
+
|
|
1068
|
+
(defn cmd-stop
|
|
1069
|
+
"Send SIGTERM to running swarm — workers finish current cycle then exit"
|
|
1070
|
+
[opts args]
|
|
1071
|
+
(let [swarm-id (or (first args) (find-latest-swarm-id))]
|
|
1072
|
+
(if-not swarm-id
|
|
1073
|
+
(println "No swarm runs found.")
|
|
1074
|
+
(let [stopped (runs/read-stopped swarm-id)]
|
|
1075
|
+
(if stopped
|
|
1076
|
+
(println (format "Swarm %s already stopped (reason: %s)" swarm-id (:reason stopped)))
|
|
1077
|
+
(let [pid (read-swarm-pid swarm-id)]
|
|
1078
|
+
(if-not pid
|
|
1079
|
+
(println (format "No PID found for swarm %s" swarm-id))
|
|
1080
|
+
(if-not (pid-alive? pid)
|
|
1081
|
+
(do
|
|
1082
|
+
(println (format "Swarm %s PID %s is not running (stale). Writing stopped event." swarm-id pid))
|
|
1083
|
+
(runs/write-stopped! swarm-id :interrupted))
|
|
1084
|
+
(do
|
|
1085
|
+
(println (format "Sending SIGTERM to swarm %s (PID %s)..." swarm-id pid))
|
|
1086
|
+
(println "Workers will finish their current cycle and exit.")
|
|
1087
|
+
(process/sh ["kill" (str pid)]))))))))))
|
|
1088
|
+
|
|
1089
|
+
(defn cmd-kill
|
|
1090
|
+
"Send SIGKILL to running swarm — immediate termination"
|
|
1091
|
+
[opts args]
|
|
1092
|
+
(let [swarm-id (or (first args) (find-latest-swarm-id))]
|
|
1093
|
+
(if-not swarm-id
|
|
1094
|
+
(println "No swarm runs found.")
|
|
1095
|
+
(let [stopped (runs/read-stopped swarm-id)]
|
|
1096
|
+
(if stopped
|
|
1097
|
+
(println (format "Swarm %s already stopped (reason: %s)" swarm-id (:reason stopped)))
|
|
1098
|
+
(let [pid (read-swarm-pid swarm-id)]
|
|
1099
|
+
(if-not pid
|
|
1100
|
+
(println (format "No PID found for swarm %s" swarm-id))
|
|
1101
|
+
(if-not (pid-alive? pid)
|
|
1102
|
+
(do
|
|
1103
|
+
(println (format "Swarm %s PID %s is not running (stale). Writing stopped event." swarm-id pid))
|
|
1104
|
+
(runs/write-stopped! swarm-id :interrupted))
|
|
1105
|
+
(do
|
|
1106
|
+
(println (format "Sending SIGKILL to swarm %s (PID %s)..." swarm-id pid))
|
|
1107
|
+
;; SIGKILL bypasses JVM shutdown hooks, so write stopped.json here
|
|
1108
|
+
(process/sh ["kill" "-9" (str pid)])
|
|
1109
|
+
(runs/write-stopped! swarm-id :interrupted)
|
|
1110
|
+
(println "Swarm killed."))))))))))
|
|
1111
|
+
|
|
558
1112
|
(defn cmd-help
|
|
559
1113
|
"Print usage information"
|
|
560
1114
|
[opts args]
|
|
@@ -563,32 +1117,66 @@
|
|
|
563
1117
|
(println "Usage: ./swarm.bb <command> [options]")
|
|
564
1118
|
(println)
|
|
565
1119
|
(println "Commands:")
|
|
566
|
-
(println " run
|
|
1120
|
+
(println " run [file] Run swarm from config (default: oompa.json, oompa/oompa.json)")
|
|
567
1121
|
(println " loop N Run N iterations")
|
|
568
1122
|
(println " swarm [file] Run multiple worker configs from oompa.json (parallel)")
|
|
569
1123
|
(println " tasks Show task status (pending/current/complete)")
|
|
570
1124
|
(println " prompt \"...\" Run ad-hoc prompt")
|
|
571
|
-
(println " status Show
|
|
1125
|
+
(println " status Show running swarms")
|
|
1126
|
+
(println " info Show detailed summary of the last run")
|
|
1127
|
+
(println " list List recent swarms (default: 20, --all for full history)")
|
|
1128
|
+
(println " view [swarm-id] Show detailed single-swarm runtime (default: latest)")
|
|
572
1129
|
(println " worktrees List worktree status")
|
|
1130
|
+
(println " stop [swarm-id] Stop swarm gracefully (finish current cycle)")
|
|
1131
|
+
(println " kill [swarm-id] Kill swarm immediately (SIGKILL)")
|
|
573
1132
|
(println " cleanup Remove all worktrees")
|
|
574
1133
|
(println " context Print context block")
|
|
575
1134
|
(println " check Check agent backends")
|
|
576
1135
|
(println " help Show this help")
|
|
1136
|
+
(println " docs Dump all core architecture and swarm design docs")
|
|
577
1137
|
(println)
|
|
578
1138
|
(println "Options:")
|
|
579
1139
|
(println " --workers N Number of parallel workers (default: 2)")
|
|
580
1140
|
(println " --workers H:N [H:N ...] Mixed workers by harness (e.g., claude:5 opencode:2)")
|
|
1141
|
+
(println " --all Show full history for list command")
|
|
1142
|
+
(println " --config PATH Config file for run/swarm")
|
|
1143
|
+
(println " --detach Run in background (run command)")
|
|
1144
|
+
(println " --startup-timeout N Detached startup validation window in seconds")
|
|
581
1145
|
(println " --iterations N Number of iterations per worker (default: 1)")
|
|
582
|
-
(println " --harness {
|
|
583
|
-
(println " --model MODEL Model to use (e.g., codex-5.
|
|
1146
|
+
(println (str " --harness {" (str/join "," (map name (sort harnesses))) "} Agent harness to use (default: codex)"))
|
|
1147
|
+
(println " --model MODEL Model to use (e.g., codex:gpt-5.3-codex:medium, claude:opus, gemini:gemini-3-pro-preview)")
|
|
584
1148
|
(println " --dry-run Skip actual merges")
|
|
585
1149
|
(println " --keep-worktrees Don't cleanup worktrees after run")
|
|
586
1150
|
(println)
|
|
587
1151
|
(println "Examples:")
|
|
1152
|
+
(println " ./swarm.bb list")
|
|
1153
|
+
(println " ./swarm.bb list --all")
|
|
1154
|
+
(println " ./swarm.bb view 6cd50f5a")
|
|
1155
|
+
(println " ./swarm.bb run --detach --config oompa/oompa_overnight_self_healing.json")
|
|
588
1156
|
(println " ./swarm.bb loop 10 --harness codex --model gpt-5.3-codex --workers 3")
|
|
589
1157
|
(println " ./swarm.bb loop --workers claude:5 opencode:2 --iterations 20")
|
|
590
1158
|
(println " ./swarm.bb swarm oompa.json # Run multi-model config"))
|
|
591
1159
|
|
|
1160
|
+
(defn cmd-docs
|
|
1161
|
+
"Dump core architecture and design documents"
|
|
1162
|
+
[opts args]
|
|
1163
|
+
(let [docs-dir "docs"
|
|
1164
|
+
core-docs ["SWARM_PHILOSOPHY.md" "SWARM_GUIDE.md" "EDN_TICKETS.md" "SYSTEMS_DESIGN.md" "OOMPA.md"]
|
|
1165
|
+
package-dir (or (System/getenv "OOMPA_PACKAGE_ROOT") ".")
|
|
1166
|
+
doc-paths (map #(str package-dir "/" docs-dir "/" %) core-docs)]
|
|
1167
|
+
(println "# Oompa Loompas Core Documentation")
|
|
1168
|
+
(println)
|
|
1169
|
+
(doseq [path doc-paths]
|
|
1170
|
+
(try
|
|
1171
|
+
(let [content (slurp path)]
|
|
1172
|
+
(println (str "## " path))
|
|
1173
|
+
(println "```markdown")
|
|
1174
|
+
(println content)
|
|
1175
|
+
(println "```")
|
|
1176
|
+
(println))
|
|
1177
|
+
(catch Exception e
|
|
1178
|
+
(println (str "Could not read " path ": " (.getMessage e))))))))
|
|
1179
|
+
|
|
592
1180
|
;; =============================================================================
|
|
593
1181
|
;; Main Entry Point
|
|
594
1182
|
;; =============================================================================
|
|
@@ -600,22 +1188,28 @@
|
|
|
600
1188
|
"tasks" cmd-tasks
|
|
601
1189
|
"prompt" cmd-prompt
|
|
602
1190
|
"status" cmd-status
|
|
1191
|
+
"info" cmd-info
|
|
1192
|
+
"list" cmd-list
|
|
1193
|
+
"view" cmd-view
|
|
1194
|
+
"stop" cmd-stop
|
|
1195
|
+
"kill" cmd-kill
|
|
603
1196
|
"worktrees" cmd-worktrees
|
|
604
1197
|
"cleanup" cmd-cleanup
|
|
605
1198
|
"context" cmd-context
|
|
606
1199
|
"check" cmd-check
|
|
607
|
-
"help" cmd-help
|
|
1200
|
+
"help" cmd-help
|
|
1201
|
+
"docs" cmd-docs})
|
|
608
1202
|
|
|
609
1203
|
(defn -main [& args]
|
|
610
1204
|
(let [[cmd & rest-args] args]
|
|
611
1205
|
(if-let [handler (get commands cmd)]
|
|
612
|
-
(
|
|
613
|
-
(
|
|
614
|
-
(handler opts args)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1206
|
+
(try
|
|
1207
|
+
(let [{:keys [opts args]} (parse-args rest-args)]
|
|
1208
|
+
(handler opts args))
|
|
1209
|
+
(catch Exception e
|
|
1210
|
+
(binding [*out* *err*]
|
|
1211
|
+
(println (format "Error: %s" (.getMessage e))))
|
|
1212
|
+
(System/exit 1)))
|
|
619
1213
|
(do
|
|
620
1214
|
(cmd-help {} [])
|
|
621
1215
|
(when cmd
|