@nbardy/oompa 0.1.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.
- package/README.md +197 -0
- package/agentnet/src/agentnet/agent.clj +293 -0
- package/agentnet/src/agentnet/cli.clj +436 -0
- package/agentnet/src/agentnet/core.clj +211 -0
- package/agentnet/src/agentnet/merge.clj +355 -0
- package/agentnet/src/agentnet/notes.clj +123 -0
- package/agentnet/src/agentnet/orchestrator.clj +367 -0
- package/agentnet/src/agentnet/review.clj +263 -0
- package/agentnet/src/agentnet/schema.clj +141 -0
- package/agentnet/src/agentnet/tasks.clj +198 -0
- package/agentnet/src/agentnet/worker.clj +427 -0
- package/agentnet/src/agentnet/worktree.clj +342 -0
- package/bin/oompa.js +37 -0
- package/config/prompts/cto.md +45 -0
- package/config/prompts/engineer.md +44 -0
- package/config/prompts/executor.md +32 -0
- package/config/prompts/planner.md +35 -0
- package/config/prompts/reviewer.md +49 -0
- package/config/prompts/worker.md +31 -0
- package/oompa.example.json +18 -0
- package/package.json +45 -0
- package/swarm.bb +18 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
(ns agentnet.orchestrator
|
|
2
|
+
"Main orchestration loop coordinating agents, worktrees, and merges.
|
|
3
|
+
|
|
4
|
+
The orchestrator manages the full lifecycle:
|
|
5
|
+
1. Initialize worktree pool
|
|
6
|
+
2. Load task queue
|
|
7
|
+
3. Dispatch tasks to workers
|
|
8
|
+
4. Run propose/review loops
|
|
9
|
+
5. Merge approved changes
|
|
10
|
+
6. Log results
|
|
11
|
+
|
|
12
|
+
Design:
|
|
13
|
+
- Workers are lightweight (just state, not threads)
|
|
14
|
+
- core.async for parallelism
|
|
15
|
+
- Each worker owns one worktree
|
|
16
|
+
- Tasks flow: queue -> worker -> review -> merge -> done
|
|
17
|
+
|
|
18
|
+
Concurrency Model:
|
|
19
|
+
- N workers, N worktrees
|
|
20
|
+
- Each worker processes one task at a time
|
|
21
|
+
- Workers can run in parallel (different worktrees)
|
|
22
|
+
- Merges are serialized (one at a time to main)"
|
|
23
|
+
(:require [agentnet.schema :as schema]
|
|
24
|
+
[agentnet.core :as core]
|
|
25
|
+
[agentnet.agent :as agent]
|
|
26
|
+
[agentnet.worktree :as worktree]
|
|
27
|
+
[agentnet.review :as review]
|
|
28
|
+
[agentnet.merge :as merge]
|
|
29
|
+
[clojure.core.async :as async]
|
|
30
|
+
[clojure.edn :as edn]
|
|
31
|
+
[clojure.java.io :as io]
|
|
32
|
+
[cheshire.core :as json]))
|
|
33
|
+
|
|
34
|
+
;; =============================================================================
|
|
35
|
+
;; Function Specs
|
|
36
|
+
;; =============================================================================
|
|
37
|
+
|
|
38
|
+
;; create-orchestrator : OrchestratorConfig -> OrchestratorState
|
|
39
|
+
;; Initialize orchestrator with workers and worktree pool
|
|
40
|
+
|
|
41
|
+
;; run! : OrchestratorState -> OrchestratorState
|
|
42
|
+
;; Process all tasks to completion
|
|
43
|
+
|
|
44
|
+
;; process-task! : Worker, Task, Context -> TaskResult
|
|
45
|
+
;; Full lifecycle for one task (propose, review, merge)
|
|
46
|
+
|
|
47
|
+
;; shutdown! : OrchestratorState -> nil
|
|
48
|
+
;; Clean up resources
|
|
49
|
+
|
|
50
|
+
;; =============================================================================
|
|
51
|
+
;; Constants
|
|
52
|
+
;; =============================================================================
|
|
53
|
+
|
|
54
|
+
(def ^:const TASKS_PATH "config/tasks.edn")
|
|
55
|
+
(def ^:const POLICY_PATH "config/policy.edn")
|
|
56
|
+
(def ^:const RUNS_DIR "runs")
|
|
57
|
+
|
|
58
|
+
;; =============================================================================
|
|
59
|
+
;; State Management
|
|
60
|
+
;; =============================================================================
|
|
61
|
+
|
|
62
|
+
(defn- now-ms []
|
|
63
|
+
(System/currentTimeMillis))
|
|
64
|
+
|
|
65
|
+
(defn- read-edn [path]
|
|
66
|
+
(let [f (io/file path)]
|
|
67
|
+
(when (.exists f)
|
|
68
|
+
(with-open [r (java.io.PushbackReader. (io/reader f))]
|
|
69
|
+
(edn/read {:eof nil} r)))))
|
|
70
|
+
|
|
71
|
+
(defn- load-tasks []
|
|
72
|
+
(or (read-edn TASKS_PATH) []))
|
|
73
|
+
|
|
74
|
+
(defn- load-policy []
|
|
75
|
+
(or (read-edn POLICY_PATH)
|
|
76
|
+
{:allow ["src/**" "tests/**"]
|
|
77
|
+
:deny ["secrets/**" "**/*.pem"]
|
|
78
|
+
:limits {:max-lines-added 800
|
|
79
|
+
:max-review-attempts 5}}))
|
|
80
|
+
|
|
81
|
+
;; =============================================================================
|
|
82
|
+
;; Worker Management
|
|
83
|
+
;; =============================================================================
|
|
84
|
+
|
|
85
|
+
(defn- create-worker [id worktree-instance]
|
|
86
|
+
{:id id
|
|
87
|
+
:status :idle
|
|
88
|
+
:worktree worktree-instance
|
|
89
|
+
:current-task nil
|
|
90
|
+
:review-loop nil})
|
|
91
|
+
|
|
92
|
+
(defn- assign-task [worker task]
|
|
93
|
+
(assoc worker
|
|
94
|
+
:status :proposing
|
|
95
|
+
:current-task task))
|
|
96
|
+
|
|
97
|
+
(defn- complete-task [worker result]
|
|
98
|
+
(assoc worker
|
|
99
|
+
:status :idle
|
|
100
|
+
:current-task nil
|
|
101
|
+
:review-loop nil
|
|
102
|
+
:last-result result))
|
|
103
|
+
|
|
104
|
+
;; =============================================================================
|
|
105
|
+
;; Task Processing
|
|
106
|
+
;; =============================================================================
|
|
107
|
+
|
|
108
|
+
(defn- build-context [tasks]
|
|
109
|
+
(core/build-context {:tasks tasks
|
|
110
|
+
:policy (load-policy)
|
|
111
|
+
:recent-sec 180}))
|
|
112
|
+
|
|
113
|
+
(defn process-task!
|
|
114
|
+
"Process a single task through full lifecycle.
|
|
115
|
+
|
|
116
|
+
Steps:
|
|
117
|
+
1. Run propose/review loop until approved
|
|
118
|
+
2. Merge approved changes to main
|
|
119
|
+
3. Clean up worktree
|
|
120
|
+
|
|
121
|
+
Returns TaskResult map."
|
|
122
|
+
[agent-config task context worktree opts]
|
|
123
|
+
(let [start-time (now-ms)
|
|
124
|
+
max-attempts (get-in opts [:limits :max-review-attempts] 5)
|
|
125
|
+
|
|
126
|
+
;; Run review loop
|
|
127
|
+
loop-result (review/review-task!
|
|
128
|
+
agent-config
|
|
129
|
+
task
|
|
130
|
+
context
|
|
131
|
+
worktree
|
|
132
|
+
{:max-attempts max-attempts})
|
|
133
|
+
|
|
134
|
+
_ (println (format "[%s] Review: %s (%d attempts)"
|
|
135
|
+
(:id task)
|
|
136
|
+
(name (:status loop-result))
|
|
137
|
+
(review/attempt-count loop-result)))
|
|
138
|
+
|
|
139
|
+
;; Merge if approved
|
|
140
|
+
merge-result (when (review/approved? loop-result)
|
|
141
|
+
(println (format "[%s] Merging..." (:id task)))
|
|
142
|
+
(merge/merge-worktree! worktree
|
|
143
|
+
{:strategy :no-ff
|
|
144
|
+
:dry-run (:dry-run opts)}))]
|
|
145
|
+
|
|
146
|
+
{:task-id (:id task)
|
|
147
|
+
:status (cond
|
|
148
|
+
(and merge-result (= :merged (:status merge-result))) :merged
|
|
149
|
+
(review/approved? loop-result) :merge-failed
|
|
150
|
+
(review/exhausted? loop-result) :review-exhausted
|
|
151
|
+
:else :failed)
|
|
152
|
+
:worker-id (:id worktree)
|
|
153
|
+
:started-at start-time
|
|
154
|
+
:completed-at (now-ms)
|
|
155
|
+
:review-attempts (review/attempt-count loop-result)
|
|
156
|
+
:merge-result merge-result
|
|
157
|
+
:error (or (:error loop-result)
|
|
158
|
+
(:error merge-result))}))
|
|
159
|
+
|
|
160
|
+
;; =============================================================================
|
|
161
|
+
;; Orchestrator Lifecycle
|
|
162
|
+
;; =============================================================================
|
|
163
|
+
|
|
164
|
+
(defn create-orchestrator
|
|
165
|
+
"Initialize orchestrator with config.
|
|
166
|
+
|
|
167
|
+
Arguments:
|
|
168
|
+
config - OrchestratorConfig
|
|
169
|
+
|
|
170
|
+
Returns OrchestratorState"
|
|
171
|
+
[{:keys [worker-count harness model worktree-root dry-run] :as config}]
|
|
172
|
+
(schema/assert-valid schema/valid-orchestrator-config? config "OrchestratorConfig")
|
|
173
|
+
|
|
174
|
+
(let [model-str (if model (format " (model: %s)" model) "")]
|
|
175
|
+
(println (format "Initializing %d workers with %s harness%s..."
|
|
176
|
+
worker-count (name harness) model-str)))
|
|
177
|
+
|
|
178
|
+
;; Initialize worktree pool
|
|
179
|
+
(let [pool (worktree/init-pool! config)
|
|
180
|
+
workers (mapv (fn [wt]
|
|
181
|
+
(create-worker (:id wt) wt))
|
|
182
|
+
pool)
|
|
183
|
+
tasks (load-tasks)]
|
|
184
|
+
|
|
185
|
+
(println (format "Loaded %d tasks, %d worktrees ready"
|
|
186
|
+
(count tasks) (count workers)))
|
|
187
|
+
|
|
188
|
+
{:config config
|
|
189
|
+
:workers workers
|
|
190
|
+
:worktree-pool pool
|
|
191
|
+
:task-queue (vec tasks)
|
|
192
|
+
:completed []
|
|
193
|
+
:failed []
|
|
194
|
+
:started-at (now-ms)}))
|
|
195
|
+
|
|
196
|
+
(defn run!
|
|
197
|
+
"Run orchestrator: process all tasks with parallel workers.
|
|
198
|
+
|
|
199
|
+
Uses core.async to parallelize across workers.
|
|
200
|
+
Returns updated OrchestratorState."
|
|
201
|
+
[state]
|
|
202
|
+
(let [{:keys [config workers worktree-pool task-queue]} state
|
|
203
|
+
{:keys [worker-count harness model review-harness review-model custom-prompt dry-run]} config
|
|
204
|
+
policy (load-policy)
|
|
205
|
+
|
|
206
|
+
;; Proposer config
|
|
207
|
+
agent-config {:type harness
|
|
208
|
+
:model model
|
|
209
|
+
:sandbox :workspace-write
|
|
210
|
+
:timeout-seconds 300}
|
|
211
|
+
|
|
212
|
+
;; Reviewer config (defaults to same as proposer)
|
|
213
|
+
reviewer-config {:type (or review-harness harness)
|
|
214
|
+
:model (or review-model model)
|
|
215
|
+
:sandbox :workspace-write
|
|
216
|
+
:timeout-seconds 300}
|
|
217
|
+
|
|
218
|
+
context (build-context task-queue)
|
|
219
|
+
|
|
220
|
+
;; Channels for task distribution
|
|
221
|
+
task-ch (async/chan)
|
|
222
|
+
result-ch (async/chan)
|
|
223
|
+
merge-ch (async/chan) ; Serialize merges
|
|
224
|
+
|
|
225
|
+
;; Worker processes
|
|
226
|
+
worker-procs
|
|
227
|
+
(doall
|
|
228
|
+
(for [worker workers]
|
|
229
|
+
(async/go-loop []
|
|
230
|
+
(when-let [task (async/<! task-ch)]
|
|
231
|
+
(let [wt (:worktree worker)
|
|
232
|
+
result (try
|
|
233
|
+
(process-task! agent-config task context wt
|
|
234
|
+
{:dry-run dry-run
|
|
235
|
+
:limits (:limits policy)})
|
|
236
|
+
(catch Exception e
|
|
237
|
+
{:task-id (:id task)
|
|
238
|
+
:status :error
|
|
239
|
+
:worker-id (:id worker)
|
|
240
|
+
:error (.getMessage e)}))]
|
|
241
|
+
(async/>! result-ch result)
|
|
242
|
+
;; Reset worktree for next task
|
|
243
|
+
(worktree/release! worktree-pool (:id wt) {:reset? true})
|
|
244
|
+
(recur))))))
|
|
245
|
+
|
|
246
|
+
;; Feed tasks to workers
|
|
247
|
+
_ (async/go
|
|
248
|
+
(doseq [task task-queue]
|
|
249
|
+
(async/>! task-ch task))
|
|
250
|
+
(async/close! task-ch))
|
|
251
|
+
|
|
252
|
+
;; Collect results
|
|
253
|
+
results (loop [remaining (count task-queue)
|
|
254
|
+
acc []]
|
|
255
|
+
(if (zero? remaining)
|
|
256
|
+
acc
|
|
257
|
+
(if-let [result (async/<!! result-ch)]
|
|
258
|
+
(do
|
|
259
|
+
(println (format "[%s] -> %s"
|
|
260
|
+
(:task-id result)
|
|
261
|
+
(name (:status result))))
|
|
262
|
+
(recur (dec remaining) (conj acc result)))
|
|
263
|
+
acc)))]
|
|
264
|
+
|
|
265
|
+
;; Close channels
|
|
266
|
+
(async/close! result-ch)
|
|
267
|
+
|
|
268
|
+
;; Update state
|
|
269
|
+
(assoc state
|
|
270
|
+
:completed (filterv #(= :merged (:status %)) results)
|
|
271
|
+
:failed (filterv #(not= :merged (:status %)) results)
|
|
272
|
+
:run-log results
|
|
273
|
+
:finished-at (now-ms))))
|
|
274
|
+
|
|
275
|
+
(defn shutdown!
|
|
276
|
+
"Clean up orchestrator resources"
|
|
277
|
+
[state]
|
|
278
|
+
(println "Shutting down orchestrator...")
|
|
279
|
+
(worktree/cleanup-pool! (:worktree-pool state))
|
|
280
|
+
nil)
|
|
281
|
+
|
|
282
|
+
;; =============================================================================
|
|
283
|
+
;; Logging
|
|
284
|
+
;; =============================================================================
|
|
285
|
+
|
|
286
|
+
(defn- ensure-dir! [path]
|
|
287
|
+
(.mkdirs (io/file path)))
|
|
288
|
+
|
|
289
|
+
(defn save-run-log!
|
|
290
|
+
"Save run results to JSONL file"
|
|
291
|
+
[state]
|
|
292
|
+
(ensure-dir! RUNS_DIR)
|
|
293
|
+
(let [timestamp (java.time.format.DateTimeFormatter/ofPattern "yyyyMMdd-HHmmss")
|
|
294
|
+
fname (format "run-%s.jsonl"
|
|
295
|
+
(.format timestamp (java.time.LocalDateTime/now)))
|
|
296
|
+
path (io/file RUNS_DIR fname)]
|
|
297
|
+
(doseq [entry (:run-log state)]
|
|
298
|
+
(spit path (str (json/generate-string entry) "\n") :append true))
|
|
299
|
+
(println (format "Run log: %s" (.getPath path)))
|
|
300
|
+
(.getPath path)))
|
|
301
|
+
|
|
302
|
+
(defn print-summary
|
|
303
|
+
"Print run summary to stdout"
|
|
304
|
+
[state]
|
|
305
|
+
(let [{:keys [completed failed started-at finished-at]} state
|
|
306
|
+
duration-sec (/ (- (or finished-at (now-ms)) started-at) 1000.0)]
|
|
307
|
+
(println)
|
|
308
|
+
(println "=== Run Summary ===")
|
|
309
|
+
(println (format "Duration: %.1fs" duration-sec))
|
|
310
|
+
(println (format "Completed: %d" (count completed)))
|
|
311
|
+
(println (format "Failed: %d" (count failed)))
|
|
312
|
+
(when (seq failed)
|
|
313
|
+
(println "Failed tasks:")
|
|
314
|
+
(doseq [{:keys [task-id status error]} failed]
|
|
315
|
+
(println (format " - %s: %s%s"
|
|
316
|
+
task-id
|
|
317
|
+
(name status)
|
|
318
|
+
(if error (str " (" error ")") "")))))))
|
|
319
|
+
|
|
320
|
+
;; =============================================================================
|
|
321
|
+
;; Convenience API
|
|
322
|
+
;; =============================================================================
|
|
323
|
+
|
|
324
|
+
(defn run-once!
|
|
325
|
+
"Convenience: create orchestrator, run, save log, shutdown.
|
|
326
|
+
|
|
327
|
+
Arguments:
|
|
328
|
+
opts - {:workers N, :harness :codex|:claude, :model string,
|
|
329
|
+
:review-harness keyword, :review-model string,
|
|
330
|
+
:custom-prompt string, :dry-run bool}
|
|
331
|
+
|
|
332
|
+
Returns run log path"
|
|
333
|
+
[opts]
|
|
334
|
+
(let [config {:worker-count (or (:workers opts) 2)
|
|
335
|
+
:harness (or (:harness opts) :codex)
|
|
336
|
+
:model (:model opts)
|
|
337
|
+
:review-harness (:review-harness opts)
|
|
338
|
+
:review-model (:review-model opts)
|
|
339
|
+
:custom-prompt (:custom-prompt opts)
|
|
340
|
+
:worktree-root ".workers"
|
|
341
|
+
:dry-run (:dry-run opts false)
|
|
342
|
+
:policy (load-policy)}
|
|
343
|
+
state (-> (create-orchestrator config)
|
|
344
|
+
(run!))]
|
|
345
|
+
(print-summary state)
|
|
346
|
+
(let [log-path (save-run-log! state)]
|
|
347
|
+
(when-not (:keep-worktrees opts)
|
|
348
|
+
(shutdown! state))
|
|
349
|
+
log-path)))
|
|
350
|
+
|
|
351
|
+
(defn run-loop!
|
|
352
|
+
"Run orchestrator in a loop N times (like the bash loop).
|
|
353
|
+
|
|
354
|
+
Arguments:
|
|
355
|
+
iterations - Number of times to run
|
|
356
|
+
opts - Same as run-once!
|
|
357
|
+
|
|
358
|
+
Returns vector of log paths"
|
|
359
|
+
[iterations opts]
|
|
360
|
+
(loop [i 0
|
|
361
|
+
logs []]
|
|
362
|
+
(if (< i iterations)
|
|
363
|
+
(do
|
|
364
|
+
(println (format "\n=== Iteration %d/%d ===" (inc i) iterations))
|
|
365
|
+
(let [log (run-once! opts)]
|
|
366
|
+
(recur (inc i) (conj logs log))))
|
|
367
|
+
logs)))
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
(ns agentnet.review
|
|
2
|
+
"Propose-review-fix loop management.
|
|
3
|
+
|
|
4
|
+
The review loop orchestrates the back-and-forth between proposer and
|
|
5
|
+
reviewer agents until changes are approved or max attempts exhausted.
|
|
6
|
+
|
|
7
|
+
Flow:
|
|
8
|
+
1. Proposer makes changes
|
|
9
|
+
2. Reviewer evaluates changes
|
|
10
|
+
3. If approved -> exit loop, ready to merge
|
|
11
|
+
4. If needs-changes -> proposer fixes based on feedback
|
|
12
|
+
5. Repeat until approved or max attempts
|
|
13
|
+
|
|
14
|
+
Design:
|
|
15
|
+
- Loop state is a pure data structure (ReviewLoop)
|
|
16
|
+
- Each step returns new state (functional)
|
|
17
|
+
- Side effects (agent calls) isolated to step functions
|
|
18
|
+
- Configurable max attempts prevents infinite loops"
|
|
19
|
+
(:require [agentnet.schema :as schema]
|
|
20
|
+
[agentnet.agent :as agent]
|
|
21
|
+
[agentnet.worktree :as worktree]
|
|
22
|
+
[clojure.string :as str]))
|
|
23
|
+
|
|
24
|
+
;; =============================================================================
|
|
25
|
+
;; Function Specs
|
|
26
|
+
;; =============================================================================
|
|
27
|
+
|
|
28
|
+
;; create-loop : Task, WorktreeId, MaxAttempts -> ReviewLoop
|
|
29
|
+
;; Initialize a new review loop
|
|
30
|
+
|
|
31
|
+
;; step! : ReviewLoop, AgentConfig, Context, Worktree -> ReviewLoop
|
|
32
|
+
;; Execute one iteration of the loop (propose or review)
|
|
33
|
+
|
|
34
|
+
;; run-loop! : ReviewLoop, AgentConfig, Context, Worktree -> ReviewLoop
|
|
35
|
+
;; Run loop to completion (approved or exhausted)
|
|
36
|
+
|
|
37
|
+
;; approved? : ReviewLoop -> Boolean
|
|
38
|
+
;; Check if loop ended with approval
|
|
39
|
+
|
|
40
|
+
;; =============================================================================
|
|
41
|
+
;; Constants
|
|
42
|
+
;; =============================================================================
|
|
43
|
+
|
|
44
|
+
(def ^:const DEFAULT_MAX_ATTEMPTS 5)
|
|
45
|
+
|
|
46
|
+
;; =============================================================================
|
|
47
|
+
;; State Management
|
|
48
|
+
;; =============================================================================
|
|
49
|
+
|
|
50
|
+
(defn- now-ms []
|
|
51
|
+
(System/currentTimeMillis))
|
|
52
|
+
|
|
53
|
+
(defn create-loop
|
|
54
|
+
"Create a new review loop for a task"
|
|
55
|
+
[task-id worktree-id {:keys [max-attempts] :or {max-attempts DEFAULT_MAX_ATTEMPTS}}]
|
|
56
|
+
{:task-id task-id
|
|
57
|
+
:worktree-id worktree-id
|
|
58
|
+
:max-attempts max-attempts
|
|
59
|
+
:attempts []
|
|
60
|
+
:status :in-progress})
|
|
61
|
+
|
|
62
|
+
(defn approved?
|
|
63
|
+
"Check if loop completed with approval"
|
|
64
|
+
[loop-state]
|
|
65
|
+
(= :approved (:status loop-state)))
|
|
66
|
+
|
|
67
|
+
(defn exhausted?
|
|
68
|
+
"Check if loop exhausted max attempts"
|
|
69
|
+
[loop-state]
|
|
70
|
+
(= :exhausted (:status loop-state)))
|
|
71
|
+
|
|
72
|
+
(defn attempt-count
|
|
73
|
+
"Get number of attempts so far"
|
|
74
|
+
[loop-state]
|
|
75
|
+
(count (:attempts loop-state)))
|
|
76
|
+
|
|
77
|
+
(defn last-feedback
|
|
78
|
+
"Get feedback from most recent attempt"
|
|
79
|
+
[loop-state]
|
|
80
|
+
(-> loop-state :attempts last :feedback))
|
|
81
|
+
|
|
82
|
+
(defn can-continue?
|
|
83
|
+
"Check if loop can continue (not done, not exhausted)"
|
|
84
|
+
[loop-state]
|
|
85
|
+
(and (= :in-progress (:status loop-state))
|
|
86
|
+
(< (attempt-count loop-state) (:max-attempts loop-state))))
|
|
87
|
+
|
|
88
|
+
;; =============================================================================
|
|
89
|
+
;; Feedback Formatting
|
|
90
|
+
;; =============================================================================
|
|
91
|
+
|
|
92
|
+
(defn- format-feedback-for-proposer
|
|
93
|
+
"Format review feedback as instructions for proposer to fix"
|
|
94
|
+
[feedback]
|
|
95
|
+
(let [{:keys [verdict comments violations suggested-fixes]} feedback
|
|
96
|
+
sections (cond-> []
|
|
97
|
+
(seq violations)
|
|
98
|
+
(conj (str "Violations:\n"
|
|
99
|
+
(str/join "\n" (map #(str "- " %) violations))))
|
|
100
|
+
|
|
101
|
+
(seq comments)
|
|
102
|
+
(conj (str "Feedback:\n"
|
|
103
|
+
(str/join "\n" (map #(str "- " %) comments))))
|
|
104
|
+
|
|
105
|
+
(seq suggested-fixes)
|
|
106
|
+
(conj (str "Suggested fixes:\n"
|
|
107
|
+
(str/join "\n" (map #(str "- " %) suggested-fixes)))))]
|
|
108
|
+
(if (seq sections)
|
|
109
|
+
(str "Previous review feedback:\n\n"
|
|
110
|
+
(str/join "\n\n" sections)
|
|
111
|
+
"\n\nPlease address the above issues.")
|
|
112
|
+
"Please review and improve the implementation.")))
|
|
113
|
+
|
|
114
|
+
;; =============================================================================
|
|
115
|
+
;; Step Execution
|
|
116
|
+
;; =============================================================================
|
|
117
|
+
|
|
118
|
+
(defn- run-proposer!
|
|
119
|
+
"Execute proposer agent, commit changes"
|
|
120
|
+
[agent-config task context worktree feedback]
|
|
121
|
+
(let [;; Augment context with feedback if this is a retry
|
|
122
|
+
augmented-context (if feedback
|
|
123
|
+
(assoc context :review_feedback
|
|
124
|
+
(format-feedback-for-proposer feedback))
|
|
125
|
+
context)
|
|
126
|
+
result (agent/propose! agent-config task augmented-context worktree)]
|
|
127
|
+
(when (zero? (:exit result))
|
|
128
|
+
;; Commit changes in worktree
|
|
129
|
+
(let [msg (if feedback
|
|
130
|
+
(format "fix: address review feedback for %s" (:id task))
|
|
131
|
+
(format "feat: implement %s" (:id task)))]
|
|
132
|
+
(worktree/commit-in-worktree! worktree msg)))
|
|
133
|
+
result))
|
|
134
|
+
|
|
135
|
+
(defn- run-reviewer!
|
|
136
|
+
"Execute reviewer agent on current worktree state"
|
|
137
|
+
[agent-config task context worktree]
|
|
138
|
+
(agent/review! agent-config task context worktree))
|
|
139
|
+
|
|
140
|
+
(defn- create-attempt
|
|
141
|
+
"Create an attempt record from review result"
|
|
142
|
+
[attempt-number review-result worktree]
|
|
143
|
+
{:attempt-number attempt-number
|
|
144
|
+
:timestamp (now-ms)
|
|
145
|
+
:feedback (or (:parsed review-result)
|
|
146
|
+
{:verdict :needs-changes
|
|
147
|
+
:comments ["Review failed to produce structured output"]})
|
|
148
|
+
:patch-hash nil}) ; TODO: compute hash of current diff
|
|
149
|
+
|
|
150
|
+
(defn step!
|
|
151
|
+
"Execute one iteration of propose->review.
|
|
152
|
+
|
|
153
|
+
Returns updated ReviewLoop with new attempt recorded."
|
|
154
|
+
[loop-state agent-config task context worktree]
|
|
155
|
+
(when-not (can-continue? loop-state)
|
|
156
|
+
(throw (ex-info "Cannot continue loop" {:loop loop-state})))
|
|
157
|
+
|
|
158
|
+
(let [attempt-num (inc (attempt-count loop-state))
|
|
159
|
+
prev-feedback (last-feedback loop-state)
|
|
160
|
+
|
|
161
|
+
;; Step 1: Proposer makes/fixes changes
|
|
162
|
+
propose-result (run-proposer! agent-config task context worktree prev-feedback)
|
|
163
|
+
|
|
164
|
+
_ (when-not (zero? (:exit propose-result))
|
|
165
|
+
(throw (ex-info "Proposer failed"
|
|
166
|
+
{:exit (:exit propose-result)
|
|
167
|
+
:stderr (:stderr propose-result)})))
|
|
168
|
+
|
|
169
|
+
;; Step 2: Reviewer evaluates
|
|
170
|
+
review-result (run-reviewer! agent-config task context worktree)
|
|
171
|
+
|
|
172
|
+
;; Record attempt
|
|
173
|
+
attempt (create-attempt attempt-num review-result worktree)
|
|
174
|
+
verdict (get-in attempt [:feedback :verdict])
|
|
175
|
+
|
|
176
|
+
;; Update loop state
|
|
177
|
+
new-attempts (conj (:attempts loop-state) attempt)
|
|
178
|
+
new-status (cond
|
|
179
|
+
(= verdict :approved) :approved
|
|
180
|
+
(>= attempt-num (:max-attempts loop-state)) :exhausted
|
|
181
|
+
:else :in-progress)]
|
|
182
|
+
|
|
183
|
+
(assoc loop-state
|
|
184
|
+
:attempts new-attempts
|
|
185
|
+
:status new-status)))
|
|
186
|
+
|
|
187
|
+
;; =============================================================================
|
|
188
|
+
;; Full Loop Execution
|
|
189
|
+
;; =============================================================================
|
|
190
|
+
|
|
191
|
+
(defn run-loop!
|
|
192
|
+
"Run the review loop to completion.
|
|
193
|
+
|
|
194
|
+
Continues until:
|
|
195
|
+
- Approved: reviewer accepts changes
|
|
196
|
+
- Exhausted: max attempts reached
|
|
197
|
+
- Aborted: unrecoverable error
|
|
198
|
+
|
|
199
|
+
Returns final ReviewLoop state."
|
|
200
|
+
[loop-state agent-config task context worktree]
|
|
201
|
+
(loop [state loop-state]
|
|
202
|
+
(if (can-continue? state)
|
|
203
|
+
(let [new-state (try
|
|
204
|
+
(step! state agent-config task context worktree)
|
|
205
|
+
(catch Exception e
|
|
206
|
+
(assoc state
|
|
207
|
+
:status :aborted
|
|
208
|
+
:error (.getMessage e))))]
|
|
209
|
+
(if (= :in-progress (:status new-state))
|
|
210
|
+
(recur new-state)
|
|
211
|
+
new-state))
|
|
212
|
+
state)))
|
|
213
|
+
|
|
214
|
+
;; =============================================================================
|
|
215
|
+
;; Convenience API
|
|
216
|
+
;; =============================================================================
|
|
217
|
+
|
|
218
|
+
(defn review-task!
|
|
219
|
+
"Complete review flow for a task: create loop, run to completion.
|
|
220
|
+
|
|
221
|
+
Arguments:
|
|
222
|
+
agent-config - AgentConfig for proposer/reviewer
|
|
223
|
+
task - Task to implement
|
|
224
|
+
context - Context map for prompts
|
|
225
|
+
worktree - Worktree to work in
|
|
226
|
+
opts - {:max-attempts N}
|
|
227
|
+
|
|
228
|
+
Returns final ReviewLoop state"
|
|
229
|
+
[agent-config task context worktree opts]
|
|
230
|
+
(let [loop-state (create-loop (:id task) (:id worktree) opts)]
|
|
231
|
+
(run-loop! loop-state agent-config task context worktree)))
|
|
232
|
+
|
|
233
|
+
(defn summarize-loop
|
|
234
|
+
"Generate human-readable summary of review loop"
|
|
235
|
+
[loop-state]
|
|
236
|
+
(let [{:keys [task-id status attempts max-attempts]} loop-state
|
|
237
|
+
attempt-summaries (map (fn [{:keys [attempt-number feedback]}]
|
|
238
|
+
(format " Attempt %d: %s"
|
|
239
|
+
attempt-number
|
|
240
|
+
(name (:verdict feedback :unknown))))
|
|
241
|
+
attempts)]
|
|
242
|
+
(str (format "Task: %s\n" task-id)
|
|
243
|
+
(format "Status: %s\n" (name status))
|
|
244
|
+
(format "Attempts: %d/%d\n" (count attempts) max-attempts)
|
|
245
|
+
(when (seq attempt-summaries)
|
|
246
|
+
(str "History:\n" (str/join "\n" attempt-summaries))))))
|
|
247
|
+
|
|
248
|
+
;; =============================================================================
|
|
249
|
+
;; Logging / Persistence
|
|
250
|
+
;; =============================================================================
|
|
251
|
+
|
|
252
|
+
(defn loop->log-entry
|
|
253
|
+
"Convert loop state to log entry format"
|
|
254
|
+
[loop-state]
|
|
255
|
+
{:task-id (:task-id loop-state)
|
|
256
|
+
:status (case (:status loop-state)
|
|
257
|
+
:approved :merged ; will become merged after actual merge
|
|
258
|
+
:exhausted :failed
|
|
259
|
+
:aborted :failed
|
|
260
|
+
:in-progress)
|
|
261
|
+
:review-attempts (attempt-count loop-state)
|
|
262
|
+
:final-verdict (get-in (last-feedback loop-state) [:verdict])
|
|
263
|
+
:error (:error loop-state)})
|