@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
- [{:keys [id swarm-id harness model prompts reasoning]} worktree-path context]
170
- (let [;; 1. Task header (always, from package)
171
- task-header (or (load-prompt "config/prompts/_task_header.md") "")
172
-
173
- ;; 2. Load and concatenate all user prompts (string or list)
174
- user-prompts (if (seq prompts)
175
- (->> prompts
176
- (map load-prompt)
177
- (remove nil?)
178
- (str/join "\n\n"))
179
- (or (load-prompt "config/prompts/worker.md")
180
- "You are a worker. Claim tasks, execute them, complete them."))
181
-
182
- ;; Assemble: task header + status + user prompts
183
- full-prompt (str task-header "\n"
184
- "Task Status: " (:task_status context) "\n"
185
- "Pending: " (:pending_tasks context) "\n\n"
186
- user-prompts)
187
- session-id (str/lower-case (str (java.util.UUID/randomUUID)))
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 "] " full-prompt)
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 "--" full-prompt))
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 until done or iterations exhausted.
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
- (let [{:keys [status]} (execute-iteration! worker iter iterations)]
501
- (case status
502
- :done
503
- (do
504
- (println (format "[%s] Worker done after %d/%d iterations" id iter iterations))
505
- (assoc worker :completed iter :status :done))
506
-
507
- :error
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 worker" id errors))
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] Error at iteration %d/%d (%d/%d), continuing..."
515
- id iter iterations errors max-consecutive-errors))
516
- (recur (inc iter) completed errors))))
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
- :no-changes
519
- (recur (inc iter) completed consec-errors)
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
- :continue
522
- (recur (inc iter) (inc completed) 0)))))))
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
- - Only output __DONE__ if you have completed work AND no more tasks can be derived from the spec. Never __DONE__ on your first action.
63
+ - Do NOT output `__DONE__` on your first action. Only use it when you've verified nothing remains.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbardy/oompa",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Git-worktree multi-agent swarm orchestrator for Codex and Claude",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",