@nbardy/oompa 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -130,6 +130,11 @@
|
|
|
130
130
|
[output]
|
|
131
131
|
(boolean (re-find #"__DONE__" (or output ""))))
|
|
132
132
|
|
|
133
|
+
(defn merge-signal?
|
|
134
|
+
"Check if output contains COMPLETE_AND_READY_FOR_MERGE signal"
|
|
135
|
+
[output]
|
|
136
|
+
(boolean (re-find #"COMPLETE_AND_READY_FOR_MERGE" (or output ""))))
|
|
137
|
+
|
|
133
138
|
(defn- extract-comments
|
|
134
139
|
"Extract bullet-point comments from output"
|
|
135
140
|
[output]
|
|
@@ -165,32 +165,39 @@
|
|
|
165
165
|
(count pending) (count current) (count complete))}))
|
|
166
166
|
|
|
167
167
|
(defn- run-agent!
|
|
168
|
-
"Run agent with prompt, return {:output string, :done? bool, :exit int}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
168
|
+
"Run agent with prompt, return {:output string, :done? bool, :merge? bool, :exit int, :session-id string}.
|
|
169
|
+
When resume? is true and harness is :claude, uses --resume to continue the existing session
|
|
170
|
+
with a lighter prompt (just task status + continue instruction)."
|
|
171
|
+
[{:keys [id swarm-id harness model prompts reasoning]} worktree-path context session-id resume?]
|
|
172
|
+
(let [;; Use provided session-id or generate fresh one
|
|
173
|
+
session-id (or session-id (str/lower-case (str (java.util.UUID/randomUUID))))
|
|
174
|
+
|
|
175
|
+
;; Build prompt — lighter for resume (agent already has full context)
|
|
176
|
+
prompt (if resume?
|
|
177
|
+
(str "Task Status: " (:task_status context) "\n"
|
|
178
|
+
"Pending: " (:pending_tasks context) "\n\n"
|
|
179
|
+
"Continue working. Signal COMPLETE_AND_READY_FOR_MERGE when your current task is done and ready for review.")
|
|
180
|
+
(let [task-header (or (load-prompt "config/prompts/_task_header.md") "")
|
|
181
|
+
user-prompts (if (seq prompts)
|
|
182
|
+
(->> prompts
|
|
183
|
+
(map load-prompt)
|
|
184
|
+
(remove nil?)
|
|
185
|
+
(str/join "\n\n"))
|
|
186
|
+
(or (load-prompt "config/prompts/worker.md")
|
|
187
|
+
"You are a worker. Claim tasks, execute them, complete them."))]
|
|
188
|
+
(str task-header "\n"
|
|
189
|
+
"Task Status: " (:task_status context) "\n"
|
|
190
|
+
"Pending: " (:pending_tasks context) "\n\n"
|
|
191
|
+
user-prompts)))
|
|
192
|
+
|
|
188
193
|
swarm-id* (or swarm-id "unknown")
|
|
189
|
-
tagged-prompt (str "[oompa:" swarm-id* ":" id "] "
|
|
194
|
+
tagged-prompt (str "[oompa:" swarm-id* ":" id "] " prompt)
|
|
190
195
|
abs-worktree (.getAbsolutePath (io/file worktree-path))
|
|
191
196
|
|
|
192
197
|
;; Build command — both harnesses run with cwd=worktree, no sandbox
|
|
193
198
|
;; so agents can `..` to reach project root for task management
|
|
199
|
+
;; Claude: --resume flag continues existing session-id conversation
|
|
200
|
+
;; Codex: no resume support, always fresh (but worktree state persists)
|
|
194
201
|
cmd (case harness
|
|
195
202
|
:codex (cond-> [(resolve-binary! "codex") "exec"
|
|
196
203
|
"--dangerously-bypass-approvals-and-sandbox"
|
|
@@ -198,9 +205,10 @@
|
|
|
198
205
|
"-C" abs-worktree]
|
|
199
206
|
model (into ["--model" model])
|
|
200
207
|
reasoning (into ["-c" (str "model_reasoning_effort=\"" reasoning "\"")])
|
|
201
|
-
true (conj "--"
|
|
208
|
+
true (conj "--" tagged-prompt))
|
|
202
209
|
:claude (cond-> [(resolve-binary! "claude") "-p" "--dangerously-skip-permissions"
|
|
203
210
|
"--session-id" session-id]
|
|
211
|
+
resume? (conj "--resume")
|
|
204
212
|
model (into ["--model" model])))
|
|
205
213
|
|
|
206
214
|
_ (when (= harness :codex)
|
|
@@ -220,7 +228,9 @@
|
|
|
220
228
|
|
|
221
229
|
{:output (:out result)
|
|
222
230
|
:exit (:exit result)
|
|
223
|
-
:done? (agent/done-signal? (:out result))
|
|
231
|
+
:done? (agent/done-signal? (:out result))
|
|
232
|
+
:merge? (agent/merge-signal? (:out result))
|
|
233
|
+
:session-id session-id}))
|
|
224
234
|
|
|
225
235
|
(defn- run-reviewer!
|
|
226
236
|
"Run reviewer on worktree changes.
|
|
@@ -308,13 +318,42 @@
|
|
|
308
318
|
{:output (:out result)
|
|
309
319
|
:exit (:exit result)}))
|
|
310
320
|
|
|
321
|
+
(defn- worktree-has-changes?
|
|
322
|
+
"Check if worktree has any uncommitted changes (new/modified/deleted files)."
|
|
323
|
+
[wt-path]
|
|
324
|
+
(let [result (process/sh ["git" "status" "--porcelain"] {:dir wt-path :out :string :err :string})]
|
|
325
|
+
(not (str/blank? (:out result)))))
|
|
326
|
+
|
|
327
|
+
(defn- create-iteration-worktree!
|
|
328
|
+
"Create a fresh worktree for an iteration. Returns {:dir :branch :path}.
|
|
329
|
+
Force-removes stale worktree+branch from previous failed runs first."
|
|
330
|
+
[project-root worker-id iteration]
|
|
331
|
+
(let [wt-dir (format ".w%s-i%d" worker-id iteration)
|
|
332
|
+
wt-branch (format "oompa/%s-i%d" worker-id iteration)
|
|
333
|
+
wt-path (str project-root "/" wt-dir)]
|
|
334
|
+
;; Clean stale worktree/branch from previous failed runs
|
|
335
|
+
(process/sh ["git" "worktree" "remove" wt-dir "--force"] {:dir project-root})
|
|
336
|
+
(process/sh ["git" "branch" "-D" wt-branch] {:dir project-root})
|
|
337
|
+
(let [result (process/sh ["git" "worktree" "add" wt-dir "-b" wt-branch]
|
|
338
|
+
{:dir project-root :out :string :err :string})]
|
|
339
|
+
(when-not (zero? (:exit result))
|
|
340
|
+
(throw (ex-info (str "Failed to create worktree: " (:err result))
|
|
341
|
+
{:dir wt-dir :branch wt-branch}))))
|
|
342
|
+
{:dir wt-dir :branch wt-branch :path wt-path}))
|
|
343
|
+
|
|
344
|
+
(defn- cleanup-worktree!
|
|
345
|
+
"Remove worktree and branch."
|
|
346
|
+
[project-root wt-dir wt-branch]
|
|
347
|
+
(process/sh ["git" "worktree" "remove" wt-dir "--force"] {:dir project-root})
|
|
348
|
+
(process/sh ["git" "branch" "-D" wt-branch] {:dir project-root}))
|
|
349
|
+
|
|
311
350
|
(defn- merge-to-main!
|
|
312
351
|
"Merge worktree changes to main branch"
|
|
313
352
|
[wt-path wt-id worker-id project-root]
|
|
314
353
|
(println (format "[%s] Merging changes to main" worker-id))
|
|
315
354
|
(let [;; Commit in worktree if needed
|
|
316
355
|
_ (process/sh ["git" "add" "-A"] {:dir wt-path})
|
|
317
|
-
_ (process/sh ["git" "commit" "-m" (str "Work from " wt-id)
|
|
356
|
+
_ (process/sh ["git" "commit" "-m" (str "Work from " wt-id)]
|
|
318
357
|
{:dir wt-path})
|
|
319
358
|
;; Checkout main and merge (in project root, not worktree)
|
|
320
359
|
checkout-result (process/sh ["git" "checkout" "main"]
|
|
@@ -358,82 +397,6 @@
|
|
|
358
397
|
(run-fix! worker wt-path output)
|
|
359
398
|
(recur (inc attempt)))))))))
|
|
360
399
|
|
|
361
|
-
(defn execute-iteration!
|
|
362
|
-
"Execute one iteration of work.
|
|
363
|
-
|
|
364
|
-
Flow:
|
|
365
|
-
1. Create worktree
|
|
366
|
-
2. Run agent
|
|
367
|
-
3. If reviewer configured: run review loop (fix → retry → reviewer)
|
|
368
|
-
4. If approved: merge to main
|
|
369
|
-
5. Cleanup worktree
|
|
370
|
-
|
|
371
|
-
Returns {:status :done|:continue|:error, :task task-or-nil}"
|
|
372
|
-
[worker iteration total-iterations]
|
|
373
|
-
(let [worker-id (:id worker)
|
|
374
|
-
project-root (System/getProperty "user.dir")
|
|
375
|
-
wt-dir (format ".w%s-i%d" worker-id iteration)
|
|
376
|
-
wt-branch (format "oompa/%s-i%d" worker-id iteration)
|
|
377
|
-
|
|
378
|
-
;; Create worktree
|
|
379
|
-
_ (println (format "[%s] Starting iteration %d/%d" worker-id iteration total-iterations))
|
|
380
|
-
wt-path (str project-root "/" wt-dir)]
|
|
381
|
-
|
|
382
|
-
(try
|
|
383
|
-
;; Clean stale worktree/branch from previous failed runs before creating
|
|
384
|
-
(process/sh ["git" "worktree" "remove" wt-dir "--force"] {:dir project-root})
|
|
385
|
-
(process/sh ["git" "branch" "-D" wt-branch] {:dir project-root})
|
|
386
|
-
|
|
387
|
-
;; Setup worktree (in project root)
|
|
388
|
-
(let [wt-result (process/sh ["git" "worktree" "add" wt-dir "-b" wt-branch]
|
|
389
|
-
{:dir project-root :out :string :err :string})]
|
|
390
|
-
(when-not (zero? (:exit wt-result))
|
|
391
|
-
(throw (ex-info (str "Failed to create worktree: " (:err wt-result))
|
|
392
|
-
{:dir wt-dir :branch wt-branch}))))
|
|
393
|
-
|
|
394
|
-
;; Build context
|
|
395
|
-
(let [context (build-context)
|
|
396
|
-
|
|
397
|
-
;; Run agent
|
|
398
|
-
{:keys [output exit done?]} (run-agent! worker wt-path context)]
|
|
399
|
-
|
|
400
|
-
(cond
|
|
401
|
-
;; Agent signaled done — only honor for can_plan workers.
|
|
402
|
-
;; Executors (can_plan: false) can't decide work is done.
|
|
403
|
-
(and done? (:can-plan worker))
|
|
404
|
-
(do
|
|
405
|
-
(println (format "[%s] Received __DONE__ signal" worker-id))
|
|
406
|
-
{:status :done})
|
|
407
|
-
|
|
408
|
-
;; can_plan: false worker said __DONE__ — ignore it, treat as success
|
|
409
|
-
(and done? (not (:can-plan worker)))
|
|
410
|
-
(do
|
|
411
|
-
(println (format "[%s] Ignoring __DONE__ (executor cannot signal done)" worker-id))
|
|
412
|
-
{:status :continue})
|
|
413
|
-
|
|
414
|
-
;; Agent errored
|
|
415
|
-
(not (zero? exit))
|
|
416
|
-
(do
|
|
417
|
-
(println (format "[%s] Agent error (exit %d): %s" worker-id exit (subs (or output "") 0 (min 200 (count (or output ""))))))
|
|
418
|
-
{:status :error :exit exit})
|
|
419
|
-
|
|
420
|
-
;; Success - run review loop before merge
|
|
421
|
-
:else
|
|
422
|
-
(let [{:keys [approved?]} (review-loop! worker wt-path worker-id)]
|
|
423
|
-
(if approved?
|
|
424
|
-
(do
|
|
425
|
-
(merge-to-main! wt-path wt-branch worker-id project-root)
|
|
426
|
-
(println (format "[%s] Iteration %d/%d complete" worker-id iteration total-iterations))
|
|
427
|
-
{:status :continue})
|
|
428
|
-
(do
|
|
429
|
-
(println (format "[%s] Iteration %d/%d rejected, discarding" worker-id iteration total-iterations))
|
|
430
|
-
{:status :continue})))))
|
|
431
|
-
|
|
432
|
-
(finally
|
|
433
|
-
;; Cleanup worktree and branch (in project root)
|
|
434
|
-
(process/sh ["git" "worktree" "remove" wt-dir "--force"] {:dir project-root})
|
|
435
|
-
(process/sh ["git" "branch" "-D" wt-branch] {:dir project-root})))))
|
|
436
|
-
|
|
437
400
|
;; =============================================================================
|
|
438
401
|
;; Worker Loop
|
|
439
402
|
;; =============================================================================
|
|
@@ -460,12 +423,17 @@
|
|
|
460
423
|
(recur (+ waited wait-poll-interval))))))
|
|
461
424
|
|
|
462
425
|
(defn run-worker!
|
|
463
|
-
"Run worker loop
|
|
426
|
+
"Run worker loop with persistent sessions.
|
|
427
|
+
|
|
428
|
+
Sessions persist across iterations — agents resume where they left off.
|
|
429
|
+
Worktrees persist until COMPLETE_AND_READY_FOR_MERGE triggers review+merge.
|
|
430
|
+
__DONE__ stops the worker entirely (planners only).
|
|
464
431
|
|
|
465
432
|
Returns final worker state."
|
|
466
433
|
[worker]
|
|
467
434
|
(tasks/ensure-dirs!)
|
|
468
|
-
(let [{:keys [id iterations]} worker
|
|
435
|
+
(let [{:keys [id iterations]} worker
|
|
436
|
+
project-root (System/getProperty "user.dir")]
|
|
469
437
|
(println (format "[%s] Starting worker (%s:%s%s, %d iterations)"
|
|
470
438
|
id
|
|
471
439
|
(name (:harness worker))
|
|
@@ -479,32 +447,97 @@
|
|
|
479
447
|
|
|
480
448
|
(loop [iter 1
|
|
481
449
|
completed 0
|
|
482
|
-
consec-errors 0
|
|
450
|
+
consec-errors 0
|
|
451
|
+
session-id nil ;; persistent session-id (nil = start fresh)
|
|
452
|
+
wt-state nil] ;; {:dir :branch :path} or nil
|
|
483
453
|
(if (> iter iterations)
|
|
484
454
|
(do
|
|
455
|
+
;; Cleanup any lingering worktree
|
|
456
|
+
(when wt-state
|
|
457
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state)))
|
|
485
458
|
(println (format "[%s] Completed %d iterations" id completed))
|
|
486
459
|
(assoc worker :completed completed :status :exhausted))
|
|
487
460
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
461
|
+
;; Ensure worktree exists (create fresh if nil, reuse if persisted)
|
|
462
|
+
(let [wt-state (try
|
|
463
|
+
(or wt-state (create-iteration-worktree! project-root id iter))
|
|
464
|
+
(catch Exception e
|
|
465
|
+
(println (format "[%s] Worktree creation failed: %s" id (.getMessage e)))
|
|
466
|
+
nil))]
|
|
467
|
+
(if (nil? wt-state)
|
|
468
|
+
;; Worktree creation failed — count as error
|
|
496
469
|
(let [errors (inc consec-errors)]
|
|
497
470
|
(if (>= errors max-consecutive-errors)
|
|
498
471
|
(do
|
|
499
|
-
(println (format "[%s] %d consecutive errors, stopping
|
|
472
|
+
(println (format "[%s] %d consecutive errors, stopping" id errors))
|
|
500
473
|
(assoc worker :completed completed :status :error))
|
|
474
|
+
(recur (inc iter) completed errors nil nil)))
|
|
475
|
+
|
|
476
|
+
;; Worktree ready — run agent
|
|
477
|
+
(let [resume? (some? session-id)
|
|
478
|
+
_ (println (format "[%s] %s iteration %d/%d"
|
|
479
|
+
id (if resume? "Resuming" "Starting") iter iterations))
|
|
480
|
+
context (build-context)
|
|
481
|
+
{:keys [output exit done? merge?] :as agent-result}
|
|
482
|
+
(run-agent! worker (:path wt-state) context session-id resume?)
|
|
483
|
+
new-session-id (:session-id agent-result)]
|
|
484
|
+
|
|
485
|
+
(cond
|
|
486
|
+
;; Agent errored — cleanup, reset session
|
|
487
|
+
(not (zero? exit))
|
|
488
|
+
(let [errors (inc consec-errors)]
|
|
489
|
+
(println (format "[%s] Agent error (exit %d): %s"
|
|
490
|
+
id exit (subs (or output "") 0 (min 200 (count (or output ""))))))
|
|
491
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
492
|
+
(if (>= errors max-consecutive-errors)
|
|
493
|
+
(do
|
|
494
|
+
(println (format "[%s] %d consecutive errors, stopping" id errors))
|
|
495
|
+
(assoc worker :completed completed :status :error))
|
|
496
|
+
(recur (inc iter) completed errors nil nil)))
|
|
497
|
+
|
|
498
|
+
;; COMPLETE_AND_READY_FOR_MERGE — review, merge, reset session
|
|
499
|
+
merge?
|
|
500
|
+
(if (worktree-has-changes? (:path wt-state))
|
|
501
|
+
(let [{:keys [approved?]} (review-loop! worker (:path wt-state) id)]
|
|
502
|
+
(if approved?
|
|
503
|
+
(do
|
|
504
|
+
(merge-to-main! (:path wt-state) (:branch wt-state) id project-root)
|
|
505
|
+
(println (format "[%s] Iteration %d/%d complete" id iter iterations))
|
|
506
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
507
|
+
;; If also __DONE__, stop after merge
|
|
508
|
+
(if (and done? (:can-plan worker))
|
|
509
|
+
(do
|
|
510
|
+
(println (format "[%s] Worker done after merge" id))
|
|
511
|
+
(assoc worker :completed (inc completed) :status :done))
|
|
512
|
+
(recur (inc iter) (inc completed) 0 nil nil)))
|
|
513
|
+
(do
|
|
514
|
+
(println (format "[%s] Iteration %d/%d rejected" id iter iterations))
|
|
515
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
516
|
+
(recur (inc iter) completed 0 nil nil))))
|
|
517
|
+
(do
|
|
518
|
+
(println (format "[%s] Merge signaled but no changes, skipping" id))
|
|
519
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
520
|
+
(recur (inc iter) completed 0 nil nil)))
|
|
521
|
+
|
|
522
|
+
;; __DONE__ without merge — only honor for planners
|
|
523
|
+
(and done? (:can-plan worker))
|
|
501
524
|
(do
|
|
502
|
-
(println (format "[%s]
|
|
503
|
-
|
|
504
|
-
(
|
|
525
|
+
(println (format "[%s] Received __DONE__ signal" id))
|
|
526
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
527
|
+
(println (format "[%s] Worker done after %d/%d iterations" id iter iterations))
|
|
528
|
+
(assoc worker :completed completed :status :done))
|
|
505
529
|
|
|
506
|
-
|
|
507
|
-
|
|
530
|
+
;; __DONE__ from executor — ignore, keep working
|
|
531
|
+
(and done? (not (:can-plan worker)))
|
|
532
|
+
(do
|
|
533
|
+
(println (format "[%s] Ignoring __DONE__ (executor)" id))
|
|
534
|
+
(recur (inc iter) completed consec-errors new-session-id wt-state))
|
|
535
|
+
|
|
536
|
+
;; No signal — agent still working, resume next iteration
|
|
537
|
+
:else
|
|
538
|
+
(do
|
|
539
|
+
(println (format "[%s] Working... (will resume)" id))
|
|
540
|
+
(recur (inc iter) completed 0 new-session-id wt-state))))))))))
|
|
508
541
|
|
|
509
542
|
;; =============================================================================
|
|
510
543
|
;; Multi-Worker Execution
|
|
@@ -47,10 +47,17 @@ EOF
|
|
|
47
47
|
- Claim one task, execute it end-to-end, complete it.
|
|
48
48
|
- If work emerges during execution, create new tasks in `../tasks/pending/`.
|
|
49
49
|
|
|
50
|
+
### Signals
|
|
51
|
+
|
|
52
|
+
Your session persists across iterations. Keep working until your task is complete.
|
|
53
|
+
|
|
54
|
+
- **`COMPLETE_AND_READY_FOR_MERGE`**: Output this on its own line when your current work is done and ready for review. Your changes will be reviewed and merged, then you start a fresh session.
|
|
55
|
+
- **`__DONE__`**: Output this only when ALL project work is truly complete and no more tasks can be derived from the spec. This stops your worker entirely.
|
|
56
|
+
|
|
50
57
|
### Rules
|
|
51
58
|
|
|
52
59
|
- Before starting work: read the project spec and all tasks to understand scope.
|
|
53
60
|
- Claim your task by moving it to `../tasks/current/`.
|
|
54
61
|
- If the `mv` fails (another worker claimed it first), pick a different task.
|
|
55
62
|
- One task per commit (or a small, tightly-related set with overlapping files).
|
|
56
|
-
-
|
|
63
|
+
- Do NOT output `__DONE__` on your first action. Only use it when you've verified nothing remains.
|