@nbardy/oompa 0.7.2 → 0.7.3
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 +6 -4
- package/agentnet/src/agentnet/agent.clj +9 -8
- package/agentnet/src/agentnet/cli.clj +268 -77
- package/agentnet/src/agentnet/cli.clj.bak +1384 -0
- package/agentnet/src/agentnet/core.clj +17 -2
- package/agentnet/src/agentnet/harness.clj +69 -36
- package/agentnet/src/agentnet/runs.clj +5 -3
- package/agentnet/src/agentnet/tasks.clj +6 -0
- package/agentnet/src/agentnet/worker.clj +676 -226
- package/bin/oompa.js +5 -1
- package/config/prompts/_task_header.md +2 -2
- package/oompa.example.json +4 -4
- package/package.json +5 -3
- package/scripts/README.md +6 -0
- package/scripts/__pycache__/stream_bridge.cpython-314.pyc +0 -0
- package/scripts/copy-repo-code.sh +110 -0
- package/scripts/install-babashka.js +97 -0
- package/scripts/test-harness-resume.sh +229 -0
package/README.md
CHANGED
|
@@ -83,9 +83,9 @@ This repo has a fleshed out version of the idea. The oompa loompas are organized
|
|
|
83
83
|
```json
|
|
84
84
|
{
|
|
85
85
|
"workers": [
|
|
86
|
-
{"model": "claude:opus", "prompt": ["config/prompts/planner.md"], "
|
|
87
|
-
{"model": "codex:gpt-5.3-codex:medium", "prompt": ["config/prompts/executor.md"], "
|
|
88
|
-
{"model": "opencode:opencode/kimi-k2.5-free", "prompt": ["config/prompts/executor.md"], "
|
|
86
|
+
{"model": "claude:opus", "prompt": ["config/prompts/planner.md"], "max_cycle": 5, "count": 1},
|
|
87
|
+
{"model": "codex:gpt-5.3-codex:medium", "prompt": ["config/prompts/executor.md"], "max_cycle": 10, "count": 2, "can_plan": false},
|
|
88
|
+
{"model": "opencode:opencode/kimi-k2.5-free", "prompt": ["config/prompts/executor.md"], "max_cycle": 10, "count": 1, "can_plan": false}
|
|
89
89
|
]
|
|
90
90
|
}
|
|
91
91
|
```
|
|
@@ -101,11 +101,13 @@ This spawns:
|
|
|
101
101
|
|-------|----------|-------------|
|
|
102
102
|
| `model` | yes | `harness:model` or `harness:model:reasoning` (e.g. `codex:gpt-5.3-codex:medium`, `claude:opus`, `opencode:opencode/kimi-k2.5-free`) |
|
|
103
103
|
| `prompt` | no | String or array of paths — concatenated into one prompt |
|
|
104
|
-
| `
|
|
104
|
+
| `max_cycle` | no | Max worker cycles for JSON config runs (default: 10) |
|
|
105
105
|
| `count` | no | Number of workers with this config (default: 1) |
|
|
106
106
|
| `can_plan` | no | If `false`, worker waits for tasks before starting (default: `true`) |
|
|
107
107
|
| `max_wait_for_tasks` | no | Max seconds a `can_plan: false` worker waits for queue work (default: `600`) |
|
|
108
108
|
|
|
109
|
+
`oompa loop` still uses the CLI flag `--iterations`. JSON worker configs use `max_cycle`.
|
|
110
|
+
|
|
109
111
|
#### Composable prompts
|
|
110
112
|
|
|
111
113
|
`prompt` accepts a string or an array. Arrays get concatenated, so you can reuse a shared base across workers:
|
|
@@ -176,16 +176,12 @@
|
|
|
176
176
|
#"(?m)^\s*#oompa_directive:include_file\s+\"([^\"]+)\"\s*$")
|
|
177
177
|
|
|
178
178
|
(defn- read-file-cached
|
|
179
|
-
"
|
|
179
|
+
"Reads prompts fresh on each run so roles can be edited live. It is a fast op."
|
|
180
180
|
[path]
|
|
181
181
|
(when path
|
|
182
|
-
(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
(when (.exists f)
|
|
186
|
-
(let [content (slurp f)]
|
|
187
|
-
(swap! prompt-file-cache assoc path content)
|
|
188
|
-
content))))))
|
|
182
|
+
(let [f (io/file path)]
|
|
183
|
+
(when (.exists f)
|
|
184
|
+
(slurp f)))))
|
|
189
185
|
|
|
190
186
|
(defn- resolve-include-path
|
|
191
187
|
"Resolve an include path relative to the file that declares it."
|
|
@@ -276,6 +272,11 @@
|
|
|
276
272
|
[output]
|
|
277
273
|
(boolean (re-find #"COMPLETE_AND_READY_FOR_MERGE" (or output ""))))
|
|
278
274
|
|
|
275
|
+
(defn needs-followup-signal?
|
|
276
|
+
"Check if output contains NEEDS_FOLLOWUP signal"
|
|
277
|
+
[output]
|
|
278
|
+
(boolean (re-find #"NEEDS_FOLLOWUP" (or output ""))))
|
|
279
|
+
|
|
279
280
|
(defn parse-claim-signal
|
|
280
281
|
"Extract task IDs from CLAIM(...) signal in output.
|
|
281
282
|
Returns vector of task ID strings, or nil if no CLAIM signal found.
|
|
@@ -175,45 +175,145 @@
|
|
|
175
175
|
(println "WARNING: Git working tree is dirty. You may experience merge conflicts.")
|
|
176
176
|
(println output))))
|
|
177
177
|
|
|
178
|
-
(defn-
|
|
179
|
-
"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
178
|
+
(defn- dirty-worktree?
|
|
179
|
+
"Returns true if the git worktree at path has uncommitted changes."
|
|
180
|
+
[path]
|
|
181
|
+
(let [{:keys [exit out]} (process/sh ["git" "-C" path "status" "--porcelain"]
|
|
182
|
+
{:out :string :err :string})]
|
|
183
|
+
(and (zero? exit) (not (str/blank? out)))))
|
|
184
|
+
|
|
185
|
+
(defn- worktree-branch-name
|
|
186
|
+
"Returns the current branch name for the worktree at path, or nil on failure."
|
|
187
|
+
[path]
|
|
188
|
+
(let [{:keys [exit out]} (process/sh ["git" "-C" path "rev-parse" "--abbrev-ref" "HEAD"]
|
|
189
|
+
{:out :string :err :string})]
|
|
190
|
+
(when (zero? exit) (str/trim out))))
|
|
191
|
+
|
|
192
|
+
(defn- remove-stale-worktree!
|
|
193
|
+
"Remove a stale worktree directory and delete its branch."
|
|
194
|
+
[path branch]
|
|
195
|
+
(process/sh ["git" "worktree" "remove" "--force" path] {:out :string :err :string})
|
|
196
|
+
(when (and branch (not (str/blank? branch)) (not= branch "HEAD"))
|
|
197
|
+
(process/sh ["git" "branch" "-D" branch] {:out :string :err :string})))
|
|
198
|
+
|
|
199
|
+
(defn- run-stale-review!
|
|
200
|
+
"Invoke the reviewer model on partial worktree changes.
|
|
201
|
+
Tries each reviewer in the fallback chain until one returns a verdict.
|
|
202
|
+
Returns :merge to merge the branch into main, :discard to throw it away."
|
|
203
|
+
[reviewer-configs worktree-path branch]
|
|
204
|
+
(let [diff-out (:out (process/sh ["git" "-C" worktree-path "diff" "HEAD"]
|
|
205
|
+
{:out :string :err :string}))
|
|
206
|
+
diff (if (> (count diff-out) 8000)
|
|
207
|
+
(str (subs diff-out 0 8000) "\n...[diff truncated at 8000 chars]")
|
|
208
|
+
diff-out)
|
|
209
|
+
status-out (:out (process/sh ["git" "-C" worktree-path "status" "--short"]
|
|
210
|
+
{:out :string}))
|
|
211
|
+
prompt (str "You are reviewing partial/incomplete changes from an interrupted swarm run.\n\n"
|
|
212
|
+
"Branch: " branch "\n"
|
|
213
|
+
"Status:\n" status-out "\n\n"
|
|
214
|
+
"Diff:\n```\n" diff "\n```\n\n"
|
|
215
|
+
"Should these changes be merged into main or discarded?\n"
|
|
216
|
+
"MERGE if: changes are correct, complete, or valuable enough to keep.\n"
|
|
217
|
+
"DISCARD if: changes are broken, trivial, or not worth merging.\n\n"
|
|
218
|
+
"Your verdict MUST appear on its own line, exactly one of:\n"
|
|
219
|
+
"VERDICT: MERGE\n"
|
|
220
|
+
"VERDICT: DISCARD\n\n"
|
|
221
|
+
"Then briefly explain why.\n")
|
|
222
|
+
result (reduce (fn [_ {:keys [harness model]}]
|
|
223
|
+
(try
|
|
224
|
+
(let [cmd (harness/build-cmd harness {:model model :prompt prompt})
|
|
225
|
+
res (process/sh cmd
|
|
226
|
+
{:in (harness/process-stdin harness prompt)
|
|
227
|
+
:out :string :err :string})
|
|
228
|
+
output (:out res)
|
|
229
|
+
has-verdict? (or (re-find #"VERDICT:\s*MERGE" output)
|
|
230
|
+
(re-find #"VERDICT:\s*DISCARD" output))]
|
|
231
|
+
(if (and (zero? (:exit res)) has-verdict?)
|
|
232
|
+
(reduced res)
|
|
233
|
+
res))
|
|
234
|
+
(catch Exception e
|
|
235
|
+
{:exit -1 :out "" :err (.getMessage e)})))
|
|
236
|
+
{:exit -1 :out ""}
|
|
237
|
+
reviewer-configs)
|
|
238
|
+
output (:out result)]
|
|
239
|
+
(cond
|
|
240
|
+
(re-find #"VERDICT:\s*MERGE" output) :merge
|
|
241
|
+
(re-find #"VERDICT:\s*DISCARD" output) :discard
|
|
242
|
+
:else :discard)))
|
|
243
|
+
|
|
244
|
+
(defn- handle-stale-worktrees!
|
|
245
|
+
"Non-destructive startup check for existing oompa worktrees.
|
|
246
|
+
|
|
247
|
+
- Always runs `git worktree prune` to clear orphaned metadata.
|
|
248
|
+
- Never auto-removes/merges/discards worktrees at startup.
|
|
249
|
+
- Prints a warning summary so concurrent swarms are not disrupted.
|
|
250
|
+
|
|
251
|
+
This avoids clobbering active work from another swarm in the same repo."
|
|
252
|
+
[_reviewer-configs]
|
|
253
|
+
;; Step 1: prune orphaned git metadata first
|
|
185
254
|
(let [prune-result (process/sh ["git" "worktree" "prune"] {:out :string :err :string})]
|
|
186
255
|
(when-not (zero? (:exit prune-result))
|
|
187
256
|
(println "WARNING: git worktree prune failed:")
|
|
188
257
|
(println (:err prune-result))))
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
br-result
|
|
258
|
+
|
|
259
|
+
;; Step 2: discover existing oompa worktree dirs and oompa/* branches
|
|
260
|
+
(let [ls-result (process/sh ["find" "." "-maxdepth" "1" "-type" "d" "-name" ".w*-i*"]
|
|
261
|
+
{:out :string})
|
|
262
|
+
stale-dirs (when (zero? (:exit ls-result))
|
|
263
|
+
(->> (str/split-lines (:out ls-result))
|
|
264
|
+
(remove str/blank?)))
|
|
265
|
+
br-result (process/sh ["git" "branch" "--list" "oompa/*"] {:out :string})
|
|
266
|
+
all-oompa-branches (when (zero? (:exit br-result))
|
|
267
|
+
(->> (str/split-lines (:out br-result))
|
|
268
|
+
(map str/trim)
|
|
269
|
+
(remove str/blank?)))]
|
|
270
|
+
|
|
271
|
+
(when (or (seq stale-dirs) (seq all-oompa-branches))
|
|
272
|
+
;; Step 3: classify for warning output only (no mutation)
|
|
273
|
+
(let [classified (mapv (fn [dir]
|
|
274
|
+
{:dir dir
|
|
275
|
+
:branch (worktree-branch-name dir)
|
|
276
|
+
:dirty? (dirty-worktree? dir)})
|
|
277
|
+
stale-dirs)
|
|
278
|
+
clean (filter (complement :dirty?) classified)
|
|
279
|
+
dirty (filter :dirty? classified)
|
|
280
|
+
dir-branches (set (keep :branch classified))
|
|
281
|
+
orphan-branches (remove #(contains? dir-branches %) all-oompa-branches)]
|
|
282
|
+
(println)
|
|
283
|
+
(println "WARNING: Existing oompa worktrees/branches detected; leaving them untouched.")
|
|
284
|
+
(println (format " Worktrees: %d (%d dirty, %d clean)"
|
|
285
|
+
(count classified) (count dirty) (count clean)))
|
|
286
|
+
(println (format " Orphan branches: %d" (count orphan-branches)))
|
|
287
|
+
(when (seq dirty)
|
|
288
|
+
(println " Dirty worktrees:")
|
|
289
|
+
(doseq [{:keys [dir branch]} dirty]
|
|
290
|
+
(println (format " %s (branch: %s)" dir (or branch "unknown")))))
|
|
291
|
+
(println " Run `oompa cleanup` manually when you want to reclaim them.")))))
|
|
292
|
+
|
|
293
|
+
(defn- cleanup-iteration-worktrees!
|
|
294
|
+
"Remove swarm iteration worktree dirs (.w*-i*) and oompa/* branches.
|
|
295
|
+
Returns {:dirs-removed n :branches-removed n}."
|
|
296
|
+
[]
|
|
297
|
+
(let [ls-result (process/sh ["find" "." "-maxdepth" "1" "-type" "d" "-name" ".w*-i*"]
|
|
197
298
|
{:out :string})
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
(doseq [b
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
(System/exit 1))))
|
|
299
|
+
dirs (if (zero? (:exit ls-result))
|
|
300
|
+
(->> (str/split-lines (:out ls-result))
|
|
301
|
+
(remove str/blank?))
|
|
302
|
+
[])
|
|
303
|
+
_ (doseq [dir dirs]
|
|
304
|
+
(remove-stale-worktree! dir (worktree-branch-name dir)))
|
|
305
|
+
br-result (process/sh ["git" "branch" "--list" "oompa/*"] {:out :string})
|
|
306
|
+
branches (if (zero? (:exit br-result))
|
|
307
|
+
(->> (str/split-lines (:out br-result))
|
|
308
|
+
(map str/trim)
|
|
309
|
+
(remove str/blank?))
|
|
310
|
+
[])
|
|
311
|
+
;; Branches may already be deleted by remove-stale-worktree!, so ignore failures.
|
|
312
|
+
_ (doseq [b branches]
|
|
313
|
+
(process/sh ["git" "branch" "-D" b] {:out :string :err :string}))]
|
|
314
|
+
{:dirs-removed (count dirs)
|
|
315
|
+
:branches-removed (count branches)}))
|
|
316
|
+
|
|
217
317
|
|
|
218
318
|
(defn- probe-model
|
|
219
319
|
"Send 'say ok' to a model via its harness CLI. Returns true if model responds.
|
|
@@ -429,7 +529,11 @@
|
|
|
429
529
|
[opts config-file]
|
|
430
530
|
(print-preflight-warnings!)
|
|
431
531
|
(when-not (.exists (io/file config-file))
|
|
432
|
-
(println (format "Config not found: %s" config-file))
|
|
532
|
+
(println (format "ERROR: Config file not found: %s" (.getCanonicalPath (io/file config-file))))
|
|
533
|
+
(println (format " Working directory: %s" (.getCanonicalPath (io/file "."))))
|
|
534
|
+
(println)
|
|
535
|
+
(println "Tip: paths are relative to the working directory. Did you mean:")
|
|
536
|
+
(println (format " oompa run --config oompa/%s" (.getName (io/file config-file))))
|
|
433
537
|
(System/exit 1))
|
|
434
538
|
(let [timeout-sec (or (:startup-timeout opts)
|
|
435
539
|
(parse-int (System/getenv "OOMPA_DETACH_STARTUP_TIMEOUT")
|
|
@@ -649,10 +753,10 @@
|
|
|
649
753
|
(println "No runs found."))))))
|
|
650
754
|
|
|
651
755
|
(def ^:private error-outcomes
|
|
652
|
-
#{"error" "merge-failed" "rejected" "stuck"})
|
|
756
|
+
#{"error" "merge-failed" "rejected" "stuck" "needs-followup"})
|
|
653
757
|
|
|
654
758
|
(def ^:private terminal-run-outcomes
|
|
655
|
-
#{"merged" "rejected" "error" "merge-failed" "sync-failed" "stuck" "no-changes"})
|
|
759
|
+
#{"merged" "rejected" "error" "merge-failed" "sync-failed" "stuck" "no-changes" "needs-followup"})
|
|
656
760
|
|
|
657
761
|
(defn- run-state
|
|
658
762
|
"Derive run lifecycle state from started/stopped events + PID liveness."
|
|
@@ -826,15 +930,18 @@
|
|
|
826
930
|
(println "No worktrees initialized."))))
|
|
827
931
|
|
|
828
932
|
(defn cmd-cleanup
|
|
829
|
-
"Remove all worktrees"
|
|
933
|
+
"Remove all worktrees (legacy pool + swarm iteration worktrees)."
|
|
830
934
|
[opts args]
|
|
831
935
|
(let [state-file (io/file ".workers/state.edn")]
|
|
936
|
+
(println "Removing worktrees...")
|
|
832
937
|
(if (.exists state-file)
|
|
833
938
|
(let [pool (read-string (slurp state-file))]
|
|
834
|
-
(
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
(println "
|
|
939
|
+
(worktree/cleanup-pool! pool))
|
|
940
|
+
(println "No legacy pool worktrees to clean up."))
|
|
941
|
+
(let [{:keys [dirs-removed branches-removed]} (cleanup-iteration-worktrees!)]
|
|
942
|
+
(println (format "Removed %d swarm worktree dir(s) and %d oompa branch(es)."
|
|
943
|
+
dirs-removed branches-removed)))
|
|
944
|
+
(println "Done.")))
|
|
838
945
|
|
|
839
946
|
(defn cmd-context
|
|
840
947
|
"Print current context (for debugging prompts)"
|
|
@@ -882,6 +989,30 @@
|
|
|
882
989
|
{:harness :codex :model s}))
|
|
883
990
|
{:harness :codex :model s}))
|
|
884
991
|
|
|
992
|
+
(defn- parse-reviewer-entry
|
|
993
|
+
"Parse reviewer config entry from either:
|
|
994
|
+
1) string model spec: \"harness:model[:reasoning]\"
|
|
995
|
+
2) map: {:model \"...\" :prompt \"path\"|[...]}.
|
|
996
|
+
Returns nil for invalid entries."
|
|
997
|
+
[entry]
|
|
998
|
+
(cond
|
|
999
|
+
(string? entry)
|
|
1000
|
+
(parse-model-string entry)
|
|
1001
|
+
|
|
1002
|
+
(map? entry)
|
|
1003
|
+
(let [model (:model entry)]
|
|
1004
|
+
(when (string? model)
|
|
1005
|
+
(let [parsed (parse-model-string model)
|
|
1006
|
+
prompts (let [p (:prompt entry)]
|
|
1007
|
+
(cond
|
|
1008
|
+
(vector? p) p
|
|
1009
|
+
(string? p) [p]
|
|
1010
|
+
:else []))]
|
|
1011
|
+
(assoc parsed :prompts prompts))))
|
|
1012
|
+
|
|
1013
|
+
:else
|
|
1014
|
+
nil))
|
|
1015
|
+
|
|
885
1016
|
(defn cmd-swarm
|
|
886
1017
|
"Run multiple worker configs from oompa.json in parallel"
|
|
887
1018
|
[opts args]
|
|
@@ -889,35 +1020,36 @@
|
|
|
889
1020
|
f (io/file config-file)
|
|
890
1021
|
swarm-id (make-swarm-id)]
|
|
891
1022
|
(when-not (.exists f)
|
|
892
|
-
(println (format "Config file not found: %s"
|
|
1023
|
+
(println (format "ERROR: Config file not found: %s" (.getCanonicalPath f)))
|
|
1024
|
+
(println (format " Working directory: %s" (.getCanonicalPath (io/file "."))))
|
|
893
1025
|
(println)
|
|
894
|
-
(println "
|
|
895
|
-
(println "
|
|
896
|
-
(println " \"workers\": [")
|
|
897
|
-
(println " {\"model\": \"codex:gpt-5.3-codex:medium\", \"prompt\": \"prompts/executor.md\", \"iterations\": 10, \"count\": 3, \"can_plan\": false},")
|
|
898
|
-
(println " {\"model\": \"claude:opus\", \"prompt\": [\"prompts/base.md\", \"prompts/planner.md\"], \"count\": 1},")
|
|
899
|
-
(println " {\"model\": \"gemini:gemini-3-pro-preview\", \"prompt\": [\"prompts/executor.md\"], \"count\": 1}")
|
|
900
|
-
(println " ]")
|
|
901
|
-
(println "}")
|
|
902
|
-
(println)
|
|
903
|
-
(println "prompt: string or array of paths — concatenated into one prompt.")
|
|
1026
|
+
(println "Tip: paths are relative to the working directory. Did you mean:")
|
|
1027
|
+
(println (format " oompa run --config oompa/%s" (.getName f)))
|
|
904
1028
|
(System/exit 1))
|
|
905
1029
|
;; Preflight: abort if git is dirty to prevent merge conflicts
|
|
906
1030
|
(check-git-clean!)
|
|
907
|
-
;; Preflight: abort if stale worktrees from prior runs would poison git
|
|
908
|
-
(check-stale-worktrees!)
|
|
909
1031
|
|
|
910
1032
|
(let [config (json/parse-string (slurp f) true)
|
|
911
|
-
;; Parse reviewer config — supports
|
|
912
|
-
;; Legacy: {"review_model": "harness:model:reasoning"}
|
|
913
|
-
;; New: {"reviewer": {"model": "harness:model:reasoning", "prompt": ["path.md"]}}
|
|
1033
|
+
;; Parse reviewer config — supports legacy + new formats.
|
|
914
1034
|
generic-reviewers (cond
|
|
915
1035
|
(:review_models config)
|
|
916
|
-
(
|
|
917
|
-
|
|
1036
|
+
(->> (:review_models config)
|
|
1037
|
+
(map parse-reviewer-entry)
|
|
1038
|
+
(remove nil?)
|
|
1039
|
+
vec)
|
|
1040
|
+
|
|
918
1041
|
(:review_model config)
|
|
919
|
-
|
|
920
|
-
|
|
1042
|
+
(->> [(:review_model config)]
|
|
1043
|
+
(map parse-reviewer-entry)
|
|
1044
|
+
(remove nil?)
|
|
1045
|
+
vec)
|
|
1046
|
+
|
|
1047
|
+
(:reviewer config)
|
|
1048
|
+
(->> [(:reviewer config)]
|
|
1049
|
+
(map parse-reviewer-entry)
|
|
1050
|
+
(remove nil?)
|
|
1051
|
+
vec)
|
|
1052
|
+
|
|
921
1053
|
:else [])
|
|
922
1054
|
|
|
923
1055
|
;; Parse planner config — optional dedicated planner
|
|
@@ -933,8 +1065,19 @@
|
|
|
933
1065
|
:prompts prompts
|
|
934
1066
|
:max-pending (or (:max_pending planner-config) 10))))
|
|
935
1067
|
|
|
1068
|
+
|
|
936
1069
|
worker-configs (:workers config)
|
|
937
1070
|
|
|
1071
|
+
;; Require max_cycle to be present on all workers
|
|
1072
|
+
_ (doseq [[idx wc] (map-indexed vector worker-configs)]
|
|
1073
|
+
(when (or (:iterations wc) (:max_cycles wc))
|
|
1074
|
+
(println (format "ERROR: Worker %d uses deprecated 'iterations' or 'max_cycles'. Consolidate strictly on 'max_cycle'." idx))
|
|
1075
|
+
(System/exit 1))
|
|
1076
|
+
(when-not (:max_cycle wc)
|
|
1077
|
+
(println (format "ERROR: Worker %d missing 'max_cycle' in config." idx))
|
|
1078
|
+
(System/exit 1)))
|
|
1079
|
+
|
|
1080
|
+
|
|
938
1081
|
;; Expand worker configs by count
|
|
939
1082
|
expanded-workers (mapcat (fn [wc]
|
|
940
1083
|
(let [cnt (or (:count wc) 1)]
|
|
@@ -945,16 +1088,38 @@
|
|
|
945
1088
|
workers (map-indexed
|
|
946
1089
|
(fn [idx wc]
|
|
947
1090
|
(let [{:keys [harness model reasoning]} (parse-model-string (:model wc))
|
|
948
|
-
;; Support per-worker reviewer override
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1091
|
+
;; Support per-worker reviewer override (legacy + new):
|
|
1092
|
+
;; - review_model: "harness:model"
|
|
1093
|
+
;; - review_models: ["harness:model", ...]
|
|
1094
|
+
;; - reviewer: {model, prompt}
|
|
1095
|
+
;; - reviewers: [string|map, ...]
|
|
1096
|
+
worker-reviewers (cond
|
|
1097
|
+
(:reviewers wc)
|
|
1098
|
+
(->> (:reviewers wc)
|
|
1099
|
+
(map parse-reviewer-entry)
|
|
1100
|
+
(remove nil?)
|
|
1101
|
+
vec)
|
|
1102
|
+
|
|
1103
|
+
(:review_models wc)
|
|
1104
|
+
(->> (:review_models wc)
|
|
1105
|
+
(map parse-reviewer-entry)
|
|
1106
|
+
(remove nil?)
|
|
1107
|
+
vec)
|
|
1108
|
+
|
|
1109
|
+
(:reviewer wc)
|
|
1110
|
+
(->> [(:reviewer wc)]
|
|
1111
|
+
(map parse-reviewer-entry)
|
|
1112
|
+
(remove nil?)
|
|
1113
|
+
vec)
|
|
1114
|
+
|
|
1115
|
+
(:review_model wc)
|
|
1116
|
+
(->> [(:review_model wc)]
|
|
1117
|
+
(map parse-reviewer-entry)
|
|
1118
|
+
(remove nil?)
|
|
1119
|
+
vec)
|
|
1120
|
+
|
|
1121
|
+
:else [])
|
|
1122
|
+
all-reviewers (->> (concat worker-reviewers generic-reviewers)
|
|
958
1123
|
(map #(select-keys % [:harness :model :reasoning :prompts]))
|
|
959
1124
|
(distinct)
|
|
960
1125
|
(vec))]
|
|
@@ -964,9 +1129,9 @@
|
|
|
964
1129
|
:harness harness
|
|
965
1130
|
:model model
|
|
966
1131
|
:reasoning reasoning
|
|
967
|
-
:runs (or (:runs wc)
|
|
968
|
-
:max-cycles (
|
|
969
|
-
:iterations (
|
|
1132
|
+
:runs (or (:runs wc) 10)
|
|
1133
|
+
:max-cycles (:max_cycle wc)
|
|
1134
|
+
:iterations (:max_cycle wc)
|
|
970
1135
|
:prompts (:prompt wc)
|
|
971
1136
|
:can-plan (:can_plan wc)
|
|
972
1137
|
:wait-between (:wait_between wc)
|
|
@@ -975,6 +1140,10 @@
|
|
|
975
1140
|
:reviewers all-reviewers})))
|
|
976
1141
|
expanded-workers)]
|
|
977
1142
|
|
|
1143
|
+
;; Preflight: handle stale worktrees from prior runs before launching workers.
|
|
1144
|
+
;; Empty ones are auto-cleaned silently; dirty ones trigger an interactive review.
|
|
1145
|
+
(handle-stale-worktrees! generic-reviewers)
|
|
1146
|
+
|
|
978
1147
|
(println (format "Swarm config from %s:" config-file))
|
|
979
1148
|
(println (format " Swarm ID: %s" swarm-id))
|
|
980
1149
|
(when planner-parsed
|
|
@@ -996,8 +1165,8 @@
|
|
|
996
1165
|
(name harness)
|
|
997
1166
|
model
|
|
998
1167
|
(if reasoning (str ":" reasoning) "")
|
|
999
|
-
(or (:runs wc)
|
|
1000
|
-
(
|
|
1168
|
+
(or (:runs wc) 10)
|
|
1169
|
+
(:max_cycle wc)
|
|
1001
1170
|
(if (:prompt wc) (str ", " (:prompt wc)) "")))))
|
|
1002
1171
|
(println)
|
|
1003
1172
|
|
|
@@ -1046,6 +1215,26 @@
|
|
|
1046
1215
|
(doseq [t (tasks/list-current)]
|
|
1047
1216
|
(println (format " - %s: %s" (:id t) (:summary t)))))))
|
|
1048
1217
|
|
|
1218
|
+
(defn cmd-requeue
|
|
1219
|
+
"Move current/ tasks back to pending/.
|
|
1220
|
+
With args, only requeue those task IDs. Without args, requeue all current tasks."
|
|
1221
|
+
[opts args]
|
|
1222
|
+
(tasks/ensure-dirs!)
|
|
1223
|
+
(let [current-ids (->> (tasks/list-current) (map :id) set)
|
|
1224
|
+
requested-ids (if (seq args) (set args) current-ids)
|
|
1225
|
+
recyclable-ids (set (filter current-ids requested-ids))
|
|
1226
|
+
recycled (if (seq args)
|
|
1227
|
+
(tasks/recycle-tasks! recyclable-ids)
|
|
1228
|
+
(tasks/recycle-all-current!))
|
|
1229
|
+
missing (sort (remove recyclable-ids requested-ids))]
|
|
1230
|
+
(if (seq recycled)
|
|
1231
|
+
(println (format "Requeued %d task(s): %s"
|
|
1232
|
+
(count recycled)
|
|
1233
|
+
(str/join ", " recycled)))
|
|
1234
|
+
(println "No current tasks were requeued."))
|
|
1235
|
+
(when (seq missing)
|
|
1236
|
+
(println (format "Not in current/: %s" (str/join ", " missing))))))
|
|
1237
|
+
|
|
1049
1238
|
(defn- find-latest-swarm-id
|
|
1050
1239
|
"Find the most recent swarm ID from runs/ directory."
|
|
1051
1240
|
[]
|
|
@@ -1121,6 +1310,7 @@
|
|
|
1121
1310
|
(println " loop N Run N iterations")
|
|
1122
1311
|
(println " swarm [file] Run multiple worker configs from oompa.json (parallel)")
|
|
1123
1312
|
(println " tasks Show task status (pending/current/complete)")
|
|
1313
|
+
(println " requeue [ids..] Move current tasks back to pending")
|
|
1124
1314
|
(println " prompt \"...\" Run ad-hoc prompt")
|
|
1125
1315
|
(println " status Show running swarms")
|
|
1126
1316
|
(println " info Show detailed summary of the last run")
|
|
@@ -1186,6 +1376,7 @@
|
|
|
1186
1376
|
"loop" cmd-loop
|
|
1187
1377
|
"swarm" cmd-swarm
|
|
1188
1378
|
"tasks" cmd-tasks
|
|
1379
|
+
"requeue" cmd-requeue
|
|
1189
1380
|
"prompt" cmd-prompt
|
|
1190
1381
|
"status" cmd-status
|
|
1191
1382
|
"info" cmd-info
|