@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 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"], "iterations": 5, "count": 1},
87
- {"model": "codex:gpt-5.3-codex:medium", "prompt": ["config/prompts/executor.md"], "iterations": 10, "count": 2, "can_plan": false},
88
- {"model": "opencode:opencode/kimi-k2.5-free", "prompt": ["config/prompts/executor.md"], "iterations": 10, "count": 1, "can_plan": false}
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
- | `iterations` | no | Max iterations per worker (default: 10) |
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
- "Read a prompt file once and cache by canonical path."
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
- (if-let [cached (get @prompt-file-cache path)]
183
- cached
184
- (let [f (io/file path)]
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- 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
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
- (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/*"]
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
- 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.")
205
- (println)
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*")
215
- (println)
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
- (println "Removing worktrees...")
835
- (worktree/cleanup-pool! pool)
836
- (println "Done."))
837
- (println "No worktrees to clean up."))))
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" config-file))
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 "Create oompa.json with format:")
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 both formats:
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
- (mapv parse-model-string (:review_models config))
917
-
1036
+ (->> (:review_models config)
1037
+ (map parse-reviewer-entry)
1038
+ (remove nil?)
1039
+ vec)
1040
+
918
1041
  (:review_model config)
919
- [(parse-model-string (:review_model config))]
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
- 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)
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) (:iterations wc) 10)
968
- :max-cycles (or (:max_cycles wc) (:iterations wc) (:runs wc) 10)
969
- :iterations (or (:iterations wc) 10)
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) (:iterations wc) 10)
1000
- (or (:max_cycles wc) (:iterations wc) (:runs wc) 10)
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