@nbardy/oompa 0.6.0 → 0.7.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 +26 -16
- package/agentnet/src/agentnet/agent.clj +19 -1
- package/agentnet/src/agentnet/cli.clj +165 -38
- package/agentnet/src/agentnet/orchestrator.clj +1 -1
- package/agentnet/src/agentnet/runs.clj +165 -0
- package/agentnet/src/agentnet/schema.clj +4 -4
- package/agentnet/src/agentnet/tasks.clj +1 -0
- package/agentnet/src/agentnet/worker.clj +517 -256
- package/oompa.example.json +8 -1
- package/package.json +3 -2
|
@@ -15,80 +15,12 @@
|
|
|
15
15
|
(:require [agentnet.tasks :as tasks]
|
|
16
16
|
[agentnet.agent :as agent]
|
|
17
17
|
[agentnet.worktree :as worktree]
|
|
18
|
+
[agentnet.runs :as runs]
|
|
19
|
+
[cheshire.core :as json]
|
|
18
20
|
[babashka.process :as process]
|
|
19
21
|
[clojure.java.io :as io]
|
|
20
22
|
[clojure.string :as str]))
|
|
21
23
|
|
|
22
|
-
;; =============================================================================
|
|
23
|
-
;; codex-persist integration
|
|
24
|
-
;; =============================================================================
|
|
25
|
-
|
|
26
|
-
(def ^:private persist-cmd* (atom nil))
|
|
27
|
-
(def ^:private persist-missing-warned?* (atom false))
|
|
28
|
-
|
|
29
|
-
(defn- command-ok?
|
|
30
|
-
"Return true if command vector is executable (exit code ignored)."
|
|
31
|
-
[cmd]
|
|
32
|
-
(try
|
|
33
|
-
(do
|
|
34
|
-
(process/sh (vec cmd) {:out :string :err :string :continue true})
|
|
35
|
-
true)
|
|
36
|
-
(catch Exception _
|
|
37
|
-
false)))
|
|
38
|
-
|
|
39
|
-
(defn- resolve-codex-persist-cmd
|
|
40
|
-
"Resolve codex-persist command vector.
|
|
41
|
-
Order:
|
|
42
|
-
1) CODEX_PERSIST_BIN env var
|
|
43
|
-
2) codex-persist on PATH
|
|
44
|
-
3) node ~/git/codex-persist/dist/cli.js"
|
|
45
|
-
[]
|
|
46
|
-
(let [cached @persist-cmd*]
|
|
47
|
-
(if (some? cached)
|
|
48
|
-
cached
|
|
49
|
-
(let [env-bin (System/getenv "CODEX_PERSIST_BIN")
|
|
50
|
-
env-cmd (when (and env-bin (not (str/blank? env-bin)))
|
|
51
|
-
[env-bin])
|
|
52
|
-
path-cmd ["codex-persist"]
|
|
53
|
-
local-cli (str (System/getProperty "user.home") "/git/codex-persist/dist/cli.js")
|
|
54
|
-
local-cmd (when (.exists (io/file local-cli))
|
|
55
|
-
["node" local-cli])
|
|
56
|
-
cmd (cond
|
|
57
|
-
(and env-cmd (command-ok? env-cmd)) env-cmd
|
|
58
|
-
(command-ok? path-cmd) path-cmd
|
|
59
|
-
(and local-cmd (command-ok? local-cmd)) local-cmd
|
|
60
|
-
:else false)]
|
|
61
|
-
(reset! persist-cmd* cmd)
|
|
62
|
-
cmd))))
|
|
63
|
-
|
|
64
|
-
(defn- safe-assistant-content
|
|
65
|
-
"Pick a non-empty assistant message payload for persistence."
|
|
66
|
-
[result]
|
|
67
|
-
(let [out (or (:out result) "")
|
|
68
|
-
err (or (:err result) "")
|
|
69
|
-
exit-code (or (:exit result) -1)]
|
|
70
|
-
(cond
|
|
71
|
-
(not (str/blank? out)) out
|
|
72
|
-
(not (str/blank? err)) (str "[agent stderr] " err)
|
|
73
|
-
:else (str "[agent exit " exit-code "]"))))
|
|
74
|
-
|
|
75
|
-
(defn- persist-message!
|
|
76
|
-
"Write a single message to codex-persist; no-op if unavailable."
|
|
77
|
-
[worker-id session-id cwd role content]
|
|
78
|
-
(let [resolved (resolve-codex-persist-cmd)]
|
|
79
|
-
(if (and resolved (not= resolved false))
|
|
80
|
-
(let [persist-cmd resolved
|
|
81
|
-
payload (if (str/blank? content) "(empty)" content)
|
|
82
|
-
result (try
|
|
83
|
-
(process/sh (into persist-cmd ["write" session-id cwd role payload])
|
|
84
|
-
{:out :string :err :string})
|
|
85
|
-
(catch Exception e
|
|
86
|
-
{:exit -1 :out "" :err (.getMessage e)}))]
|
|
87
|
-
(when-not (zero? (:exit result))
|
|
88
|
-
(println (format "[%s] codex-persist write failed (%s)" worker-id role))))
|
|
89
|
-
(when (compare-and-set! persist-missing-warned?* false true)
|
|
90
|
-
(println "[oompa] codex-persist not found; set CODEX_PERSIST_BIN or install/link codex-persist")))))
|
|
91
|
-
|
|
92
24
|
;; =============================================================================
|
|
93
25
|
;; Worker State
|
|
94
26
|
;; =============================================================================
|
|
@@ -97,6 +29,10 @@
|
|
|
97
29
|
"Root of the oompa package — set by bin/oompa.js, falls back to cwd."
|
|
98
30
|
(or (System/getenv "OOMPA_PACKAGE_ROOT") "."))
|
|
99
31
|
|
|
32
|
+
;; Serializes merge-to-main! calls across concurrent workers to prevent
|
|
33
|
+
;; git index corruption from parallel checkout+merge operations.
|
|
34
|
+
(def ^:private merge-lock (Object.))
|
|
35
|
+
|
|
100
36
|
;; Resolve absolute paths for CLI binaries at first use.
|
|
101
37
|
;; ProcessBuilder with :dir set can fail to find bare command names on some
|
|
102
38
|
;; platforms (macOS + babashka), so we resolve once via `which` and cache.
|
|
@@ -127,8 +63,10 @@
|
|
|
127
63
|
"Create a worker config.
|
|
128
64
|
:prompts is a string or vector of strings — paths to prompt files.
|
|
129
65
|
:can-plan when false, worker waits for tasks before starting (backpressure).
|
|
130
|
-
:reasoning reasoning effort level (e.g. \"low\", \"medium\", \"high\") — codex only.
|
|
131
|
-
|
|
66
|
+
:reasoning reasoning effort level (e.g. \"low\", \"medium\", \"high\") — codex only.
|
|
67
|
+
:review-prompts paths to reviewer prompt files (loaded and concatenated for review)."
|
|
68
|
+
[{:keys [id swarm-id harness model iterations prompts can-plan reasoning
|
|
69
|
+
review-harness review-model review-prompts]}]
|
|
132
70
|
{:id id
|
|
133
71
|
:swarm-id swarm-id
|
|
134
72
|
:harness (or harness :codex)
|
|
@@ -142,6 +80,10 @@
|
|
|
142
80
|
:reasoning reasoning
|
|
143
81
|
:review-harness review-harness
|
|
144
82
|
:review-model review-model
|
|
83
|
+
:review-prompts (cond
|
|
84
|
+
(vector? review-prompts) review-prompts
|
|
85
|
+
(string? review-prompts) [review-prompts]
|
|
86
|
+
:else [])
|
|
145
87
|
:completed 0
|
|
146
88
|
:status :idle})
|
|
147
89
|
|
|
@@ -164,13 +106,53 @@
|
|
|
164
106
|
:task_status (format "Pending: %d, In Progress: %d, Complete: %d"
|
|
165
107
|
(count pending) (count current) (count complete))}))
|
|
166
108
|
|
|
109
|
+
(defn- opencode-attach-url
|
|
110
|
+
"Optional opencode server URL for run --attach mode."
|
|
111
|
+
[]
|
|
112
|
+
(let [url (or (System/getenv "OOMPA_OPENCODE_ATTACH")
|
|
113
|
+
(System/getenv "OPENCODE_ATTACH"))]
|
|
114
|
+
(when (and url (not (str/blank? url)))
|
|
115
|
+
url)))
|
|
116
|
+
|
|
117
|
+
(defn- parse-opencode-run-output
|
|
118
|
+
"Parse `opencode run --format json` output.
|
|
119
|
+
Returns {:session-id string|nil, :text string|nil}."
|
|
120
|
+
[s]
|
|
121
|
+
(let [raw (or s "")
|
|
122
|
+
events (->> (str/split-lines raw)
|
|
123
|
+
(keep (fn [line]
|
|
124
|
+
(try
|
|
125
|
+
(json/parse-string line true)
|
|
126
|
+
(catch Exception _
|
|
127
|
+
nil))))
|
|
128
|
+
doall)
|
|
129
|
+
session-id (or (some #(or (:sessionID %)
|
|
130
|
+
(:sessionId %)
|
|
131
|
+
(get-in % [:part :sessionID])
|
|
132
|
+
(get-in % [:part :sessionId]))
|
|
133
|
+
events)
|
|
134
|
+
(some-> (re-find #"(ses_[A-Za-z0-9]+)" raw) second))
|
|
135
|
+
text (->> events
|
|
136
|
+
(keep (fn [event]
|
|
137
|
+
(let [event-type (or (:type event) (get-in event [:part :type]))
|
|
138
|
+
chunk (or (:text event) (get-in event [:part :text]))]
|
|
139
|
+
(when (and (= event-type "text")
|
|
140
|
+
(string? chunk)
|
|
141
|
+
(not (str/blank? chunk)))
|
|
142
|
+
chunk))))
|
|
143
|
+
(str/join ""))]
|
|
144
|
+
{:session-id session-id
|
|
145
|
+
:text (when-not (str/blank? text) text)}))
|
|
146
|
+
|
|
167
147
|
(defn- run-agent!
|
|
168
148
|
"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,
|
|
149
|
+
When resume? is true and harness is :claude/:opencode, continues the existing session
|
|
170
150
|
with a lighter prompt (just task status + continue instruction)."
|
|
171
151
|
[{:keys [id swarm-id harness model prompts reasoning]} worktree-path context session-id resume?]
|
|
172
|
-
(let [;; Use provided session-id
|
|
173
|
-
session-id (or session-id
|
|
152
|
+
(let [;; Use provided session-id, otherwise generate one for harnesses that accept custom IDs.
|
|
153
|
+
session-id (or session-id
|
|
154
|
+
(when (#{:codex :claude} harness)
|
|
155
|
+
(str/lower-case (str (java.util.UUID/randomUUID)))))
|
|
174
156
|
|
|
175
157
|
;; Build prompt — lighter for resume (agent already has full context)
|
|
176
158
|
prompt (if resume?
|
|
@@ -193,11 +175,14 @@
|
|
|
193
175
|
swarm-id* (or swarm-id "unknown")
|
|
194
176
|
tagged-prompt (str "[oompa:" swarm-id* ":" id "] " prompt)
|
|
195
177
|
abs-worktree (.getAbsolutePath (io/file worktree-path))
|
|
178
|
+
opencode-attach (opencode-attach-url)
|
|
196
179
|
|
|
197
|
-
;; Build command —
|
|
180
|
+
;; Build command — all harnesses run with cwd=worktree, no sandbox
|
|
198
181
|
;; so agents can `..` to reach project root for task management
|
|
199
182
|
;; Claude: --resume flag continues existing session-id conversation
|
|
200
|
-
;;
|
|
183
|
+
;; Opencode: -s/--session + --continue continue existing session
|
|
184
|
+
;; and --format json for deterministic per-run session capture.
|
|
185
|
+
;; Codex: no native resume support, always fresh (but worktree state persists)
|
|
201
186
|
cmd (case harness
|
|
202
187
|
:codex (cond-> [(resolve-binary! "codex") "exec"
|
|
203
188
|
"--dangerously-bypass-approvals-and-sandbox"
|
|
@@ -209,53 +194,82 @@
|
|
|
209
194
|
:claude (cond-> [(resolve-binary! "claude") "-p" "--dangerously-skip-permissions"
|
|
210
195
|
"--session-id" session-id]
|
|
211
196
|
resume? (conj "--resume")
|
|
212
|
-
model (into ["--model" model]))
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
197
|
+
model (into ["--model" model]))
|
|
198
|
+
:opencode (cond-> [(resolve-binary! "opencode") "run" "--format" "json"]
|
|
199
|
+
model (into ["-m" model])
|
|
200
|
+
opencode-attach (into ["--attach" opencode-attach])
|
|
201
|
+
(and resume? session-id) (into ["-s" session-id "--continue"])
|
|
202
|
+
true (conj tagged-prompt)))
|
|
203
|
+
|
|
204
|
+
;; Run agent — all run with cwd=worktree
|
|
218
205
|
result (try
|
|
219
206
|
(if (= harness :claude)
|
|
220
207
|
(process/sh cmd {:dir abs-worktree :in tagged-prompt :out :string :err :string})
|
|
221
208
|
(process/sh cmd {:dir abs-worktree :out :string :err :string}))
|
|
222
209
|
(catch Exception e
|
|
223
210
|
(println (format "[%s] Agent exception: %s" id (.getMessage e)))
|
|
224
|
-
{:exit -1 :out "" :err (.getMessage e)}))
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
211
|
+
{:exit -1 :out "" :err (.getMessage e)}))
|
|
212
|
+
parsed-opencode (when (= harness :opencode)
|
|
213
|
+
(parse-opencode-run-output (:out result)))
|
|
214
|
+
output (if (= harness :opencode)
|
|
215
|
+
(or (:text parsed-opencode) (:out result))
|
|
216
|
+
(:out result))
|
|
217
|
+
session-id' (if (= harness :opencode)
|
|
218
|
+
(or (:session-id parsed-opencode) session-id)
|
|
219
|
+
session-id)]
|
|
220
|
+
|
|
221
|
+
{:output output
|
|
230
222
|
:exit (:exit result)
|
|
231
|
-
:done? (agent/done-signal?
|
|
232
|
-
:merge? (agent/merge-signal?
|
|
233
|
-
:session-id session-id}))
|
|
223
|
+
:done? (agent/done-signal? output)
|
|
224
|
+
:merge? (agent/merge-signal? output)
|
|
225
|
+
:session-id session-id'}))
|
|
234
226
|
|
|
235
227
|
(defn- run-reviewer!
|
|
236
228
|
"Run reviewer on worktree changes.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
229
|
+
Uses custom review-prompts when configured, otherwise falls back to default.
|
|
230
|
+
prev-feedback: vector of previous review outputs (for multi-round context).
|
|
231
|
+
Returns {:verdict :approved|:needs-changes|:rejected, :comments [...], :output string}"
|
|
232
|
+
[{:keys [id swarm-id review-harness review-model review-prompts]} worktree-path prev-feedback]
|
|
233
|
+
(let [;; Get actual diff content (not just stat) — truncate to 8000 chars for prompt budget
|
|
234
|
+
diff-result (process/sh ["git" "diff" "main"]
|
|
241
235
|
{:dir worktree-path :out :string :err :string})
|
|
242
|
-
diff-
|
|
236
|
+
diff-content (let [d (:out diff-result)]
|
|
237
|
+
(if (> (count d) 8000)
|
|
238
|
+
(str (subs d 0 8000) "\n... [diff truncated at 8000 chars]")
|
|
239
|
+
d))
|
|
243
240
|
|
|
244
|
-
;; Build review prompt
|
|
241
|
+
;; Build review prompt — use custom prompts if configured, else default
|
|
245
242
|
swarm-id* (or swarm-id "unknown")
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
243
|
+
custom-prompt (when (seq review-prompts)
|
|
244
|
+
(->> review-prompts
|
|
245
|
+
(map load-prompt)
|
|
246
|
+
(remove nil?)
|
|
247
|
+
(str/join "\n\n")))
|
|
248
|
+
|
|
249
|
+
;; Include previous review history for multi-round context
|
|
250
|
+
history-block (when (seq prev-feedback)
|
|
251
|
+
(str "\n## Previous Review Rounds\n\n"
|
|
252
|
+
"The worker has already attempted fixes based on earlier feedback. "
|
|
253
|
+
"Do NOT raise new issues — only verify the original issues are resolved.\n\n"
|
|
254
|
+
(->> prev-feedback
|
|
255
|
+
(map-indexed (fn [i fb]
|
|
256
|
+
(str "### Round " (inc i) " feedback:\n" fb)))
|
|
257
|
+
(str/join "\n\n"))
|
|
258
|
+
"\n\n"))
|
|
259
|
+
|
|
260
|
+
review-body (str (or custom-prompt
|
|
261
|
+
(str "Review the changes in this worktree.\n"
|
|
262
|
+
"Focus on architecture and design, not style.\n"))
|
|
263
|
+
"\n\nDiff:\n```\n" diff-content "\n```\n"
|
|
264
|
+
(when history-block history-block)
|
|
265
|
+
"\nYour verdict MUST be on its own line, exactly one of:\n"
|
|
266
|
+
"VERDICT: APPROVED\n"
|
|
267
|
+
"VERDICT: NEEDS_CHANGES\n"
|
|
268
|
+
"VERDICT: REJECTED\n")
|
|
269
|
+
review-prompt (str "[oompa:" swarm-id* ":" id "] " review-body)
|
|
257
270
|
|
|
258
271
|
abs-wt (.getAbsolutePath (io/file worktree-path))
|
|
272
|
+
opencode-attach (opencode-attach-url)
|
|
259
273
|
|
|
260
274
|
;; Build command — cwd=worktree, no sandbox
|
|
261
275
|
cmd (case review-harness
|
|
@@ -266,7 +280,11 @@
|
|
|
266
280
|
review-model (into ["--model" review-model])
|
|
267
281
|
true (conj "--" review-prompt))
|
|
268
282
|
:claude (cond-> [(resolve-binary! "claude") "-p" "--dangerously-skip-permissions"]
|
|
269
|
-
review-model (into ["--model" review-model]))
|
|
283
|
+
review-model (into ["--model" review-model]))
|
|
284
|
+
:opencode (cond-> [(resolve-binary! "opencode") "run"]
|
|
285
|
+
review-model (into ["-m" review-model])
|
|
286
|
+
opencode-attach (into ["--attach" opencode-attach])
|
|
287
|
+
true (conj review-prompt)))
|
|
270
288
|
|
|
271
289
|
;; Run reviewer — cwd=worktree
|
|
272
290
|
result (try
|
|
@@ -276,27 +294,50 @@
|
|
|
276
294
|
(catch Exception e
|
|
277
295
|
{:exit -1 :out "" :err (.getMessage e)}))
|
|
278
296
|
|
|
279
|
-
output (:out result)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
297
|
+
output (:out result)
|
|
298
|
+
|
|
299
|
+
;; Parse verdict — require explicit VERDICT: prefix to avoid false matches
|
|
300
|
+
verdict (cond
|
|
301
|
+
(re-find #"VERDICT:\s*APPROVED" output) :approved
|
|
302
|
+
(re-find #"VERDICT:\s*REJECTED" output) :rejected
|
|
303
|
+
(re-find #"VERDICT:\s*NEEDS_CHANGES" output) :needs-changes
|
|
304
|
+
;; Fallback to loose matching if reviewer didn't use prefix
|
|
305
|
+
(re-find #"(?i)\bAPPROVED\b" output) :approved
|
|
306
|
+
(re-find #"(?i)\bREJECTED\b" output) :rejected
|
|
307
|
+
:else :needs-changes)]
|
|
308
|
+
|
|
309
|
+
;; Log reviewer output (truncated) for visibility
|
|
310
|
+
(println (format "[%s] Reviewer verdict: %s" id (name verdict)))
|
|
311
|
+
(let [summary (subs output 0 (min 300 (count output)))]
|
|
312
|
+
(println (format "[%s] Review: %s%s" id summary
|
|
313
|
+
(if (> (count output) 300) "..." ""))))
|
|
314
|
+
|
|
315
|
+
{:verdict verdict
|
|
285
316
|
:comments (when (not= (:exit result) 0)
|
|
286
317
|
[(:err result)])
|
|
287
318
|
:output output}))
|
|
288
319
|
|
|
289
320
|
(defn- run-fix!
|
|
290
321
|
"Ask worker to fix issues based on reviewer feedback.
|
|
322
|
+
all-feedback: vector of all reviewer outputs so far (accumulated across rounds).
|
|
291
323
|
Returns {:output string, :exit int}"
|
|
292
|
-
[{:keys [id swarm-id harness model]} worktree-path feedback]
|
|
324
|
+
[{:keys [id swarm-id harness model]} worktree-path all-feedback]
|
|
293
325
|
(let [swarm-id* (or swarm-id "unknown")
|
|
326
|
+
feedback-text (if (> (count all-feedback) 1)
|
|
327
|
+
(str "The reviewer has given feedback across " (count all-feedback) " rounds.\n"
|
|
328
|
+
"Fix ALL outstanding issues:\n\n"
|
|
329
|
+
(->> all-feedback
|
|
330
|
+
(map-indexed (fn [i fb]
|
|
331
|
+
(str "--- Round " (inc i) " ---\n" fb)))
|
|
332
|
+
(str/join "\n\n")))
|
|
333
|
+
(str "The reviewer found issues with your changes:\n\n"
|
|
334
|
+
(first all-feedback)))
|
|
294
335
|
fix-prompt (str "[oompa:" swarm-id* ":" id "] "
|
|
295
|
-
"
|
|
296
|
-
|
|
297
|
-
"Please fix these issues in the worktree.")
|
|
336
|
+
feedback-text "\n\n"
|
|
337
|
+
"Fix these issues. Do not add anything the reviewer did not ask for.")
|
|
298
338
|
|
|
299
339
|
abs-wt (.getAbsolutePath (io/file worktree-path))
|
|
340
|
+
opencode-attach (opencode-attach-url)
|
|
300
341
|
|
|
301
342
|
cmd (case harness
|
|
302
343
|
:codex (cond-> [(resolve-binary! "codex") "exec"
|
|
@@ -306,7 +347,11 @@
|
|
|
306
347
|
model (into ["--model" model])
|
|
307
348
|
true (conj "--" fix-prompt))
|
|
308
349
|
:claude (cond-> [(resolve-binary! "claude") "-p" "--dangerously-skip-permissions"]
|
|
309
|
-
model (into ["--model" model]))
|
|
350
|
+
model (into ["--model" model]))
|
|
351
|
+
:opencode (cond-> [(resolve-binary! "opencode") "run"]
|
|
352
|
+
model (into ["-m" model])
|
|
353
|
+
opencode-attach (into ["--attach" opencode-attach])
|
|
354
|
+
true (conj fix-prompt)))
|
|
310
355
|
|
|
311
356
|
result (try
|
|
312
357
|
(if (= harness :claude)
|
|
@@ -319,10 +364,19 @@
|
|
|
319
364
|
:exit (:exit result)}))
|
|
320
365
|
|
|
321
366
|
(defn- worktree-has-changes?
|
|
322
|
-
"Check if worktree has
|
|
367
|
+
"Check if worktree has committed OR uncommitted changes vs main.
|
|
368
|
+
Workers commit before signaling merge, so we must check both:
|
|
369
|
+
1. Uncommitted changes (git status --porcelain)
|
|
370
|
+
2. Commits ahead of main (git rev-list --count main..HEAD)"
|
|
323
371
|
[wt-path]
|
|
324
|
-
(let [
|
|
325
|
-
|
|
372
|
+
(let [uncommitted (process/sh ["git" "status" "--porcelain"]
|
|
373
|
+
{:dir wt-path :out :string :err :string})
|
|
374
|
+
ahead (process/sh ["git" "rev-list" "--count" "main..HEAD"]
|
|
375
|
+
{:dir wt-path :out :string :err :string})
|
|
376
|
+
ahead-count (try (Integer/parseInt (str/trim (:out ahead)))
|
|
377
|
+
(catch Exception _ 0))]
|
|
378
|
+
(or (not (str/blank? (:out uncommitted)))
|
|
379
|
+
(pos? ahead-count))))
|
|
326
380
|
|
|
327
381
|
(defn- create-iteration-worktree!
|
|
328
382
|
"Create a fresh worktree for an iteration. Returns {:dir :branch :path}.
|
|
@@ -347,55 +401,136 @@
|
|
|
347
401
|
(process/sh ["git" "worktree" "remove" wt-dir "--force"] {:dir project-root})
|
|
348
402
|
(process/sh ["git" "branch" "-D" wt-branch] {:dir project-root}))
|
|
349
403
|
|
|
404
|
+
(defn- get-head-hash
|
|
405
|
+
"Get the short HEAD commit hash."
|
|
406
|
+
[dir]
|
|
407
|
+
(let [result (process/sh ["git" "rev-parse" "--short" "HEAD"]
|
|
408
|
+
{:dir dir :out :string :err :string})]
|
|
409
|
+
(when (zero? (:exit result))
|
|
410
|
+
(str/trim (:out result)))))
|
|
411
|
+
|
|
412
|
+
(defn- annotate-completed-tasks!
|
|
413
|
+
"After a successful merge (called under merge-lock), annotate any tasks in
|
|
414
|
+
complete/ that lack metadata. Adds :completed-by, :completed-at,
|
|
415
|
+
:review-rounds, :merged-commit."
|
|
416
|
+
[project-root worker-id review-rounds]
|
|
417
|
+
(let [commit-hash (get-head-hash project-root)
|
|
418
|
+
complete-dir (io/file project-root "tasks" "complete")]
|
|
419
|
+
(when (.exists complete-dir)
|
|
420
|
+
(doseq [f (.listFiles complete-dir)]
|
|
421
|
+
(when (str/ends-with? (.getName f) ".edn")
|
|
422
|
+
(try
|
|
423
|
+
(let [task (read-string (slurp f))]
|
|
424
|
+
(when-not (:completed-by task)
|
|
425
|
+
(spit f (pr-str (assoc task
|
|
426
|
+
:completed-by worker-id
|
|
427
|
+
:completed-at (str (java.time.Instant/now))
|
|
428
|
+
:review-rounds (or review-rounds 0)
|
|
429
|
+
:merged-commit (or commit-hash "unknown"))))))
|
|
430
|
+
(catch Exception e
|
|
431
|
+
(println (format "[%s] Failed to annotate task %s: %s"
|
|
432
|
+
worker-id (.getName f) (.getMessage e))))))))))
|
|
433
|
+
|
|
350
434
|
(defn- merge-to-main!
|
|
351
|
-
"Merge worktree changes to main branch
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
435
|
+
"Merge worktree changes to main branch. Serialized via merge-lock to prevent
|
|
436
|
+
concurrent workers from corrupting the git index. On success, annotates any
|
|
437
|
+
newly-completed tasks with worker metadata. Returns true on success.
|
|
438
|
+
review-rounds: number of review rounds (0 for auto-merged task-only changes)."
|
|
439
|
+
[wt-path wt-id worker-id project-root review-rounds]
|
|
440
|
+
(locking merge-lock
|
|
441
|
+
(println (format "[%s] Merging changes to main" worker-id))
|
|
442
|
+
(let [;; Commit in worktree if needed (no-op if already committed)
|
|
443
|
+
_ (process/sh ["git" "add" "-A"] {:dir wt-path})
|
|
444
|
+
_ (process/sh ["git" "commit" "-m" (str "Work from " wt-id)]
|
|
445
|
+
{:dir wt-path})
|
|
446
|
+
;; Checkout main and merge (in project root, not worktree)
|
|
447
|
+
checkout-result (process/sh ["git" "checkout" "main"]
|
|
448
|
+
{:dir project-root :out :string :err :string})
|
|
449
|
+
_ (when-not (zero? (:exit checkout-result))
|
|
450
|
+
(println (format "[%s] MERGE FAILED: could not checkout main: %s"
|
|
451
|
+
worker-id (:err checkout-result))))
|
|
452
|
+
merge-result (when (zero? (:exit checkout-result))
|
|
453
|
+
(process/sh ["git" "merge" wt-id "--no-edit"]
|
|
454
|
+
{:dir project-root :out :string :err :string}))
|
|
455
|
+
success (and (zero? (:exit checkout-result))
|
|
456
|
+
(zero? (:exit merge-result)))]
|
|
457
|
+
(if success
|
|
458
|
+
(do
|
|
459
|
+
(println (format "[%s] Merge successful" worker-id))
|
|
460
|
+
;; Annotate completed tasks while still holding merge-lock
|
|
461
|
+
(annotate-completed-tasks! project-root worker-id review-rounds))
|
|
462
|
+
(when merge-result
|
|
463
|
+
(println (format "[%s] MERGE FAILED: %s" worker-id (:err merge-result)))))
|
|
464
|
+
success)))
|
|
465
|
+
|
|
466
|
+
(defn- task-only-diff?
|
|
467
|
+
"Check if all changes in worktree are task files only (no code changes).
|
|
468
|
+
Returns true if diff only touches files under tasks/ directory."
|
|
469
|
+
[wt-path]
|
|
470
|
+
(let [result (process/sh ["git" "diff" "main" "--name-only"]
|
|
471
|
+
{:dir wt-path :out :string :err :string})
|
|
472
|
+
files (when (zero? (:exit result))
|
|
473
|
+
(->> (str/split-lines (:out result))
|
|
474
|
+
(remove str/blank?)))]
|
|
475
|
+
(and (seq files)
|
|
476
|
+
(every? #(str/starts-with? % "tasks/") files))))
|
|
477
|
+
|
|
478
|
+
(defn- diff-file-names
|
|
479
|
+
"Get list of changed file names vs main."
|
|
480
|
+
[wt-path]
|
|
481
|
+
(let [result (process/sh ["git" "diff" "main" "--name-only"]
|
|
482
|
+
{:dir wt-path :out :string :err :string})]
|
|
483
|
+
(when (zero? (:exit result))
|
|
484
|
+
(->> (str/split-lines (:out result))
|
|
485
|
+
(remove str/blank?)
|
|
486
|
+
vec))))
|
|
366
487
|
|
|
367
488
|
(defn- review-loop!
|
|
368
489
|
"Run review loop: reviewer checks → if issues, fix & retry → back to reviewer.
|
|
490
|
+
Accumulates feedback across rounds so reviewer doesn't raise new issues
|
|
491
|
+
and fixer has full context of all prior feedback.
|
|
492
|
+
Writes review logs to runs/{swarm-id}/reviews/ for post-mortem analysis.
|
|
369
493
|
Returns {:approved? bool, :attempts int}"
|
|
370
|
-
[worker wt-path worker-id]
|
|
494
|
+
[worker wt-path worker-id iteration]
|
|
371
495
|
(if-not (and (:review-harness worker) (:review-model worker))
|
|
372
496
|
;; No reviewer configured, auto-approve
|
|
373
497
|
{:approved? true :attempts 0}
|
|
374
498
|
|
|
375
|
-
;; Run review loop
|
|
376
|
-
(loop [attempt 1
|
|
499
|
+
;; Run review loop with accumulated feedback
|
|
500
|
+
(loop [attempt 1
|
|
501
|
+
prev-feedback []]
|
|
377
502
|
(println (format "[%s] Review attempt %d/%d" worker-id attempt max-review-retries))
|
|
378
|
-
(let [{:keys [verdict output]} (run-reviewer! worker wt-path)
|
|
503
|
+
(let [{:keys [verdict output]} (run-reviewer! worker wt-path prev-feedback)
|
|
504
|
+
diff-files (diff-file-names wt-path)]
|
|
505
|
+
|
|
506
|
+
;; Persist review log for this round
|
|
507
|
+
(when (:swarm-id worker)
|
|
508
|
+
(runs/write-review-log! (:swarm-id worker) worker-id iteration attempt
|
|
509
|
+
{:verdict verdict
|
|
510
|
+
:output output
|
|
511
|
+
:diff-files (or diff-files [])}))
|
|
512
|
+
|
|
379
513
|
(case verdict
|
|
380
514
|
:approved
|
|
381
515
|
(do
|
|
382
|
-
(println (format "[%s] Reviewer APPROVED" worker-id))
|
|
516
|
+
(println (format "[%s] Reviewer APPROVED (attempt %d)" worker-id attempt))
|
|
383
517
|
{:approved? true :attempts attempt})
|
|
384
518
|
|
|
385
519
|
:rejected
|
|
386
520
|
(do
|
|
387
|
-
(println (format "[%s] Reviewer REJECTED" worker-id))
|
|
521
|
+
(println (format "[%s] Reviewer REJECTED (attempt %d)" worker-id attempt))
|
|
388
522
|
{:approved? false :attempts attempt})
|
|
389
523
|
|
|
390
524
|
;; :needs-changes
|
|
391
|
-
(
|
|
392
|
-
(
|
|
393
|
-
(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
(
|
|
397
|
-
|
|
398
|
-
|
|
525
|
+
(let [all-feedback (conj prev-feedback output)]
|
|
526
|
+
(if (>= attempt max-review-retries)
|
|
527
|
+
(do
|
|
528
|
+
(println (format "[%s] Max review retries reached (%d rounds)" worker-id attempt))
|
|
529
|
+
{:approved? false :attempts attempt})
|
|
530
|
+
(do
|
|
531
|
+
(println (format "[%s] Reviewer requested changes, fixing..." worker-id))
|
|
532
|
+
(run-fix! worker wt-path all-feedback)
|
|
533
|
+
(recur (inc attempt) all-feedback)))))))))
|
|
399
534
|
|
|
400
535
|
;; =============================================================================
|
|
401
536
|
;; Worker Loop
|
|
@@ -429,7 +564,8 @@
|
|
|
429
564
|
Worktrees persist until COMPLETE_AND_READY_FOR_MERGE triggers review+merge.
|
|
430
565
|
__DONE__ stops the worker entirely (planners only).
|
|
431
566
|
|
|
432
|
-
|
|
567
|
+
Tracks per-worker metrics: merges, rejections, errors, review-rounds-total.
|
|
568
|
+
Returns final worker state with metrics attached."
|
|
433
569
|
[worker]
|
|
434
570
|
(tasks/ensure-dirs!)
|
|
435
571
|
(let [{:keys [id iterations]} worker
|
|
@@ -445,99 +581,138 @@
|
|
|
445
581
|
(when-not (:can-plan worker)
|
|
446
582
|
(wait-for-tasks! id))
|
|
447
583
|
|
|
584
|
+
;; metrics tracks: {:merges N :rejections N :errors N :review-rounds-total N}
|
|
448
585
|
(loop [iter 1
|
|
449
586
|
completed 0
|
|
450
587
|
consec-errors 0
|
|
588
|
+
metrics {:merges 0 :rejections 0 :errors 0 :review-rounds-total 0}
|
|
451
589
|
session-id nil ;; persistent session-id (nil = start fresh)
|
|
452
590
|
wt-state nil] ;; {:dir :branch :path} or nil
|
|
453
|
-
(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
(
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
(if approved?
|
|
591
|
+
(let [finish (fn [status]
|
|
592
|
+
(assoc worker :completed completed :status status
|
|
593
|
+
:merges (:merges metrics)
|
|
594
|
+
:rejections (:rejections metrics)
|
|
595
|
+
:errors (:errors metrics)
|
|
596
|
+
:review-rounds-total (:review-rounds-total metrics)))]
|
|
597
|
+
(if (> iter iterations)
|
|
598
|
+
(do
|
|
599
|
+
;; Cleanup any lingering worktree
|
|
600
|
+
(when wt-state
|
|
601
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state)))
|
|
602
|
+
(println (format "[%s] Completed %d iterations (%d merges, %d rejections, %d errors)"
|
|
603
|
+
id completed (:merges metrics) (:rejections metrics) (:errors metrics)))
|
|
604
|
+
(finish :exhausted))
|
|
605
|
+
|
|
606
|
+
;; Ensure worktree exists (create fresh if nil, reuse if persisted)
|
|
607
|
+
(let [wt-state (try
|
|
608
|
+
(or wt-state (create-iteration-worktree! project-root id iter))
|
|
609
|
+
(catch Exception e
|
|
610
|
+
(println (format "[%s] Worktree creation failed: %s" id (.getMessage e)))
|
|
611
|
+
nil))]
|
|
612
|
+
(if (nil? wt-state)
|
|
613
|
+
;; Worktree creation failed — count as error
|
|
614
|
+
(let [errors (inc consec-errors)
|
|
615
|
+
metrics (update metrics :errors inc)]
|
|
616
|
+
(if (>= errors max-consecutive-errors)
|
|
617
|
+
(do
|
|
618
|
+
(println (format "[%s] %d consecutive errors, stopping" id errors))
|
|
619
|
+
(finish :error))
|
|
620
|
+
(recur (inc iter) completed errors metrics nil nil)))
|
|
621
|
+
|
|
622
|
+
;; Worktree ready — run agent
|
|
623
|
+
(let [resume? (some? session-id)
|
|
624
|
+
_ (println (format "[%s] %s iteration %d/%d"
|
|
625
|
+
id (if resume? "Resuming" "Starting") iter iterations))
|
|
626
|
+
context (build-context)
|
|
627
|
+
{:keys [output exit done? merge?] :as agent-result}
|
|
628
|
+
(run-agent! worker (:path wt-state) context session-id resume?)
|
|
629
|
+
new-session-id (:session-id agent-result)]
|
|
630
|
+
|
|
631
|
+
(cond
|
|
632
|
+
;; Agent errored — cleanup, reset session
|
|
633
|
+
(not (zero? exit))
|
|
634
|
+
(let [errors (inc consec-errors)
|
|
635
|
+
metrics (update metrics :errors inc)]
|
|
636
|
+
(println (format "[%s] Agent error (exit %d): %s"
|
|
637
|
+
id exit (subs (or output "") 0 (min 200 (count (or output ""))))))
|
|
638
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
639
|
+
(if (>= errors max-consecutive-errors)
|
|
503
640
|
(do
|
|
504
|
-
(
|
|
505
|
-
(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
641
|
+
(println (format "[%s] %d consecutive errors, stopping" id errors))
|
|
642
|
+
(finish :error))
|
|
643
|
+
(recur (inc iter) completed errors metrics nil nil)))
|
|
644
|
+
|
|
645
|
+
;; COMPLETE_AND_READY_FOR_MERGE — review, merge, reset session
|
|
646
|
+
merge?
|
|
647
|
+
(if (worktree-has-changes? (:path wt-state))
|
|
648
|
+
(if (task-only-diff? (:path wt-state))
|
|
649
|
+
;; Task-only changes — skip review, auto-merge
|
|
513
650
|
(do
|
|
514
|
-
(println (format "[%s]
|
|
515
|
-
(
|
|
516
|
-
|
|
651
|
+
(println (format "[%s] Task-only diff, auto-merging" id))
|
|
652
|
+
(let [merged? (merge-to-main! (:path wt-state) (:branch wt-state) id project-root 0)
|
|
653
|
+
metrics (if merged? (update metrics :merges inc) metrics)]
|
|
654
|
+
(println (format "[%s] Iteration %d/%d complete" id iter iterations))
|
|
655
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
656
|
+
(if (and done? (:can-plan worker))
|
|
657
|
+
(do
|
|
658
|
+
(println (format "[%s] Worker done after merge" id))
|
|
659
|
+
(assoc worker :completed (inc completed) :status :done
|
|
660
|
+
:merges (:merges metrics)
|
|
661
|
+
:rejections (:rejections metrics)
|
|
662
|
+
:errors (:errors metrics)
|
|
663
|
+
:review-rounds-total (:review-rounds-total metrics)))
|
|
664
|
+
(recur (inc iter) (inc completed) 0 metrics nil nil))))
|
|
665
|
+
;; Code changes — full review loop
|
|
666
|
+
(let [{:keys [approved? attempts]} (review-loop! worker (:path wt-state) id iter)
|
|
667
|
+
metrics (-> metrics
|
|
668
|
+
(update :review-rounds-total + (or attempts 0))
|
|
669
|
+
(update (if approved? :merges :rejections) inc))]
|
|
670
|
+
(if approved?
|
|
671
|
+
(do
|
|
672
|
+
(merge-to-main! (:path wt-state) (:branch wt-state) id project-root (or attempts 0))
|
|
673
|
+
(println (format "[%s] Iteration %d/%d complete" id iter iterations))
|
|
674
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
675
|
+
;; If also __DONE__, stop after merge
|
|
676
|
+
(if (and done? (:can-plan worker))
|
|
677
|
+
(do
|
|
678
|
+
(println (format "[%s] Worker done after merge" id))
|
|
679
|
+
(assoc worker :completed (inc completed) :status :done
|
|
680
|
+
:merges (:merges metrics)
|
|
681
|
+
:rejections (:rejections metrics)
|
|
682
|
+
:errors (:errors metrics)
|
|
683
|
+
:review-rounds-total (:review-rounds-total metrics)))
|
|
684
|
+
(recur (inc iter) (inc completed) 0 metrics nil nil)))
|
|
685
|
+
(do
|
|
686
|
+
(println (format "[%s] Iteration %d/%d rejected" id iter iterations))
|
|
687
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
688
|
+
(recur (inc iter) completed 0 metrics nil nil)))))
|
|
689
|
+
(do
|
|
690
|
+
(println (format "[%s] Merge signaled but no changes, skipping" id))
|
|
691
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
692
|
+
(recur (inc iter) completed 0 metrics nil nil)))
|
|
693
|
+
|
|
694
|
+
;; __DONE__ without merge — only honor for planners
|
|
695
|
+
(and done? (:can-plan worker))
|
|
696
|
+
(do
|
|
697
|
+
(println (format "[%s] Received __DONE__ signal" id))
|
|
698
|
+
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
699
|
+
(println (format "[%s] Worker done after %d/%d iterations" id iter iterations))
|
|
700
|
+
(finish :done))
|
|
701
|
+
|
|
702
|
+
;; __DONE__ from executor — ignore signal, but reset session since
|
|
703
|
+
;; the agent process exited. Resuming a dead session causes exit 1
|
|
704
|
+
;; which cascades into consecutive errors and premature stopping.
|
|
705
|
+
(and done? (not (:can-plan worker)))
|
|
517
706
|
(do
|
|
518
|
-
(println (format "[%s]
|
|
707
|
+
(println (format "[%s] Ignoring __DONE__ (executor), resetting session" id))
|
|
519
708
|
(cleanup-worktree! project-root (:dir wt-state) (:branch wt-state))
|
|
520
|
-
(recur (inc iter) completed 0 nil nil))
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
(println (format "[%s] Worker done after %d/%d iterations" id iter iterations))
|
|
528
|
-
(assoc worker :completed completed :status :done))
|
|
529
|
-
|
|
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))))))))))
|
|
709
|
+
(recur (inc iter) completed 0 metrics nil nil))
|
|
710
|
+
|
|
711
|
+
;; No signal — agent still working, resume next iteration
|
|
712
|
+
:else
|
|
713
|
+
(do
|
|
714
|
+
(println (format "[%s] Working... (will resume)" id))
|
|
715
|
+
(recur (inc iter) completed 0 metrics new-session-id wt-state)))))))))))
|
|
541
716
|
|
|
542
717
|
;; =============================================================================
|
|
543
718
|
;; Multi-Worker Execution
|
|
@@ -545,6 +720,7 @@
|
|
|
545
720
|
|
|
546
721
|
(defn run-workers!
|
|
547
722
|
"Run multiple workers in parallel.
|
|
723
|
+
Writes swarm summary to runs/{swarm-id}/summary.edn on completion.
|
|
548
724
|
|
|
549
725
|
Arguments:
|
|
550
726
|
workers - seq of worker configs
|
|
@@ -552,21 +728,106 @@
|
|
|
552
728
|
Returns seq of final worker states."
|
|
553
729
|
[workers]
|
|
554
730
|
(tasks/ensure-dirs!)
|
|
555
|
-
(
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
(
|
|
560
|
-
(
|
|
561
|
-
(
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
(
|
|
567
|
-
|
|
568
|
-
(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
731
|
+
(let [swarm-id (-> workers first :swarm-id)]
|
|
732
|
+
(println (format "Launching %d workers..." (count workers)))
|
|
733
|
+
|
|
734
|
+
(let [futures (doall
|
|
735
|
+
(map-indexed
|
|
736
|
+
(fn [idx worker]
|
|
737
|
+
(let [worker (assoc worker :id (or (:id worker) (str "w" idx)))]
|
|
738
|
+
(future (run-worker! worker))))
|
|
739
|
+
workers))]
|
|
740
|
+
|
|
741
|
+
(println "All workers launched. Waiting for completion...")
|
|
742
|
+
(let [results (mapv deref futures)]
|
|
743
|
+
(println "\nAll workers complete.")
|
|
744
|
+
(doseq [w results]
|
|
745
|
+
(println (format " [%s] %s - %d completed, %d merges, %d rejections, %d errors, %d review rounds"
|
|
746
|
+
(:id w)
|
|
747
|
+
(name (:status w))
|
|
748
|
+
(:completed w)
|
|
749
|
+
(or (:merges w) 0)
|
|
750
|
+
(or (:rejections w) 0)
|
|
751
|
+
(or (:errors w) 0)
|
|
752
|
+
(or (:review-rounds-total w) 0))))
|
|
753
|
+
|
|
754
|
+
;; Write swarm summary to disk
|
|
755
|
+
(when swarm-id
|
|
756
|
+
(runs/write-summary! swarm-id results)
|
|
757
|
+
(println (format "\nSwarm summary written to runs/%s/summary.edn" swarm-id)))
|
|
758
|
+
|
|
759
|
+
results))))
|
|
760
|
+
|
|
761
|
+
;; =============================================================================
|
|
762
|
+
;; Planner — first-class config concept, NOT a worker
|
|
763
|
+
;; =============================================================================
|
|
764
|
+
;; The planner creates task EDN files in tasks/pending/.
|
|
765
|
+
;; It runs in the project root (no worktree), has no review/merge cycle,
|
|
766
|
+
;; and respects max_pending backpressure to avoid flooding the queue.
|
|
767
|
+
|
|
768
|
+
(defn run-planner!
|
|
769
|
+
"Run planner agent to create tasks. No worktree, no review, no merge.
|
|
770
|
+
Runs in project root. Respects max_pending cap.
|
|
771
|
+
Returns {:tasks-created N}"
|
|
772
|
+
[{:keys [harness model prompts max-pending swarm-id]}]
|
|
773
|
+
(tasks/ensure-dirs!)
|
|
774
|
+
(let [project-root (System/getProperty "user.dir")
|
|
775
|
+
pending-before (tasks/pending-count)
|
|
776
|
+
max-pending (or max-pending 10)]
|
|
777
|
+
;; Backpressure: skip if queue is full
|
|
778
|
+
(if (>= pending-before max-pending)
|
|
779
|
+
(do
|
|
780
|
+
(println (format "[planner] Skipping — %d pending tasks (max: %d)" pending-before max-pending))
|
|
781
|
+
{:tasks-created 0})
|
|
782
|
+
;; Run agent
|
|
783
|
+
(let [context (build-context)
|
|
784
|
+
prompt-text (str (when (seq prompts)
|
|
785
|
+
(->> prompts
|
|
786
|
+
(map load-prompt)
|
|
787
|
+
(remove nil?)
|
|
788
|
+
(str/join "\n\n")))
|
|
789
|
+
"\n\nTask Status: " (:task_status context) "\n"
|
|
790
|
+
"Pending: " (:pending_tasks context) "\n\n"
|
|
791
|
+
"Create tasks in tasks/pending/ as .edn files.\n"
|
|
792
|
+
"Maximum " (- max-pending pending-before) " new tasks.\n"
|
|
793
|
+
"Signal __DONE__ when finished planning.")
|
|
794
|
+
swarm-id* (or swarm-id "unknown")
|
|
795
|
+
tagged-prompt (str "[oompa:" swarm-id* ":planner] " prompt-text)
|
|
796
|
+
abs-root (.getAbsolutePath (io/file project-root))
|
|
797
|
+
opencode-attach (opencode-attach-url)
|
|
798
|
+
|
|
799
|
+
cmd (case harness
|
|
800
|
+
:codex (cond-> [(resolve-binary! "codex") "exec"
|
|
801
|
+
"--dangerously-bypass-approvals-and-sandbox"
|
|
802
|
+
"--skip-git-repo-check"
|
|
803
|
+
"-C" abs-root]
|
|
804
|
+
model (into ["--model" model])
|
|
805
|
+
true (conj "--" tagged-prompt))
|
|
806
|
+
:claude (cond-> [(resolve-binary! "claude") "-p" "--dangerously-skip-permissions"]
|
|
807
|
+
model (into ["--model" model]))
|
|
808
|
+
:opencode (cond-> [(resolve-binary! "opencode") "run"]
|
|
809
|
+
model (into ["-m" model])
|
|
810
|
+
opencode-attach (into ["--attach" opencode-attach])
|
|
811
|
+
true (conj tagged-prompt)))
|
|
812
|
+
|
|
813
|
+
_ (println (format "[planner] Running (%s:%s, max_pending: %d, current: %d)"
|
|
814
|
+
(name harness) (or model "default") max-pending pending-before))
|
|
815
|
+
|
|
816
|
+
result (try
|
|
817
|
+
(if (= harness :claude)
|
|
818
|
+
(process/sh cmd {:dir abs-root :in tagged-prompt :out :string :err :string})
|
|
819
|
+
(process/sh cmd {:dir abs-root :out :string :err :string}))
|
|
820
|
+
(catch Exception e
|
|
821
|
+
(println (format "[planner] Agent exception: %s" (.getMessage e)))
|
|
822
|
+
{:exit -1 :out "" :err (.getMessage e)}))
|
|
823
|
+
|
|
824
|
+
;; Commit any new task files
|
|
825
|
+
_ (process/sh ["git" "add" "tasks/pending/"] {:dir abs-root})
|
|
826
|
+
_ (process/sh ["git" "commit" "-m" "Planner: add tasks"]
|
|
827
|
+
{:dir abs-root :out :string :err :string})
|
|
828
|
+
|
|
829
|
+
pending-after (tasks/pending-count)
|
|
830
|
+
created (- pending-after pending-before)]
|
|
831
|
+
|
|
832
|
+
(println (format "[planner] Done. Created %d tasks (pending: %d)" created pending-after))
|
|
833
|
+
{:tasks-created created}))))
|