@nbardy/oompa 0.5.1 → 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.
|
|
@@ -314,6 +324,29 @@
|
|
|
314
324
|
(let [result (process/sh ["git" "status" "--porcelain"] {:dir wt-path :out :string :err :string})]
|
|
315
325
|
(not (str/blank? (:out result)))))
|
|
316
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
|
+
|
|
317
350
|
(defn- merge-to-main!
|
|
318
351
|
"Merge worktree changes to main branch"
|
|
319
352
|
[wt-path wt-id worker-id project-root]
|
|
@@ -364,88 +397,6 @@
|
|
|
364
397
|
(run-fix! worker wt-path output)
|
|
365
398
|
(recur (inc attempt)))))))))
|
|
366
399
|
|
|
367
|
-
(defn execute-iteration!
|
|
368
|
-
"Execute one iteration of work.
|
|
369
|
-
|
|
370
|
-
Flow:
|
|
371
|
-
1. Create worktree
|
|
372
|
-
2. Run agent
|
|
373
|
-
3. If reviewer configured: run review loop (fix → retry → reviewer)
|
|
374
|
-
4. If approved: merge to main
|
|
375
|
-
5. Cleanup worktree
|
|
376
|
-
|
|
377
|
-
Returns {:status :done|:continue|:error, :task task-or-nil}"
|
|
378
|
-
[worker iteration total-iterations]
|
|
379
|
-
(let [worker-id (:id worker)
|
|
380
|
-
project-root (System/getProperty "user.dir")
|
|
381
|
-
wt-dir (format ".w%s-i%d" worker-id iteration)
|
|
382
|
-
wt-branch (format "oompa/%s-i%d" worker-id iteration)
|
|
383
|
-
|
|
384
|
-
;; Create worktree
|
|
385
|
-
_ (println (format "[%s] Starting iteration %d/%d" worker-id iteration total-iterations))
|
|
386
|
-
wt-path (str project-root "/" wt-dir)]
|
|
387
|
-
|
|
388
|
-
(try
|
|
389
|
-
;; Clean stale worktree/branch from previous failed runs before creating
|
|
390
|
-
(process/sh ["git" "worktree" "remove" wt-dir "--force"] {:dir project-root})
|
|
391
|
-
(process/sh ["git" "branch" "-D" wt-branch] {:dir project-root})
|
|
392
|
-
|
|
393
|
-
;; Setup worktree (in project root)
|
|
394
|
-
(let [wt-result (process/sh ["git" "worktree" "add" wt-dir "-b" wt-branch]
|
|
395
|
-
{:dir project-root :out :string :err :string})]
|
|
396
|
-
(when-not (zero? (:exit wt-result))
|
|
397
|
-
(throw (ex-info (str "Failed to create worktree: " (:err wt-result))
|
|
398
|
-
{:dir wt-dir :branch wt-branch}))))
|
|
399
|
-
|
|
400
|
-
;; Build context
|
|
401
|
-
(let [context (build-context)
|
|
402
|
-
|
|
403
|
-
;; Run agent
|
|
404
|
-
{:keys [output exit done?]} (run-agent! worker wt-path context)]
|
|
405
|
-
|
|
406
|
-
(cond
|
|
407
|
-
;; Agent signaled done — only honor for can_plan workers.
|
|
408
|
-
;; Executors (can_plan: false) can't decide work is done.
|
|
409
|
-
(and done? (:can-plan worker))
|
|
410
|
-
(do
|
|
411
|
-
(println (format "[%s] Received __DONE__ signal" worker-id))
|
|
412
|
-
{:status :done})
|
|
413
|
-
|
|
414
|
-
;; can_plan: false worker said __DONE__ — ignore it, treat as success
|
|
415
|
-
(and done? (not (:can-plan worker)))
|
|
416
|
-
(do
|
|
417
|
-
(println (format "[%s] Ignoring __DONE__ (executor cannot signal done)" worker-id))
|
|
418
|
-
{:status :continue})
|
|
419
|
-
|
|
420
|
-
;; Agent errored
|
|
421
|
-
(not (zero? exit))
|
|
422
|
-
(do
|
|
423
|
-
(println (format "[%s] Agent error (exit %d): %s" worker-id exit (subs (or output "") 0 (min 200 (count (or output ""))))))
|
|
424
|
-
{:status :error :exit exit})
|
|
425
|
-
|
|
426
|
-
;; Agent produced no file changes — wasted iteration
|
|
427
|
-
(not (worktree-has-changes? wt-path))
|
|
428
|
-
(do
|
|
429
|
-
(println (format "[%s] No changes produced, skipping review" worker-id))
|
|
430
|
-
{:status :no-changes})
|
|
431
|
-
|
|
432
|
-
;; Success with changes - run review loop before merge
|
|
433
|
-
:else
|
|
434
|
-
(let [{:keys [approved?]} (review-loop! worker wt-path worker-id)]
|
|
435
|
-
(if approved?
|
|
436
|
-
(do
|
|
437
|
-
(merge-to-main! wt-path wt-branch worker-id project-root)
|
|
438
|
-
(println (format "[%s] Iteration %d/%d complete" worker-id iteration total-iterations))
|
|
439
|
-
{:status :continue})
|
|
440
|
-
(do
|
|
441
|
-
(println (format "[%s] Iteration %d/%d rejected, discarding" worker-id iteration total-iterations))
|
|
442
|
-
{:status :continue})))))
|
|
443
|
-
|
|
444
|
-
(finally
|
|
445
|
-
;; Cleanup worktree and branch (in project root)
|
|
446
|
-
(process/sh ["git" "worktree" "remove" wt-dir "--force"] {:dir project-root})
|
|
447
|
-
(process/sh ["git" "branch" "-D" wt-branch] {:dir project-root})))))
|
|
448
|
-
|
|
449
400
|
;; =============================================================================
|
|
450
401
|
;; Worker Loop
|
|
451
402
|
;; =============================================================================
|
|
@@ -472,12 +423,17 @@
|
|
|
472
423
|
(recur (+ waited wait-poll-interval))))))
|
|
473
424
|
|
|
474
425
|
(defn run-worker!
|
|
475
|
-
"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).
|
|
476
431
|
|
|
477
432
|
Returns final worker state."
|
|
478
433
|
[worker]
|
|
479
434
|
(tasks/ensure-dirs!)
|
|
480
|
-
(let [{:keys [id iterations]} worker
|
|
435
|
+
(let [{:keys [id iterations]} worker
|
|
436
|
+
project-root (System/getProperty "user.dir")]
|
|
481
437
|
(println (format "[%s] Starting worker (%s:%s%s, %d iterations)"
|
|
482
438
|
id
|
|
483
439
|
(name (:harness worker))
|
|
@@ -491,35 +447,97 @@
|
|
|
491
447
|
|
|
492
448
|
(loop [iter 1
|
|
493
449
|
completed 0
|
|
494
|
-
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
|
|
495
453
|
(if (> iter iterations)
|
|
496
454
|
(do
|
|
455
|
+
;; Cleanup any lingering worktree
|
|
456
|
+
(when wt-state
|
|
457
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state)))
|
|
497
458
|
(println (format "[%s] Completed %d iterations" id completed))
|
|
498
459
|
(assoc worker :completed completed :status :exhausted))
|
|
499
460
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
|
508
469
|
(let [errors (inc consec-errors)]
|
|
509
470
|
(if (>= errors max-consecutive-errors)
|
|
510
471
|
(do
|
|
511
|
-
(println (format "[%s] %d consecutive errors, stopping
|
|
472
|
+
(println (format "[%s] %d consecutive errors, stopping" id errors))
|
|
512
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))
|
|
513
524
|
(do
|
|
514
|
-
(println (format "[%s]
|
|
515
|
-
|
|
516
|
-
(
|
|
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))
|
|
517
529
|
|
|
518
|
-
|
|
519
|
-
|
|
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))
|
|
520
535
|
|
|
521
|
-
|
|
522
|
-
|
|
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))))))))))
|
|
523
541
|
|
|
524
542
|
;; =============================================================================
|
|
525
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.
|