@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
- [{: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.
@@ -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) "--allow-empty"]
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 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).
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
- (let [{:keys [status]} (execute-iteration! worker iter iterations)]
489
- (case status
490
- :done
491
- (do
492
- (println (format "[%s] Worker done after %d/%d iterations" id iter iterations))
493
- (assoc worker :completed iter :status :done))
494
-
495
- :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
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 worker" id errors))
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] Error at iteration %d/%d (%d/%d), continuing..."
503
- id iter iterations errors max-consecutive-errors))
504
- (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))
505
529
 
506
- :continue
507
- (recur (inc iter) (inc completed) 0)))))))
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
- - 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.0",
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",