@nbardy/oompa 0.6.0 → 0.7.1
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 +43 -16
- package/agentnet/src/agentnet/agent.clj +144 -7
- package/agentnet/src/agentnet/cli.clj +313 -60
- package/agentnet/src/agentnet/harness.clj +217 -0
- package/agentnet/src/agentnet/orchestrator.clj +3 -1
- package/agentnet/src/agentnet/runs.clj +190 -0
- package/agentnet/src/agentnet/schema.clj +4 -4
- package/agentnet/src/agentnet/tasks.clj +48 -0
- package/agentnet/src/agentnet/worker.clj +875 -339
- package/bin/test-models +1 -1
- package/config/prompts/_agent_scope_rules.md +7 -0
- package/config/prompts/_task_header.md +16 -48
- package/config/prompts/cto.md +2 -0
- package/config/prompts/engineer.md +2 -0
- package/config/prompts/executor.md +2 -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 +17 -2
- package/package.json +3 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
./swarm.bb run # Run all tasks once
|
|
6
6
|
./swarm.bb run --workers 4 # With 4 parallel workers
|
|
7
7
|
./swarm.bb loop 20 --harness claude # 20 iterations with Claude
|
|
8
|
-
./swarm.bb loop --workers claude:5
|
|
8
|
+
./swarm.bb loop --workers claude:5 opencode:2 --iterations 20 # Mixed harnesses
|
|
9
9
|
./swarm.bb swarm oompa.json # Multi-model from config
|
|
10
10
|
./swarm.bb prompt \"...\" # Ad-hoc task
|
|
11
11
|
./swarm.bb status # Show last run
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
[agentnet.worker :as worker]
|
|
17
17
|
[agentnet.tasks :as tasks]
|
|
18
18
|
[agentnet.agent :as agent]
|
|
19
|
+
[agentnet.harness :as harness]
|
|
20
|
+
[agentnet.runs :as runs]
|
|
19
21
|
[babashka.process :as process]
|
|
20
22
|
[clojure.string :as str]
|
|
21
23
|
[clojure.java.io :as io]
|
|
@@ -30,20 +32,22 @@
|
|
|
30
32
|
(Integer/parseInt s)
|
|
31
33
|
(catch Exception _ default)))
|
|
32
34
|
|
|
35
|
+
(def ^:private harnesses (harness/known-harnesses))
|
|
36
|
+
|
|
33
37
|
(defn- make-swarm-id
|
|
34
38
|
"Generate a short run-level swarm ID."
|
|
35
39
|
[]
|
|
36
40
|
(subs (str (java.util.UUID/randomUUID)) 0 8))
|
|
37
41
|
|
|
38
42
|
(defn- parse-worker-spec
|
|
39
|
-
"Parse 'harness:count' into {:harness :
|
|
43
|
+
"Parse 'harness:count' into {:harness :opencode, :count 5}.
|
|
40
44
|
Throws on invalid format."
|
|
41
45
|
[s]
|
|
42
46
|
(let [[harness count-str] (str/split s #":" 2)
|
|
43
47
|
h (keyword harness)
|
|
44
48
|
cnt (parse-int count-str 0)]
|
|
45
|
-
(when-not (
|
|
46
|
-
(throw (ex-info (str "Unknown harness in worker spec: " s ".
|
|
49
|
+
(when-not (harnesses h)
|
|
50
|
+
(throw (ex-info (str "Unknown harness in worker spec: " s ". Known: " (str/join ", " (map name (sort harnesses)))) {})))
|
|
47
51
|
(when (zero? cnt)
|
|
48
52
|
(throw (ex-info (str "Invalid count in worker spec: " s ". Use format 'harness:count'") {})))
|
|
49
53
|
{:harness h :count cnt}))
|
|
@@ -80,7 +84,7 @@
|
|
|
80
84
|
(= arg "--workers")
|
|
81
85
|
(let [next-arg (second remaining)]
|
|
82
86
|
(if (worker-spec? next-arg)
|
|
83
|
-
;; Collect all worker specs: --workers claude:5
|
|
87
|
+
;; Collect all worker specs: --workers claude:5 opencode:2
|
|
84
88
|
(let [[specs rest] (collect-worker-specs (next remaining))]
|
|
85
89
|
(recur (assoc opts :worker-specs specs) rest))
|
|
86
90
|
;; Simple count: --workers 4
|
|
@@ -93,8 +97,8 @@
|
|
|
93
97
|
|
|
94
98
|
(= arg "--harness")
|
|
95
99
|
(let [h (keyword (second remaining))]
|
|
96
|
-
(when-not (
|
|
97
|
-
(throw (ex-info (str "Unknown harness: " (second remaining) ".
|
|
100
|
+
(when-not (harnesses h)
|
|
101
|
+
(throw (ex-info (str "Unknown harness: " (second remaining) ". Known: " (str/join ", " (map name (sort harnesses)))) {})))
|
|
98
102
|
(recur (assoc opts :harness h)
|
|
99
103
|
(nnext remaining)))
|
|
100
104
|
|
|
@@ -135,14 +139,67 @@
|
|
|
135
139
|
|
|
136
140
|
(declare cmd-swarm parse-model-string)
|
|
137
141
|
|
|
142
|
+
(defn- check-git-clean!
|
|
143
|
+
"Abort if git working tree is dirty. Dirty index causes merge conflicts
|
|
144
|
+
and wasted worker iterations."
|
|
145
|
+
[]
|
|
146
|
+
(let [result (process/sh ["git" "status" "--porcelain"]
|
|
147
|
+
{:out :string :err :string})
|
|
148
|
+
output (str/trim (:out result))]
|
|
149
|
+
(when (and (zero? (:exit result)) (not (str/blank? output)))
|
|
150
|
+
(println "ERROR: Git working tree is dirty. Resolve before running swarm.")
|
|
151
|
+
(println)
|
|
152
|
+
(println output)
|
|
153
|
+
(println)
|
|
154
|
+
(println "Run 'git stash' or 'git commit' first.")
|
|
155
|
+
(System/exit 1))))
|
|
156
|
+
|
|
157
|
+
(defn- check-stale-worktrees!
|
|
158
|
+
"Abort if stale oompa worktrees or branches exist from a prior run.
|
|
159
|
+
Corrupted .git/worktrees/ entries poison git worktree add for ALL workers,
|
|
160
|
+
not just the worker whose entry is stale. (See swarm af32b180 — kimi-k2.5
|
|
161
|
+
w9 went 20/20 doing nothing because w10's corrupt commondir blocked it.)"
|
|
162
|
+
[]
|
|
163
|
+
;; Prune orphaned metadata first — cleans entries whose directories are gone
|
|
164
|
+
(let [prune-result (process/sh ["git" "worktree" "prune"] {:out :string :err :string})]
|
|
165
|
+
(when-not (zero? (:exit prune-result))
|
|
166
|
+
(println "WARNING: git worktree prune failed:")
|
|
167
|
+
(println (:err prune-result))))
|
|
168
|
+
(let [;; Find .ww* directories (oompa per-iteration worktree naming convention)
|
|
169
|
+
ls-result (process/sh ["find" "." "-maxdepth" "1" "-type" "d" "-name" ".ww*"]
|
|
170
|
+
{:out :string})
|
|
171
|
+
stale-dirs (when (zero? (:exit ls-result))
|
|
172
|
+
(->> (str/split-lines (:out ls-result))
|
|
173
|
+
(remove str/blank?)))
|
|
174
|
+
;; Find oompa/* branches
|
|
175
|
+
br-result (process/sh ["git" "branch" "--list" "oompa/*"]
|
|
176
|
+
{:out :string})
|
|
177
|
+
stale-branches (when (zero? (:exit br-result))
|
|
178
|
+
(->> (str/split-lines (:out br-result))
|
|
179
|
+
(map str/trim)
|
|
180
|
+
(remove str/blank?)))]
|
|
181
|
+
(when (or (seq stale-dirs) (seq stale-branches))
|
|
182
|
+
(println "ERROR: Stale oompa worktrees detected from a prior run.")
|
|
183
|
+
(println " Corrupt worktree metadata will cause worker failures.")
|
|
184
|
+
(println)
|
|
185
|
+
(when (seq stale-dirs)
|
|
186
|
+
(println (format " Stale directories (%d):" (count stale-dirs)))
|
|
187
|
+
(doseq [d stale-dirs] (println (str " " d))))
|
|
188
|
+
(when (seq stale-branches)
|
|
189
|
+
(println (format " Stale branches (%d):" (count stale-branches)))
|
|
190
|
+
(doseq [b stale-branches] (println (str " " b))))
|
|
191
|
+
(println)
|
|
192
|
+
(println "Clean up with:")
|
|
193
|
+
(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*")
|
|
194
|
+
(println)
|
|
195
|
+
(System/exit 1))))
|
|
196
|
+
|
|
138
197
|
(defn- probe-model
|
|
139
198
|
"Send 'say ok' to a model via its harness CLI. Returns true if model responds.
|
|
140
|
-
|
|
141
|
-
[harness model]
|
|
199
|
+
Uses harness/build-probe-cmd for the command, /dev/null stdin to prevent hang."
|
|
200
|
+
[harness-kw model]
|
|
142
201
|
(try
|
|
143
|
-
(let [cmd (
|
|
144
|
-
:claude ["claude" "--model" model "-p" "[oompa:probe] say ok" "--max-turns" "1"]
|
|
145
|
-
:codex ["codex" "exec" "--dangerously-bypass-approvals-and-sandbox" "--skip-git-repo-check" "--model" model "--" "[oompa:probe] say ok"])
|
|
202
|
+
(let [cmd (harness/build-probe-cmd harness-kw model)
|
|
146
203
|
null-in (io/input-stream (io/file "/dev/null"))
|
|
147
204
|
proc (process/process cmd {:out :string :err :string :in null-in})
|
|
148
205
|
result (deref proc 30000 :timeout)]
|
|
@@ -187,7 +244,7 @@
|
|
|
187
244
|
(cmd-swarm opts (or (seq args) ["oompa.json"]))
|
|
188
245
|
(let [swarm-id (make-swarm-id)]
|
|
189
246
|
(if-let [specs (:worker-specs opts)]
|
|
190
|
-
;; Mixed worker specs: --workers claude:5
|
|
247
|
+
;; Mixed worker specs: --workers claude:5 opencode:2
|
|
191
248
|
(let [workers (mapcat
|
|
192
249
|
(fn [spec]
|
|
193
250
|
(let [{:keys [harness count]} spec]
|
|
@@ -206,10 +263,10 @@
|
|
|
206
263
|
(println (format " %dx %s" (:count spec) (name (:harness spec)))))
|
|
207
264
|
(println)
|
|
208
265
|
(worker/run-workers! workers))
|
|
209
|
-
;; Simple mode
|
|
266
|
+
;; Simple mode retired — use oompa.json or --workers harness:count
|
|
210
267
|
(do
|
|
211
|
-
(println
|
|
212
|
-
(
|
|
268
|
+
(println "Simple mode is no longer supported. Use oompa.json or --workers harness:count.")
|
|
269
|
+
(System/exit 1))))))
|
|
213
270
|
|
|
214
271
|
(defn cmd-loop
|
|
215
272
|
"Run orchestrator N times"
|
|
@@ -219,7 +276,7 @@
|
|
|
219
276
|
(:iterations opts)
|
|
220
277
|
20)]
|
|
221
278
|
(if-let [specs (:worker-specs opts)]
|
|
222
|
-
;; Mixed worker specs: --workers claude:5
|
|
279
|
+
;; Mixed worker specs: --workers claude:5 opencode:2
|
|
223
280
|
(let [workers (mapcat
|
|
224
281
|
(fn [spec]
|
|
225
282
|
(let [{:keys [harness count]} spec]
|
|
@@ -238,14 +295,10 @@
|
|
|
238
295
|
(println (format " %dx %s" (:count spec) (name (:harness spec)))))
|
|
239
296
|
(println)
|
|
240
297
|
(worker/run-workers! workers))
|
|
241
|
-
;; Simple mode
|
|
242
|
-
(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
(println (format "Starting %d iterations with %s harness%s..."
|
|
246
|
-
iterations (name (:harness opts)) model-str))
|
|
247
|
-
(println (format "Swarm ID: %s" swarm-id))
|
|
248
|
-
(orchestrator/run-loop! iterations (assoc opts :swarm-id swarm-id))))))
|
|
298
|
+
;; Simple mode retired — use oompa.json or --workers harness:count
|
|
299
|
+
(do
|
|
300
|
+
(println "Simple mode is no longer supported. Use oompa.json or --workers harness:count.")
|
|
301
|
+
(System/exit 1)))))
|
|
249
302
|
|
|
250
303
|
(defn cmd-prompt
|
|
251
304
|
"Run ad-hoc prompt as single task"
|
|
@@ -265,25 +318,64 @@
|
|
|
265
318
|
(orchestrator/run-once! opts))))
|
|
266
319
|
|
|
267
320
|
(defn cmd-status
|
|
268
|
-
"Show status of last run"
|
|
321
|
+
"Show status of last run — reads event-sourced runs/{swarm-id}/ data."
|
|
269
322
|
[opts args]
|
|
270
|
-
(let [
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
(println (format "
|
|
323
|
+
(let [run-ids (runs/list-runs)]
|
|
324
|
+
(if (seq run-ids)
|
|
325
|
+
(let [swarm-id (or (first args) (first run-ids))
|
|
326
|
+
started (runs/read-started swarm-id)
|
|
327
|
+
stopped (runs/read-stopped swarm-id)
|
|
328
|
+
cycles (runs/list-cycles swarm-id)
|
|
329
|
+
reviews (runs/list-reviews swarm-id)]
|
|
330
|
+
(println (format "Swarm: %s" swarm-id))
|
|
331
|
+
(when started
|
|
332
|
+
(println (format " Started: %s" (:started-at started)))
|
|
333
|
+
(println (format " PID: %s" (or (:pid started) "N/A")))
|
|
334
|
+
(println (format " Config: %s" (or (:config-file started) "N/A")))
|
|
335
|
+
(println (format " Workers: %d" (count (:workers started)))))
|
|
278
336
|
(println)
|
|
279
|
-
(
|
|
280
|
-
(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
337
|
+
(if stopped
|
|
338
|
+
(println (format "Stopped: %s (reason: %s%s)"
|
|
339
|
+
(:stopped-at stopped)
|
|
340
|
+
(:reason stopped)
|
|
341
|
+
(if (:error stopped)
|
|
342
|
+
(str ", error: " (:error stopped))
|
|
343
|
+
"")))
|
|
344
|
+
(println " (still running — no stopped event yet)"))
|
|
345
|
+
(when (seq cycles)
|
|
346
|
+
(println)
|
|
347
|
+
(println (format "Cycles: %d total" (count cycles)))
|
|
348
|
+
(doseq [c cycles]
|
|
349
|
+
(println (format " %s-c%d: %s (%dms, claimed: %s)"
|
|
350
|
+
(:worker-id c) (:cycle c)
|
|
351
|
+
(:outcome c)
|
|
352
|
+
(or (:duration-ms c) 0)
|
|
353
|
+
(str/join ", " (or (:claimed-task-ids c) []))))))
|
|
354
|
+
(when (seq reviews)
|
|
355
|
+
(println)
|
|
356
|
+
(println (format "Reviews: %d total" (count reviews)))
|
|
357
|
+
(doseq [r reviews]
|
|
358
|
+
(println (format " %s-c%d-r%d: %s"
|
|
359
|
+
(:worker-id r) (:cycle r) (:round r)
|
|
360
|
+
(:verdict r))))))
|
|
361
|
+
;; Fall back to legacy JSONL format
|
|
362
|
+
(let [runs-dir (io/file "runs")
|
|
363
|
+
files (when (.exists runs-dir)
|
|
364
|
+
(->> (.listFiles runs-dir)
|
|
365
|
+
(filter #(.isFile %))
|
|
366
|
+
(sort-by #(.lastModified %) >)))]
|
|
367
|
+
(if-let [latest (first files)]
|
|
368
|
+
(do
|
|
369
|
+
(println (format "Latest run (legacy): %s" (.getName latest)))
|
|
284
370
|
(println)
|
|
285
|
-
(
|
|
286
|
-
|
|
371
|
+
(with-open [r (io/reader latest)]
|
|
372
|
+
(let [entries (mapv #(json/parse-string % true) (line-seq r))
|
|
373
|
+
by-status (group-by :status entries)]
|
|
374
|
+
(doseq [[status tasks] (sort-by first by-status)]
|
|
375
|
+
(println (format "%s: %d" (name status) (count tasks))))
|
|
376
|
+
(println)
|
|
377
|
+
(println (format "Total: %d tasks" (count entries))))))
|
|
378
|
+
(println "No runs found."))))))
|
|
287
379
|
|
|
288
380
|
(defn cmd-worktrees
|
|
289
381
|
"List worktree status"
|
|
@@ -323,21 +415,43 @@
|
|
|
323
415
|
"Check if agent backends are available"
|
|
324
416
|
[opts args]
|
|
325
417
|
(println "Checking agent backends...")
|
|
326
|
-
(doseq [
|
|
327
|
-
(let [available? (
|
|
418
|
+
(doseq [harness-kw (sort (harness/known-harnesses))]
|
|
419
|
+
(let [available? (harness/check-available harness-kw)]
|
|
328
420
|
(println (format " %s: %s"
|
|
329
|
-
(name
|
|
421
|
+
(name harness-kw)
|
|
330
422
|
(if available? "✓ available" "✗ not found"))))))
|
|
331
423
|
|
|
424
|
+
(def ^:private reasoning-variants
|
|
425
|
+
#{"minimal" "low" "medium" "high" "max" "xhigh"})
|
|
426
|
+
|
|
332
427
|
(defn- parse-model-string
|
|
333
428
|
"Parse model string into {:harness :model :reasoning}.
|
|
334
|
-
|
|
429
|
+
|
|
430
|
+
Supported formats:
|
|
431
|
+
- harness:model
|
|
432
|
+
- harness:model:reasoning (codex only)
|
|
433
|
+
- model (defaults harness to :codex)
|
|
434
|
+
|
|
435
|
+
Note: non-codex model identifiers may contain ':' (for example
|
|
436
|
+
openrouter/...:free). Those suffixes are preserved in :model."
|
|
335
437
|
[s]
|
|
336
438
|
(if (and s (str/includes? s ":"))
|
|
337
|
-
(let [
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
439
|
+
(let [[harness-str rest*] (str/split s #":" 2)
|
|
440
|
+
harness (keyword harness-str)]
|
|
441
|
+
(if (contains? harnesses harness)
|
|
442
|
+
(if (= harness :codex)
|
|
443
|
+
;; Codex may include a reasoning suffix at the end. Only treat the
|
|
444
|
+
;; last segment as reasoning if it matches a known variant.
|
|
445
|
+
(if-let [idx (str/last-index-of rest* ":")]
|
|
446
|
+
(let [model* (subs rest* 0 idx)
|
|
447
|
+
reasoning* (subs rest* (inc idx))]
|
|
448
|
+
(if (contains? reasoning-variants reasoning*)
|
|
449
|
+
{:harness harness :model model* :reasoning reasoning*}
|
|
450
|
+
{:harness harness :model rest*}))
|
|
451
|
+
{:harness harness :model rest*})
|
|
452
|
+
;; Non-codex: preserve full model string (including any ':suffix').
|
|
453
|
+
{:harness harness :model rest*})
|
|
454
|
+
;; Not a known harness prefix, treat as raw model on default harness.
|
|
341
455
|
{:harness :codex :model s}))
|
|
342
456
|
{:harness :codex :model s}))
|
|
343
457
|
|
|
@@ -354,14 +468,50 @@
|
|
|
354
468
|
(println "{")
|
|
355
469
|
(println " \"workers\": [")
|
|
356
470
|
(println " {\"model\": \"codex:gpt-5.3-codex:medium\", \"prompt\": \"prompts/executor.md\", \"iterations\": 10, \"count\": 3, \"can_plan\": false},")
|
|
357
|
-
(println " {\"model\": \"claude:opus\", \"prompt\": [\"prompts/base.md\", \"prompts/planner.md\"], \"count\": 1}")
|
|
471
|
+
(println " {\"model\": \"claude:opus\", \"prompt\": [\"prompts/base.md\", \"prompts/planner.md\"], \"count\": 1},")
|
|
472
|
+
(println " {\"model\": \"gemini:gemini-3-pro-preview\", \"prompt\": [\"prompts/executor.md\"], \"count\": 1}")
|
|
358
473
|
(println " ]")
|
|
359
474
|
(println "}")
|
|
360
475
|
(println)
|
|
361
476
|
(println "prompt: string or array of paths — concatenated into one prompt.")
|
|
362
477
|
(System/exit 1))
|
|
478
|
+
;; Preflight: abort if git is dirty to prevent merge conflicts
|
|
479
|
+
(check-git-clean!)
|
|
480
|
+
;; Preflight: abort if stale worktrees from prior runs would poison git
|
|
481
|
+
(check-stale-worktrees!)
|
|
482
|
+
|
|
363
483
|
(let [config (json/parse-string (slurp f) true)
|
|
364
|
-
|
|
484
|
+
;; Parse reviewer config — supports both formats:
|
|
485
|
+
;; Legacy: {"review_model": "harness:model:reasoning"}
|
|
486
|
+
;; New: {"reviewer": {"model": "harness:model:reasoning", "prompt": ["path.md"]}}
|
|
487
|
+
reviewer-config (:reviewer config)
|
|
488
|
+
review-parsed (cond
|
|
489
|
+
reviewer-config
|
|
490
|
+
(let [parsed (parse-model-string (:model reviewer-config))
|
|
491
|
+
prompts (let [p (:prompt reviewer-config)]
|
|
492
|
+
(cond (vector? p) p
|
|
493
|
+
(string? p) [p]
|
|
494
|
+
:else []))]
|
|
495
|
+
(assoc parsed :prompts prompts))
|
|
496
|
+
|
|
497
|
+
(:review_model config)
|
|
498
|
+
(parse-model-string (:review_model config))
|
|
499
|
+
|
|
500
|
+
:else nil)
|
|
501
|
+
|
|
502
|
+
;; Parse planner config — optional dedicated planner
|
|
503
|
+
;; Runs in project root, no worktree/review/merge, respects max_pending backpressure
|
|
504
|
+
planner-config (:planner config)
|
|
505
|
+
planner-parsed (when planner-config
|
|
506
|
+
(let [parsed (parse-model-string (:model planner-config))
|
|
507
|
+
prompts (let [p (:prompt planner-config)]
|
|
508
|
+
(cond (vector? p) p
|
|
509
|
+
(string? p) [p]
|
|
510
|
+
:else []))]
|
|
511
|
+
(assoc parsed
|
|
512
|
+
:prompts prompts
|
|
513
|
+
:max-pending (or (:max_pending planner-config) 10))))
|
|
514
|
+
|
|
365
515
|
worker-configs (:workers config)
|
|
366
516
|
|
|
367
517
|
;; Expand worker configs by count
|
|
@@ -383,14 +533,30 @@
|
|
|
383
533
|
:iterations (or (:iterations wc) 10)
|
|
384
534
|
:prompts (:prompt wc)
|
|
385
535
|
:can-plan (:can_plan wc)
|
|
386
|
-
:
|
|
387
|
-
:
|
|
536
|
+
:wait-between (:wait_between wc)
|
|
537
|
+
:max-working-resumes (:max_working_resumes wc)
|
|
538
|
+
:review-harness (:harness review-parsed)
|
|
539
|
+
:review-model (:model review-parsed)
|
|
540
|
+
:review-prompts (:prompts review-parsed)})))
|
|
388
541
|
expanded-workers)]
|
|
389
542
|
|
|
390
543
|
(println (format "Swarm config from %s:" config-file))
|
|
391
544
|
(println (format " Swarm ID: %s" swarm-id))
|
|
392
|
-
(when
|
|
393
|
-
(println (format "
|
|
545
|
+
(when planner-parsed
|
|
546
|
+
(println (format " Planner: %s:%s (max_pending: %d%s)"
|
|
547
|
+
(name (:harness planner-parsed))
|
|
548
|
+
(:model planner-parsed)
|
|
549
|
+
(:max-pending planner-parsed)
|
|
550
|
+
(if (seq (:prompts planner-parsed))
|
|
551
|
+
(str ", prompts: " (str/join ", " (:prompts planner-parsed)))
|
|
552
|
+
""))))
|
|
553
|
+
(when review-parsed
|
|
554
|
+
(println (format " Reviewer: %s:%s%s"
|
|
555
|
+
(name (:harness review-parsed))
|
|
556
|
+
(:model review-parsed)
|
|
557
|
+
(if (seq (:prompts review-parsed))
|
|
558
|
+
(str " (prompts: " (str/join ", " (:prompts review-parsed)) ")")
|
|
559
|
+
""))))
|
|
394
560
|
(println (format " Workers: %d total" (count workers)))
|
|
395
561
|
(doseq [[idx wc] (map-indexed vector worker-configs)]
|
|
396
562
|
(let [{:keys [harness model reasoning]} (parse-model-string (:model wc))]
|
|
@@ -404,7 +570,27 @@
|
|
|
404
570
|
(println)
|
|
405
571
|
|
|
406
572
|
;; Preflight: probe each unique model before launching workers
|
|
407
|
-
|
|
573
|
+
;; Include planner model in validation if configured
|
|
574
|
+
(validate-models! (cond-> worker-configs
|
|
575
|
+
planner-config (conj planner-config))
|
|
576
|
+
review-parsed)
|
|
577
|
+
|
|
578
|
+
;; Write started event to runs/{swarm-id}/started.json
|
|
579
|
+
(runs/write-started! swarm-id
|
|
580
|
+
{:workers workers
|
|
581
|
+
:planner-config planner-parsed
|
|
582
|
+
:reviewer-config review-parsed
|
|
583
|
+
:config-file config-file})
|
|
584
|
+
(println (format "\nStarted event written to runs/%s/started.json" swarm-id))
|
|
585
|
+
|
|
586
|
+
;; Run planner if configured — synchronously before workers
|
|
587
|
+
(when planner-parsed
|
|
588
|
+
(println)
|
|
589
|
+
(println (format " Planner: %s:%s (max_pending: %d)"
|
|
590
|
+
(name (:harness planner-parsed))
|
|
591
|
+
(:model planner-parsed)
|
|
592
|
+
(:max-pending planner-parsed)))
|
|
593
|
+
(worker/run-planner! (assoc planner-parsed :swarm-id swarm-id)))
|
|
408
594
|
|
|
409
595
|
;; Run workers using new worker module
|
|
410
596
|
(worker/run-workers! workers))))
|
|
@@ -428,6 +614,69 @@
|
|
|
428
614
|
(doseq [t (tasks/list-current)]
|
|
429
615
|
(println (format " - %s: %s" (:id t) (:summary t)))))))
|
|
430
616
|
|
|
617
|
+
(defn- find-latest-swarm-id
|
|
618
|
+
"Find the most recent swarm ID from runs/ directory."
|
|
619
|
+
[]
|
|
620
|
+
(first (runs/list-runs)))
|
|
621
|
+
|
|
622
|
+
(defn- read-swarm-pid
|
|
623
|
+
"Read PID from started.json for a swarm. Returns nil if not found."
|
|
624
|
+
[swarm-id]
|
|
625
|
+
(when-let [started (runs/read-started swarm-id)]
|
|
626
|
+
(:pid started)))
|
|
627
|
+
|
|
628
|
+
(defn- pid-alive?
|
|
629
|
+
"Check if a process is alive via kill -0."
|
|
630
|
+
[pid]
|
|
631
|
+
(try
|
|
632
|
+
(zero? (:exit (process/sh ["kill" "-0" (str pid)]
|
|
633
|
+
{:out :string :err :string})))
|
|
634
|
+
(catch Exception _ false)))
|
|
635
|
+
|
|
636
|
+
(defn cmd-stop
|
|
637
|
+
"Send SIGTERM to running swarm — workers finish current cycle then exit"
|
|
638
|
+
[opts args]
|
|
639
|
+
(let [swarm-id (or (first args) (find-latest-swarm-id))]
|
|
640
|
+
(if-not swarm-id
|
|
641
|
+
(println "No swarm runs found.")
|
|
642
|
+
(let [stopped (runs/read-stopped swarm-id)]
|
|
643
|
+
(if stopped
|
|
644
|
+
(println (format "Swarm %s already stopped (reason: %s)" swarm-id (:reason stopped)))
|
|
645
|
+
(let [pid (read-swarm-pid swarm-id)]
|
|
646
|
+
(if-not pid
|
|
647
|
+
(println (format "No PID found for swarm %s" swarm-id))
|
|
648
|
+
(if-not (pid-alive? pid)
|
|
649
|
+
(do
|
|
650
|
+
(println (format "Swarm %s PID %s is not running (stale). Writing stopped event." swarm-id pid))
|
|
651
|
+
(runs/write-stopped! swarm-id :interrupted))
|
|
652
|
+
(do
|
|
653
|
+
(println (format "Sending SIGTERM to swarm %s (PID %s)..." swarm-id pid))
|
|
654
|
+
(println "Workers will finish their current cycle and exit.")
|
|
655
|
+
(process/sh ["kill" (str pid)]))))))))))
|
|
656
|
+
|
|
657
|
+
(defn cmd-kill
|
|
658
|
+
"Send SIGKILL to running swarm — immediate termination"
|
|
659
|
+
[opts args]
|
|
660
|
+
(let [swarm-id (or (first args) (find-latest-swarm-id))]
|
|
661
|
+
(if-not swarm-id
|
|
662
|
+
(println "No swarm runs found.")
|
|
663
|
+
(let [stopped (runs/read-stopped swarm-id)]
|
|
664
|
+
(if stopped
|
|
665
|
+
(println (format "Swarm %s already stopped (reason: %s)" swarm-id (:reason stopped)))
|
|
666
|
+
(let [pid (read-swarm-pid swarm-id)]
|
|
667
|
+
(if-not pid
|
|
668
|
+
(println (format "No PID found for swarm %s" swarm-id))
|
|
669
|
+
(if-not (pid-alive? pid)
|
|
670
|
+
(do
|
|
671
|
+
(println (format "Swarm %s PID %s is not running (stale). Writing stopped event." swarm-id pid))
|
|
672
|
+
(runs/write-stopped! swarm-id :interrupted))
|
|
673
|
+
(do
|
|
674
|
+
(println (format "Sending SIGKILL to swarm %s (PID %s)..." swarm-id pid))
|
|
675
|
+
;; SIGKILL bypasses JVM shutdown hooks, so write stopped.json here
|
|
676
|
+
(process/sh ["kill" "-9" (str pid)])
|
|
677
|
+
(runs/write-stopped! swarm-id :interrupted)
|
|
678
|
+
(println "Swarm killed."))))))))))
|
|
679
|
+
|
|
431
680
|
(defn cmd-help
|
|
432
681
|
"Print usage information"
|
|
433
682
|
[opts args]
|
|
@@ -443,6 +692,8 @@
|
|
|
443
692
|
(println " prompt \"...\" Run ad-hoc prompt")
|
|
444
693
|
(println " status Show last run summary")
|
|
445
694
|
(println " worktrees List worktree status")
|
|
695
|
+
(println " stop [swarm-id] Stop swarm gracefully (finish current cycle)")
|
|
696
|
+
(println " kill [swarm-id] Kill swarm immediately (SIGKILL)")
|
|
446
697
|
(println " cleanup Remove all worktrees")
|
|
447
698
|
(println " context Print context block")
|
|
448
699
|
(println " check Check agent backends")
|
|
@@ -450,16 +701,16 @@
|
|
|
450
701
|
(println)
|
|
451
702
|
(println "Options:")
|
|
452
703
|
(println " --workers N Number of parallel workers (default: 2)")
|
|
453
|
-
(println " --workers H:N [H:N ...] Mixed workers by harness (e.g., claude:5
|
|
704
|
+
(println " --workers H:N [H:N ...] Mixed workers by harness (e.g., claude:5 opencode:2)")
|
|
454
705
|
(println " --iterations N Number of iterations per worker (default: 1)")
|
|
455
|
-
(println " --harness {
|
|
456
|
-
(println " --model MODEL Model to use (e.g., codex-5.
|
|
706
|
+
(println (str " --harness {" (str/join "," (map name (sort harnesses))) "} Agent harness to use (default: codex)"))
|
|
707
|
+
(println " --model MODEL Model to use (e.g., codex:gpt-5.3-codex:medium, claude:opus, gemini:gemini-3-pro-preview)")
|
|
457
708
|
(println " --dry-run Skip actual merges")
|
|
458
709
|
(println " --keep-worktrees Don't cleanup worktrees after run")
|
|
459
710
|
(println)
|
|
460
711
|
(println "Examples:")
|
|
461
712
|
(println " ./swarm.bb loop 10 --harness codex --model gpt-5.3-codex --workers 3")
|
|
462
|
-
(println " ./swarm.bb loop --workers claude:5
|
|
713
|
+
(println " ./swarm.bb loop --workers claude:5 opencode:2 --iterations 20")
|
|
463
714
|
(println " ./swarm.bb swarm oompa.json # Run multi-model config"))
|
|
464
715
|
|
|
465
716
|
;; =============================================================================
|
|
@@ -473,6 +724,8 @@
|
|
|
473
724
|
"tasks" cmd-tasks
|
|
474
725
|
"prompt" cmd-prompt
|
|
475
726
|
"status" cmd-status
|
|
727
|
+
"stop" cmd-stop
|
|
728
|
+
"kill" cmd-kill
|
|
476
729
|
"worktrees" cmd-worktrees
|
|
477
730
|
"cleanup" cmd-cleanup
|
|
478
731
|
"context" cmd-context
|