@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,436 @@
|
|
|
1
|
+
(ns agentnet.cli
|
|
2
|
+
"Command-line interface for AgentNet orchestrator.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
./swarm.bb run # Run all tasks once
|
|
6
|
+
./swarm.bb run --workers 4 # With 4 parallel workers
|
|
7
|
+
./swarm.bb loop 20 --harness claude # 20 iterations with Claude
|
|
8
|
+
./swarm.bb loop --workers claude:5 codex:4 --iterations 20 # Mixed harnesses
|
|
9
|
+
./swarm.bb swarm oompa.json # Multi-model from config
|
|
10
|
+
./swarm.bb prompt \"...\" # Ad-hoc task
|
|
11
|
+
./swarm.bb status # Show last run
|
|
12
|
+
./swarm.bb worktrees # List worktree status
|
|
13
|
+
./swarm.bb cleanup # Remove all worktrees"
|
|
14
|
+
(:require [agentnet.orchestrator :as orchestrator]
|
|
15
|
+
[agentnet.worktree :as worktree]
|
|
16
|
+
[agentnet.worker :as worker]
|
|
17
|
+
[agentnet.tasks :as tasks]
|
|
18
|
+
[agentnet.agent :as agent]
|
|
19
|
+
[clojure.string :as str]
|
|
20
|
+
[clojure.java.io :as io]
|
|
21
|
+
[cheshire.core :as json]))
|
|
22
|
+
|
|
23
|
+
;; =============================================================================
|
|
24
|
+
;; Argument Parsing
|
|
25
|
+
;; =============================================================================
|
|
26
|
+
|
|
27
|
+
(defn- parse-int [s default]
|
|
28
|
+
(try
|
|
29
|
+
(Integer/parseInt s)
|
|
30
|
+
(catch Exception _ default)))
|
|
31
|
+
|
|
32
|
+
(defn- make-swarm-id
|
|
33
|
+
"Generate a short run-level swarm ID."
|
|
34
|
+
[]
|
|
35
|
+
(subs (str (java.util.UUID/randomUUID)) 0 8))
|
|
36
|
+
|
|
37
|
+
(defn- parse-worker-spec
|
|
38
|
+
"Parse 'harness:count' into {:harness :claude, :count 5}.
|
|
39
|
+
Throws on invalid format."
|
|
40
|
+
[s]
|
|
41
|
+
(let [[harness count-str] (str/split s #":" 2)
|
|
42
|
+
h (keyword harness)
|
|
43
|
+
cnt (parse-int count-str 0)]
|
|
44
|
+
(when-not (#{:codex :claude} h)
|
|
45
|
+
(throw (ex-info (str "Unknown harness in worker spec: " s ". Use 'codex:N' or 'claude:N'") {})))
|
|
46
|
+
(when (zero? cnt)
|
|
47
|
+
(throw (ex-info (str "Invalid count in worker spec: " s ". Use format 'harness:count'") {})))
|
|
48
|
+
{:harness h :count cnt}))
|
|
49
|
+
|
|
50
|
+
(defn- worker-spec? [s]
|
|
51
|
+
"Check if string looks like 'harness:count' format"
|
|
52
|
+
(and (string? s)
|
|
53
|
+
(not (str/starts-with? s "--"))
|
|
54
|
+
(str/includes? s ":")
|
|
55
|
+
(re-matches #"[a-z]+:\d+" s)))
|
|
56
|
+
|
|
57
|
+
(defn- collect-worker-specs
|
|
58
|
+
"Collect consecutive worker specs from args. Returns [specs remaining-args]."
|
|
59
|
+
[args]
|
|
60
|
+
(loop [specs []
|
|
61
|
+
remaining args]
|
|
62
|
+
(if-let [arg (first remaining)]
|
|
63
|
+
(if (worker-spec? arg)
|
|
64
|
+
(recur (conj specs (parse-worker-spec arg)) (next remaining))
|
|
65
|
+
[specs remaining])
|
|
66
|
+
[specs remaining])))
|
|
67
|
+
|
|
68
|
+
(defn parse-args [args]
|
|
69
|
+
(loop [opts {:workers 2
|
|
70
|
+
:harness :codex
|
|
71
|
+
:model nil
|
|
72
|
+
:dry-run false
|
|
73
|
+
:iterations 1
|
|
74
|
+
:worker-specs nil}
|
|
75
|
+
remaining args]
|
|
76
|
+
(if-let [arg (first remaining)]
|
|
77
|
+
(cond
|
|
78
|
+
;; --workers can take either N or harness:count specs
|
|
79
|
+
(= arg "--workers")
|
|
80
|
+
(let [next-arg (second remaining)]
|
|
81
|
+
(if (worker-spec? next-arg)
|
|
82
|
+
;; Collect all worker specs: --workers claude:5 codex:4
|
|
83
|
+
(let [[specs rest] (collect-worker-specs (next remaining))]
|
|
84
|
+
(recur (assoc opts :worker-specs specs) rest))
|
|
85
|
+
;; Simple count: --workers 4
|
|
86
|
+
(recur (assoc opts :workers (parse-int next-arg 2))
|
|
87
|
+
(nnext remaining))))
|
|
88
|
+
|
|
89
|
+
(= arg "--iterations")
|
|
90
|
+
(recur (assoc opts :iterations (parse-int (second remaining) 1))
|
|
91
|
+
(nnext remaining))
|
|
92
|
+
|
|
93
|
+
(= arg "--harness")
|
|
94
|
+
(let [h (keyword (second remaining))]
|
|
95
|
+
(when-not (#{:codex :claude} h)
|
|
96
|
+
(throw (ex-info (str "Unknown harness: " (second remaining) ". Use 'codex' or 'claude'") {})))
|
|
97
|
+
(recur (assoc opts :harness h)
|
|
98
|
+
(nnext remaining)))
|
|
99
|
+
|
|
100
|
+
(= arg "--model")
|
|
101
|
+
(recur (assoc opts :model (second remaining))
|
|
102
|
+
(nnext remaining))
|
|
103
|
+
|
|
104
|
+
;; Legacy flags (still supported)
|
|
105
|
+
(= arg "--claude")
|
|
106
|
+
(recur (assoc opts :harness :claude)
|
|
107
|
+
(next remaining))
|
|
108
|
+
|
|
109
|
+
(= arg "--codex")
|
|
110
|
+
(recur (assoc opts :harness :codex)
|
|
111
|
+
(next remaining))
|
|
112
|
+
|
|
113
|
+
(= arg "--dry-run")
|
|
114
|
+
(recur (assoc opts :dry-run true)
|
|
115
|
+
(next remaining))
|
|
116
|
+
|
|
117
|
+
(= arg "--keep-worktrees")
|
|
118
|
+
(recur (assoc opts :keep-worktrees true)
|
|
119
|
+
(next remaining))
|
|
120
|
+
|
|
121
|
+
(= arg "--")
|
|
122
|
+
{:opts opts :args (vec (next remaining))}
|
|
123
|
+
|
|
124
|
+
(str/starts-with? arg "--")
|
|
125
|
+
(throw (ex-info (str "Unknown option: " arg) {:arg arg}))
|
|
126
|
+
|
|
127
|
+
:else
|
|
128
|
+
{:opts opts :args (vec remaining)})
|
|
129
|
+
{:opts opts :args []})))
|
|
130
|
+
|
|
131
|
+
;; =============================================================================
|
|
132
|
+
;; Commands
|
|
133
|
+
;; =============================================================================
|
|
134
|
+
|
|
135
|
+
(defn cmd-run
|
|
136
|
+
"Run orchestrator once"
|
|
137
|
+
[opts args]
|
|
138
|
+
(let [swarm-id (make-swarm-id)]
|
|
139
|
+
(if-let [specs (:worker-specs opts)]
|
|
140
|
+
;; Mixed worker specs: --workers claude:5 codex:4
|
|
141
|
+
(let [workers (mapcat
|
|
142
|
+
(fn [spec]
|
|
143
|
+
(let [{:keys [harness count]} spec]
|
|
144
|
+
(map-indexed
|
|
145
|
+
(fn [idx _]
|
|
146
|
+
(worker/create-worker
|
|
147
|
+
{:id (format "%s-%d" (name harness) idx)
|
|
148
|
+
:swarm-id swarm-id
|
|
149
|
+
:harness harness
|
|
150
|
+
:model (:model opts)
|
|
151
|
+
:iterations 1}))
|
|
152
|
+
(range count))))
|
|
153
|
+
specs)]
|
|
154
|
+
(println (format "Running once with mixed workers (swarm %s):" swarm-id))
|
|
155
|
+
(doseq [spec specs]
|
|
156
|
+
(println (format " %dx %s" (:count spec) (name (:harness spec)))))
|
|
157
|
+
(println)
|
|
158
|
+
(worker/run-workers! workers))
|
|
159
|
+
;; Simple mode
|
|
160
|
+
(do
|
|
161
|
+
(println (format "Swarm ID: %s" swarm-id))
|
|
162
|
+
(orchestrator/run-once! (assoc opts :swarm-id swarm-id))))))
|
|
163
|
+
|
|
164
|
+
(defn cmd-loop
|
|
165
|
+
"Run orchestrator N times"
|
|
166
|
+
[opts args]
|
|
167
|
+
(let [swarm-id (make-swarm-id)
|
|
168
|
+
iterations (or (some-> (first args) (parse-int nil))
|
|
169
|
+
(:iterations opts)
|
|
170
|
+
20)]
|
|
171
|
+
(if-let [specs (:worker-specs opts)]
|
|
172
|
+
;; Mixed worker specs: --workers claude:5 codex:4
|
|
173
|
+
(let [workers (mapcat
|
|
174
|
+
(fn [spec]
|
|
175
|
+
(let [{:keys [harness count]} spec]
|
|
176
|
+
(map-indexed
|
|
177
|
+
(fn [idx _]
|
|
178
|
+
(worker/create-worker
|
|
179
|
+
{:id (format "%s-%d" (name harness) idx)
|
|
180
|
+
:swarm-id swarm-id
|
|
181
|
+
:harness harness
|
|
182
|
+
:model (:model opts)
|
|
183
|
+
:iterations iterations}))
|
|
184
|
+
(range count))))
|
|
185
|
+
specs)]
|
|
186
|
+
(println (format "Starting %d iterations with mixed workers (swarm %s):" iterations swarm-id))
|
|
187
|
+
(doseq [spec specs]
|
|
188
|
+
(println (format " %dx %s" (:count spec) (name (:harness spec)))))
|
|
189
|
+
(println)
|
|
190
|
+
(worker/run-workers! workers))
|
|
191
|
+
;; Simple mode: --workers N --harness X
|
|
192
|
+
(let [model-str (if (:model opts)
|
|
193
|
+
(format " (model: %s)" (:model opts))
|
|
194
|
+
"")]
|
|
195
|
+
(println (format "Starting %d iterations with %s harness%s..."
|
|
196
|
+
iterations (name (:harness opts)) model-str))
|
|
197
|
+
(println (format "Swarm ID: %s" swarm-id))
|
|
198
|
+
(orchestrator/run-loop! iterations (assoc opts :swarm-id swarm-id))))))
|
|
199
|
+
|
|
200
|
+
(defn cmd-prompt
|
|
201
|
+
"Run ad-hoc prompt as single task"
|
|
202
|
+
[opts args]
|
|
203
|
+
(let [prompt-text (str/join " " args)]
|
|
204
|
+
(when (str/blank? prompt-text)
|
|
205
|
+
(println "Error: prompt text required")
|
|
206
|
+
(System/exit 1))
|
|
207
|
+
;; Create temporary task
|
|
208
|
+
(let [task {:id (format "prompt-%d" (System/currentTimeMillis))
|
|
209
|
+
:summary prompt-text
|
|
210
|
+
:targets ["src" "tests" "docs"]
|
|
211
|
+
:priority 1}]
|
|
212
|
+
;; Write to temporary tasks file
|
|
213
|
+
(spit "config/tasks.edn" (pr-str [task]))
|
|
214
|
+
;; Run
|
|
215
|
+
(orchestrator/run-once! opts))))
|
|
216
|
+
|
|
217
|
+
(defn cmd-status
|
|
218
|
+
"Show status of last run"
|
|
219
|
+
[opts args]
|
|
220
|
+
(let [runs-dir (io/file "runs")
|
|
221
|
+
files (when (.exists runs-dir)
|
|
222
|
+
(->> (.listFiles runs-dir)
|
|
223
|
+
(filter #(.isFile %))
|
|
224
|
+
(sort-by #(.lastModified %) >)))]
|
|
225
|
+
(if-let [latest (first files)]
|
|
226
|
+
(do
|
|
227
|
+
(println (format "Latest run: %s" (.getName latest)))
|
|
228
|
+
(println)
|
|
229
|
+
(with-open [r (io/reader latest)]
|
|
230
|
+
(let [entries (mapv #(json/parse-string % true) (line-seq r))
|
|
231
|
+
by-status (group-by :status entries)]
|
|
232
|
+
(doseq [[status tasks] (sort-by first by-status)]
|
|
233
|
+
(println (format "%s: %d" (name status) (count tasks))))
|
|
234
|
+
(println)
|
|
235
|
+
(println (format "Total: %d tasks" (count entries))))))
|
|
236
|
+
(println "No runs found."))))
|
|
237
|
+
|
|
238
|
+
(defn cmd-worktrees
|
|
239
|
+
"List worktree status"
|
|
240
|
+
[opts args]
|
|
241
|
+
(let [state-file (io/file ".workers/state.edn")]
|
|
242
|
+
(if (.exists state-file)
|
|
243
|
+
(let [pool (read-string (slurp state-file))
|
|
244
|
+
pool' (worktree/list-worktrees pool)]
|
|
245
|
+
(println "Worktrees:")
|
|
246
|
+
(doseq [{:keys [id path status current-task]} pool']
|
|
247
|
+
(println (format " %s: %s%s"
|
|
248
|
+
id
|
|
249
|
+
(name status)
|
|
250
|
+
(if current-task
|
|
251
|
+
(str " [" current-task "]")
|
|
252
|
+
"")))))
|
|
253
|
+
(println "No worktrees initialized."))))
|
|
254
|
+
|
|
255
|
+
(defn cmd-cleanup
|
|
256
|
+
"Remove all worktrees"
|
|
257
|
+
[opts args]
|
|
258
|
+
(let [state-file (io/file ".workers/state.edn")]
|
|
259
|
+
(if (.exists state-file)
|
|
260
|
+
(let [pool (read-string (slurp state-file))]
|
|
261
|
+
(println "Removing worktrees...")
|
|
262
|
+
(worktree/cleanup-pool! pool)
|
|
263
|
+
(println "Done."))
|
|
264
|
+
(println "No worktrees to clean up."))))
|
|
265
|
+
|
|
266
|
+
(defn cmd-context
|
|
267
|
+
"Print current context (for debugging prompts)"
|
|
268
|
+
[opts args]
|
|
269
|
+
(let [ctx (orchestrator/build-context [])]
|
|
270
|
+
(println (:context_header ctx))))
|
|
271
|
+
|
|
272
|
+
(defn cmd-check
|
|
273
|
+
"Check if agent backends are available"
|
|
274
|
+
[opts args]
|
|
275
|
+
(println "Checking agent backends...")
|
|
276
|
+
(doseq [agent-type [:codex :claude]]
|
|
277
|
+
(let [available? (agent/check-available agent-type)]
|
|
278
|
+
(println (format " %s: %s"
|
|
279
|
+
(name agent-type)
|
|
280
|
+
(if available? "✓ available" "✗ not found"))))))
|
|
281
|
+
|
|
282
|
+
(defn- parse-model-string
|
|
283
|
+
"Parse 'harness:model' string into {:harness :model}"
|
|
284
|
+
[s]
|
|
285
|
+
(if (and s (str/includes? s ":"))
|
|
286
|
+
(let [[h m] (str/split s #":" 2)]
|
|
287
|
+
{:harness (keyword h) :model m})
|
|
288
|
+
{:harness :codex :model s}))
|
|
289
|
+
|
|
290
|
+
(defn cmd-swarm
|
|
291
|
+
"Run multiple worker configs from oompa.json in parallel"
|
|
292
|
+
[opts args]
|
|
293
|
+
(let [config-file (or (first args) "oompa.json")
|
|
294
|
+
f (io/file config-file)
|
|
295
|
+
swarm-id (make-swarm-id)]
|
|
296
|
+
(when-not (.exists f)
|
|
297
|
+
(println (format "Config file not found: %s" config-file))
|
|
298
|
+
(println)
|
|
299
|
+
(println "Create oompa.json with format:")
|
|
300
|
+
(println "{")
|
|
301
|
+
(println " \"review_model\": \"codex:codex-5.2\",")
|
|
302
|
+
(println " \"workers\": [")
|
|
303
|
+
(println " {\"model\": \"codex:codex-5.2-mini\", \"iterations\": 10, \"count\": 3},")
|
|
304
|
+
(println " {\"model\": \"codex:codex-5.2\", \"iterations\": 5, \"count\": 1, \"prompt\": \"prompts/planner.md\"}")
|
|
305
|
+
(println " ]")
|
|
306
|
+
(println "}")
|
|
307
|
+
(System/exit 1))
|
|
308
|
+
(let [config (json/parse-string (slurp f) true)
|
|
309
|
+
review-model (some-> (:review_model config) parse-model-string)
|
|
310
|
+
worker-configs (:workers config)
|
|
311
|
+
|
|
312
|
+
;; Expand worker configs by count
|
|
313
|
+
expanded-workers (mapcat (fn [wc]
|
|
314
|
+
(let [cnt (or (:count wc) 1)]
|
|
315
|
+
(repeat cnt (dissoc wc :count))))
|
|
316
|
+
worker-configs)
|
|
317
|
+
|
|
318
|
+
;; Convert to worker format
|
|
319
|
+
workers (map-indexed
|
|
320
|
+
(fn [idx wc]
|
|
321
|
+
(let [{:keys [harness model]} (parse-model-string (:model wc))]
|
|
322
|
+
(worker/create-worker
|
|
323
|
+
{:id (str "w" idx)
|
|
324
|
+
:swarm-id swarm-id
|
|
325
|
+
:harness harness
|
|
326
|
+
:model model
|
|
327
|
+
:iterations (or (:iterations wc) 10)
|
|
328
|
+
:custom-prompt (:prompt wc)
|
|
329
|
+
:review-harness (:harness review-model)
|
|
330
|
+
:review-model (:model review-model)})))
|
|
331
|
+
expanded-workers)]
|
|
332
|
+
|
|
333
|
+
(println (format "Swarm config from %s:" config-file))
|
|
334
|
+
(println (format " Swarm ID: %s" swarm-id))
|
|
335
|
+
(when review-model
|
|
336
|
+
(println (format " Review: %s:%s" (name (:harness review-model)) (:model review-model))))
|
|
337
|
+
(println (format " Workers: %d total" (count workers)))
|
|
338
|
+
(doseq [[idx wc] (map-indexed vector worker-configs)]
|
|
339
|
+
(let [{:keys [harness model]} (parse-model-string (:model wc))]
|
|
340
|
+
(println (format " - %dx %s:%s (%d iters%s)"
|
|
341
|
+
(or (:count wc) 1)
|
|
342
|
+
(name harness)
|
|
343
|
+
model
|
|
344
|
+
(or (:iterations wc) 10)
|
|
345
|
+
(if (:prompt wc) (str ", " (:prompt wc)) "")))))
|
|
346
|
+
(println)
|
|
347
|
+
|
|
348
|
+
;; Run workers using new worker module
|
|
349
|
+
(worker/run-workers! workers))))
|
|
350
|
+
|
|
351
|
+
(defn cmd-tasks
|
|
352
|
+
"Show task status"
|
|
353
|
+
[opts args]
|
|
354
|
+
(tasks/ensure-dirs!)
|
|
355
|
+
(let [status (tasks/status-summary)]
|
|
356
|
+
(println "Task Status:")
|
|
357
|
+
(println (format " Pending: %d" (:pending status)))
|
|
358
|
+
(println (format " Current: %d" (:current status)))
|
|
359
|
+
(println (format " Complete: %d" (:complete status)))
|
|
360
|
+
(println)
|
|
361
|
+
(when (pos? (:pending status))
|
|
362
|
+
(println "Pending tasks:")
|
|
363
|
+
(doseq [t (tasks/list-pending)]
|
|
364
|
+
(println (format " - %s: %s" (:id t) (:summary t)))))
|
|
365
|
+
(when (pos? (:current status))
|
|
366
|
+
(println "In-progress tasks:")
|
|
367
|
+
(doseq [t (tasks/list-current)]
|
|
368
|
+
(println (format " - %s: %s" (:id t) (:summary t)))))))
|
|
369
|
+
|
|
370
|
+
(defn cmd-help
|
|
371
|
+
"Print usage information"
|
|
372
|
+
[opts args]
|
|
373
|
+
(println "AgentNet Orchestrator")
|
|
374
|
+
(println)
|
|
375
|
+
(println "Usage: ./swarm.bb <command> [options]")
|
|
376
|
+
(println)
|
|
377
|
+
(println "Commands:")
|
|
378
|
+
(println " run Run all tasks once")
|
|
379
|
+
(println " loop N Run N iterations")
|
|
380
|
+
(println " swarm [file] Run multiple worker configs from oompa.json (parallel)")
|
|
381
|
+
(println " tasks Show task status (pending/current/complete)")
|
|
382
|
+
(println " prompt \"...\" Run ad-hoc prompt")
|
|
383
|
+
(println " status Show last run summary")
|
|
384
|
+
(println " worktrees List worktree status")
|
|
385
|
+
(println " cleanup Remove all worktrees")
|
|
386
|
+
(println " context Print context block")
|
|
387
|
+
(println " check Check agent backends")
|
|
388
|
+
(println " help Show this help")
|
|
389
|
+
(println)
|
|
390
|
+
(println "Options:")
|
|
391
|
+
(println " --workers N Number of parallel workers (default: 2)")
|
|
392
|
+
(println " --workers H:N [H:N ...] Mixed workers by harness (e.g., claude:5 codex:4)")
|
|
393
|
+
(println " --iterations N Number of iterations per worker (default: 1)")
|
|
394
|
+
(println " --harness {codex,claude} Agent harness to use (default: codex)")
|
|
395
|
+
(println " --model MODEL Model to use (e.g., codex-5.2, opus-4.5)")
|
|
396
|
+
(println " --dry-run Skip actual merges")
|
|
397
|
+
(println " --keep-worktrees Don't cleanup worktrees after run")
|
|
398
|
+
(println)
|
|
399
|
+
(println "Examples:")
|
|
400
|
+
(println " ./swarm.bb loop 10 --harness codex --model codex-5.2-mini --workers 3")
|
|
401
|
+
(println " ./swarm.bb loop --workers claude:5 codex:4 --iterations 20")
|
|
402
|
+
(println " ./swarm.bb swarm oompa.json # Run multi-model config"))
|
|
403
|
+
|
|
404
|
+
;; =============================================================================
|
|
405
|
+
;; Main Entry Point
|
|
406
|
+
;; =============================================================================
|
|
407
|
+
|
|
408
|
+
(def commands
|
|
409
|
+
{"run" cmd-run
|
|
410
|
+
"loop" cmd-loop
|
|
411
|
+
"swarm" cmd-swarm
|
|
412
|
+
"tasks" cmd-tasks
|
|
413
|
+
"prompt" cmd-prompt
|
|
414
|
+
"status" cmd-status
|
|
415
|
+
"worktrees" cmd-worktrees
|
|
416
|
+
"cleanup" cmd-cleanup
|
|
417
|
+
"context" cmd-context
|
|
418
|
+
"check" cmd-check
|
|
419
|
+
"help" cmd-help})
|
|
420
|
+
|
|
421
|
+
(defn -main [& args]
|
|
422
|
+
(let [[cmd & rest-args] args]
|
|
423
|
+
(if-let [handler (get commands cmd)]
|
|
424
|
+
(let [{:keys [opts args]} (parse-args rest-args)]
|
|
425
|
+
(try
|
|
426
|
+
(handler opts args)
|
|
427
|
+
(catch Exception e
|
|
428
|
+
(binding [*out* *err*]
|
|
429
|
+
(println (format "Error: %s" (.getMessage e))))
|
|
430
|
+
(System/exit 1))))
|
|
431
|
+
(do
|
|
432
|
+
(cmd-help {} [])
|
|
433
|
+
(when cmd
|
|
434
|
+
(println)
|
|
435
|
+
(println (format "Unknown command: %s" cmd)))
|
|
436
|
+
(System/exit (if cmd 1 0))))))
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
(ns agentnet.core
|
|
2
|
+
"Context assembly utilities shared by the AgentNet orchestrator."
|
|
3
|
+
(:require [agentnet.notes :as notes]
|
|
4
|
+
[babashka.process :as process]
|
|
5
|
+
[clojure.java.io :as io]
|
|
6
|
+
[clojure.string :as str]))
|
|
7
|
+
|
|
8
|
+
(defn now-ms []
|
|
9
|
+
(System/currentTimeMillis))
|
|
10
|
+
|
|
11
|
+
(defn format-ago
|
|
12
|
+
"Return human-readable relative time string for epoch milliseconds."
|
|
13
|
+
[^long ts-ms]
|
|
14
|
+
(let [delta (max 0 (- (now-ms) ts-ms))
|
|
15
|
+
s (long (/ delta 1000))]
|
|
16
|
+
(cond
|
|
17
|
+
(< s 15) "just now"
|
|
18
|
+
(< s 60) (str s "s ago")
|
|
19
|
+
:else (let [m (long (/ s 60))]
|
|
20
|
+
(cond
|
|
21
|
+
(< m 60) (str m "m ago")
|
|
22
|
+
:else (let [h (long (/ m 60))]
|
|
23
|
+
(if (< h 24)
|
|
24
|
+
(str h "h ago")
|
|
25
|
+
(str (long (/ h 24)) "d ago"))))))))
|
|
26
|
+
|
|
27
|
+
(defn- git-cmd
|
|
28
|
+
"Run git command in repo and return trimmed stdout or nil on failure."
|
|
29
|
+
[repo & args]
|
|
30
|
+
(try
|
|
31
|
+
(let [cmd (into ["git" "-C" repo] args)
|
|
32
|
+
{:keys [exit out]} (process/sh cmd {:out :string :err :string})]
|
|
33
|
+
(when (zero? exit)
|
|
34
|
+
(str/trim out)))
|
|
35
|
+
(catch Exception _ nil)))
|
|
36
|
+
|
|
37
|
+
(defn- repo-branch [repo]
|
|
38
|
+
(or (git-cmd repo "rev-parse" "--abbrev-ref" "HEAD") "HEAD"))
|
|
39
|
+
|
|
40
|
+
(defn- repo-head [repo]
|
|
41
|
+
(or (git-cmd repo "rev-parse" "--short" "HEAD") "HEAD"))
|
|
42
|
+
|
|
43
|
+
(defn- bulletize
|
|
44
|
+
"Render a collection as Markdown bullets; fallback provided when empty."
|
|
45
|
+
([items] (bulletize items "- (empty)"))
|
|
46
|
+
([items empty-text]
|
|
47
|
+
(if (seq items)
|
|
48
|
+
(str/join "\n" (map #(str "- " %) items))
|
|
49
|
+
empty-text)))
|
|
50
|
+
|
|
51
|
+
(defn- queue-lines [tasks]
|
|
52
|
+
(->> tasks
|
|
53
|
+
(sort-by (juxt (comp (fnil identity 1000) :priority) :id))
|
|
54
|
+
(map (fn [{:keys [id summary]}]
|
|
55
|
+
(format "`%s` • %s" id summary)))))
|
|
56
|
+
|
|
57
|
+
(defn- note->path [{:keys [dir name]}]
|
|
58
|
+
(str dir "/" name))
|
|
59
|
+
|
|
60
|
+
(defn- pending-items []
|
|
61
|
+
(->> (notes/green-ready)
|
|
62
|
+
(map (fn [note]
|
|
63
|
+
(let [paths (or (:targets note)
|
|
64
|
+
[(note->path note)])]
|
|
65
|
+
{:id (or (:id note) (:name note))
|
|
66
|
+
:age (format-ago (:mtime note))
|
|
67
|
+
:files (vec (take 3 paths))})))
|
|
68
|
+
(take 7)
|
|
69
|
+
vec))
|
|
70
|
+
|
|
71
|
+
(defn- recent-notes [recent-sec]
|
|
72
|
+
(let [cut (- (now-ms) (* 1000 (long (or recent-sec 120))))]
|
|
73
|
+
(->> ["scratch" "ready_for_review" "notes_FROM_CTO"]
|
|
74
|
+
(mapcat notes/list-notes)
|
|
75
|
+
(filter #(>= (:mtime %) cut))
|
|
76
|
+
(sort-by :mtime >)
|
|
77
|
+
(take 7)
|
|
78
|
+
(map (fn [note]
|
|
79
|
+
{:path (note->path note)
|
|
80
|
+
:age (format-ago (:mtime note))}))
|
|
81
|
+
vec)))
|
|
82
|
+
|
|
83
|
+
(defn- backlog-entries [tasks]
|
|
84
|
+
(->> tasks
|
|
85
|
+
(sort-by (juxt (comp (fnil identity 1000) :priority) :id))
|
|
86
|
+
(map #(select-keys % [:id :summary]))
|
|
87
|
+
(remove #(nil? (:id %)))
|
|
88
|
+
(take 7)
|
|
89
|
+
vec))
|
|
90
|
+
|
|
91
|
+
(def default-policy-rules
|
|
92
|
+
["patch-only" ".agent/* only" "minimal diff" "respect targets"])
|
|
93
|
+
|
|
94
|
+
(defn- policy-rules [policy]
|
|
95
|
+
(let [allow (:allow policy)
|
|
96
|
+
deny (:deny policy)
|
|
97
|
+
limit+ (:max-lines-added policy)
|
|
98
|
+
limit- (:max-lines-deleted policy)
|
|
99
|
+
files (:max-files policy)
|
|
100
|
+
custom (remove nil?
|
|
101
|
+
[(when (seq allow)
|
|
102
|
+
(str "allow:" (str/join "," allow)))
|
|
103
|
+
(when (seq deny)
|
|
104
|
+
(str "deny:" (str/join "," deny)))
|
|
105
|
+
(when limit+
|
|
106
|
+
(format "+≤%s lines" limit+))
|
|
107
|
+
(when limit-
|
|
108
|
+
(format "-≤%s lines" limit-))
|
|
109
|
+
(when files
|
|
110
|
+
(format "files≤%s" files))])]
|
|
111
|
+
(->> (concat default-policy-rules custom)
|
|
112
|
+
(distinct)
|
|
113
|
+
(take 7)
|
|
114
|
+
vec)))
|
|
115
|
+
|
|
116
|
+
(defn- next-work-suggestions [tasks pending hotspots]
|
|
117
|
+
(let [top-task (first tasks)
|
|
118
|
+
top-pending (first pending)
|
|
119
|
+
top-hotspot (first hotspots)
|
|
120
|
+
suggestions (cond-> []
|
|
121
|
+
top-pending
|
|
122
|
+
(conj (format "CTO: review %s (%s)" (:id top-pending) (:age top-pending)))
|
|
123
|
+
|
|
124
|
+
top-task
|
|
125
|
+
(conj (format "Engineer: pick %s — %s" (:id top-task) (:summary top-task)))
|
|
126
|
+
|
|
127
|
+
top-hotspot
|
|
128
|
+
(conj (format "CTO: investigate %s" (:path top-hotspot))))]
|
|
129
|
+
(take 5 (if (empty? suggestions)
|
|
130
|
+
["noop"]
|
|
131
|
+
suggestions))))
|
|
132
|
+
|
|
133
|
+
(defn- render-pending [items]
|
|
134
|
+
(if (seq items)
|
|
135
|
+
(mapcat (fn [{:keys [id age files]}]
|
|
136
|
+
[(format " - id: %s" (pr-str id))
|
|
137
|
+
(format " age: %s" age)
|
|
138
|
+
(format " files: [%s]" (str/join "," (map pr-str files)))])
|
|
139
|
+
items)
|
|
140
|
+
[" - []"]))
|
|
141
|
+
|
|
142
|
+
(defn- render-backlog [items]
|
|
143
|
+
(if (seq items)
|
|
144
|
+
(map (fn [{:keys [id summary]}]
|
|
145
|
+
(format " - {id: %s, summary: %s}" (pr-str id) (pr-str summary)))
|
|
146
|
+
items)
|
|
147
|
+
[" - []"]))
|
|
148
|
+
|
|
149
|
+
(defn- render-hotspots [items]
|
|
150
|
+
(if (seq items)
|
|
151
|
+
(map (fn [{:keys [path age]}]
|
|
152
|
+
(format " - {path: %s, age: %s}" (pr-str path) (pr-str age)))
|
|
153
|
+
items)
|
|
154
|
+
[" - []"]))
|
|
155
|
+
|
|
156
|
+
(defn- render-next-work [items]
|
|
157
|
+
(if (seq items)
|
|
158
|
+
(map #(str " - " (pr-str %)) items)
|
|
159
|
+
[" - \"noop\""]))
|
|
160
|
+
|
|
161
|
+
(defn- render-targets [targets]
|
|
162
|
+
(format "targets: [%s]" (str/join "," (map pr-str (or targets [])))))
|
|
163
|
+
|
|
164
|
+
(defn- render-policy [policy]
|
|
165
|
+
(format "policy: [%s]" (str/join "," (map pr-str policy))))
|
|
166
|
+
|
|
167
|
+
(defn- sanitize-mode [mode]
|
|
168
|
+
(if (#{"review" "propose"} mode) mode "propose"))
|
|
169
|
+
|
|
170
|
+
(defn build-context
|
|
171
|
+
"Return map of context tokens, including YAML header for prompts."
|
|
172
|
+
[{:keys [tasks policy repo recent-sec targets mode-hint]}
|
|
173
|
+
& {:as opts}]
|
|
174
|
+
(let [repo (or repo ".")
|
|
175
|
+
recent-sec (long (or recent-sec 180))
|
|
176
|
+
backlog (backlog-entries tasks)
|
|
177
|
+
pending (pending-items)
|
|
178
|
+
hotspots (recent-notes recent-sec)
|
|
179
|
+
policy-lines (policy-rules (or policy {}))
|
|
180
|
+
next-work (next-work-suggestions backlog pending hotspots)
|
|
181
|
+
branch (repo-branch repo)
|
|
182
|
+
head (repo-head repo)
|
|
183
|
+
header-lines (concat
|
|
184
|
+
["--- # agent_context"
|
|
185
|
+
(format "repo:\n branch: %s\n head: %s\n recent_window: %ss"
|
|
186
|
+
branch head recent-sec)
|
|
187
|
+
"pending:"]
|
|
188
|
+
(render-pending pending)
|
|
189
|
+
["backlog:"]
|
|
190
|
+
(render-backlog backlog)
|
|
191
|
+
["hotspots:"]
|
|
192
|
+
(render-hotspots hotspots)
|
|
193
|
+
[(render-targets (or targets []))
|
|
194
|
+
(render-policy policy-lines)
|
|
195
|
+
"next_work:"]
|
|
196
|
+
(render-next-work next-work)
|
|
197
|
+
[(format "mode: %s" (sanitize-mode mode-hint))
|
|
198
|
+
"---"])
|
|
199
|
+
header (str (str/join "\n" header-lines) "\n")
|
|
200
|
+
queue-md (bulletize (take 7 (queue-lines tasks)))
|
|
201
|
+
hotspots-md (bulletize (map #(format "`%s` (%s)" (:path %) (:age %))
|
|
202
|
+
hotspots)
|
|
203
|
+
"- (none)")
|
|
204
|
+
next-work-md (bulletize next-work "- noop")
|
|
205
|
+
diffstat-md "- (quiet)"]
|
|
206
|
+
{:context_header header
|
|
207
|
+
:ach_yaml header
|
|
208
|
+
:queue_md queue-md
|
|
209
|
+
:recent_files_md hotspots-md
|
|
210
|
+
:next_work_md next-work-md
|
|
211
|
+
:diffstat_md diffstat-md}))
|