@nbardy/oompa 0.7.0 → 0.7.1
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 +21 -4
- package/agentnet/src/agentnet/agent.clj +125 -6
- package/agentnet/src/agentnet/cli.clj +189 -63
- package/agentnet/src/agentnet/harness.clj +217 -0
- package/agentnet/src/agentnet/orchestrator.clj +2 -0
- package/agentnet/src/agentnet/runs.clj +73 -48
- package/agentnet/src/agentnet/schema.clj +1 -1
- package/agentnet/src/agentnet/tasks.clj +47 -0
- package/agentnet/src/agentnet/worker.clj +580 -305
- package/bin/test-models +1 -1
- package/config/prompts/_agent_scope_rules.md +7 -0
- package/config/prompts/_task_header.md +16 -48
- package/config/prompts/cto.md +2 -0
- package/config/prompts/engineer.md +2 -0
- package/config/prompts/executor.md +2 -0
- package/config/prompts/planner.md +3 -1
- package/config/prompts/reviewer.md +2 -0
- package/config/prompts/worker.md +7 -4
- package/oompa.example.json +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
(ns agentnet.harness
|
|
2
|
+
"Harness configuration registry.
|
|
3
|
+
|
|
4
|
+
Each harness is a data map describing how to invoke a CLI agent.
|
|
5
|
+
CLI flag syntax is owned by agent-cli (shared with claude-web-view).
|
|
6
|
+
This module owns: stdin behavior, session strategy, output parsing.
|
|
7
|
+
|
|
8
|
+
Harness = Codex | Claude | Opencode | Gemini
|
|
9
|
+
δ(harness) → config map → agent-cli JSON → command vector"
|
|
10
|
+
(:require [babashka.process :as process]
|
|
11
|
+
[cheshire.core :as json]
|
|
12
|
+
[clojure.string :as str]))
|
|
13
|
+
|
|
14
|
+
;; =============================================================================
|
|
15
|
+
;; Binary Resolution (shared across all harnesses)
|
|
16
|
+
;; =============================================================================
|
|
17
|
+
|
|
18
|
+
(def ^:private binary-paths* (atom {}))
|
|
19
|
+
|
|
20
|
+
(defn resolve-binary!
|
|
21
|
+
"Resolve the absolute path of a CLI binary. Caches result.
|
|
22
|
+
Throws if binary not found on PATH.
|
|
23
|
+
ProcessBuilder with :dir can fail to find bare command names on macOS/babashka,
|
|
24
|
+
so we resolve once via `which` and cache."
|
|
25
|
+
[name]
|
|
26
|
+
(or (get @binary-paths* name)
|
|
27
|
+
(let [result (try
|
|
28
|
+
(process/sh ["which" name] {:out :string :err :string})
|
|
29
|
+
(catch Exception _ {:exit -1 :out "" :err ""}))]
|
|
30
|
+
(if (zero? (:exit result))
|
|
31
|
+
(let [path (str/trim (:out result))]
|
|
32
|
+
(swap! binary-paths* assoc name path)
|
|
33
|
+
path)
|
|
34
|
+
(throw (ex-info (str "Binary not found on PATH: " name) {:binary name}))))))
|
|
35
|
+
|
|
36
|
+
;; =============================================================================
|
|
37
|
+
;; Opencode Output Parsing (harness-specific, lives here not in agent-cli)
|
|
38
|
+
;; =============================================================================
|
|
39
|
+
|
|
40
|
+
(defn- parse-opencode-run-output
|
|
41
|
+
"Parse `opencode run --format json` NDJSON output.
|
|
42
|
+
Returns {:session-id string|nil, :text string|nil}."
|
|
43
|
+
[s]
|
|
44
|
+
(let [raw (or s "")
|
|
45
|
+
events (->> (str/split-lines raw)
|
|
46
|
+
(keep (fn [line]
|
|
47
|
+
(try
|
|
48
|
+
(json/parse-string line true)
|
|
49
|
+
(catch Exception _
|
|
50
|
+
nil))))
|
|
51
|
+
doall)
|
|
52
|
+
session-id (or (some #(or (:sessionID %)
|
|
53
|
+
(:sessionId %)
|
|
54
|
+
(get-in % [:part :sessionID])
|
|
55
|
+
(get-in % [:part :sessionId]))
|
|
56
|
+
events)
|
|
57
|
+
(some-> (re-find #"(ses_[A-Za-z0-9]+)" raw) second))
|
|
58
|
+
text (->> events
|
|
59
|
+
(keep (fn [event]
|
|
60
|
+
(let [event-type (or (:type event) (get-in event [:part :type]))
|
|
61
|
+
chunk (or (:text event) (get-in event [:part :text]))]
|
|
62
|
+
(when (and (= event-type "text")
|
|
63
|
+
(string? chunk)
|
|
64
|
+
(not (str/blank? chunk)))
|
|
65
|
+
chunk))))
|
|
66
|
+
(str/join ""))]
|
|
67
|
+
{:session-id session-id
|
|
68
|
+
:text (when-not (str/blank? text) text)}))
|
|
69
|
+
|
|
70
|
+
;; =============================================================================
|
|
71
|
+
;; Env Helpers
|
|
72
|
+
;; =============================================================================
|
|
73
|
+
|
|
74
|
+
(defn- opencode-attach-url
|
|
75
|
+
"Optional opencode server URL for run --attach mode."
|
|
76
|
+
[]
|
|
77
|
+
(let [url (or (System/getenv "OOMPA_OPENCODE_ATTACH")
|
|
78
|
+
(System/getenv "OPENCODE_ATTACH"))]
|
|
79
|
+
(when (and url (not (str/blank? url)))
|
|
80
|
+
url)))
|
|
81
|
+
|
|
82
|
+
;; =============================================================================
|
|
83
|
+
;; Harness Registry — oompa-specific behavior fields only
|
|
84
|
+
;; =============================================================================
|
|
85
|
+
;;
|
|
86
|
+
;; CLI flag syntax (model flags, session flags, bypass, prompt delivery)
|
|
87
|
+
;; is owned by agent-cli. This registry only tracks:
|
|
88
|
+
;;
|
|
89
|
+
;; :stdin - what to pass as process stdin (:close or :prompt)
|
|
90
|
+
;; :session - session ID strategy (:uuid, :extracted, :implicit)
|
|
91
|
+
;; :output - output format (:plain or :ndjson)
|
|
92
|
+
|
|
93
|
+
(def registry
|
|
94
|
+
{:codex {:stdin :close :session :uuid :output :plain}
|
|
95
|
+
:claude {:stdin :prompt :session :uuid :output :plain}
|
|
96
|
+
:opencode {:stdin :close :session :extracted :output :ndjson}
|
|
97
|
+
:gemini {:stdin :close :session :implicit :output :plain}})
|
|
98
|
+
|
|
99
|
+
;; =============================================================================
|
|
100
|
+
;; Registry Access
|
|
101
|
+
;; =============================================================================
|
|
102
|
+
|
|
103
|
+
(defn get-config
|
|
104
|
+
"Look up harness config. Throws on unknown harness (no silent fallback)."
|
|
105
|
+
[harness-kw]
|
|
106
|
+
(or (get registry harness-kw)
|
|
107
|
+
(throw (ex-info (str "Unknown harness: " harness-kw
|
|
108
|
+
". Known: " (str/join ", " (map name (keys registry))))
|
|
109
|
+
{:harness harness-kw}))))
|
|
110
|
+
|
|
111
|
+
(defn known-harnesses
|
|
112
|
+
"Set of all registered harness keywords."
|
|
113
|
+
[]
|
|
114
|
+
(set (keys registry)))
|
|
115
|
+
|
|
116
|
+
;; =============================================================================
|
|
117
|
+
;; Command Builder — delegates to agent-cli via JSON input
|
|
118
|
+
;; =============================================================================
|
|
119
|
+
|
|
120
|
+
(defn- add-extra-args
|
|
121
|
+
"Attach harness-specific extraArgs to the JSON input map.
|
|
122
|
+
Only opencode needs extra flags (--format json, --attach)."
|
|
123
|
+
[m harness-kw opts]
|
|
124
|
+
(if (= harness-kw :opencode)
|
|
125
|
+
(let [url (opencode-attach-url)
|
|
126
|
+
extra (cond-> []
|
|
127
|
+
(:format? opts) (into ["--format" "json" "--print-logs" "--log-level" "WARN"])
|
|
128
|
+
url (into ["--attach" url]))]
|
|
129
|
+
(if (seq extra) (assoc m :extraArgs extra) m))
|
|
130
|
+
m))
|
|
131
|
+
|
|
132
|
+
(defn build-cmd
|
|
133
|
+
"Build CLI command vector via agent-cli JSON input.
|
|
134
|
+
Sends a JSON dict to `agent-cli build --input -`, parses the CommandSpec,
|
|
135
|
+
and resolves the binary to an absolute path.
|
|
136
|
+
|
|
137
|
+
agent-cli owns all CLI flag syntax (session create/resume, model decomposition,
|
|
138
|
+
bypass flags, prompt delivery). This function just maps oompa opts to JSON."
|
|
139
|
+
[harness-kw opts]
|
|
140
|
+
(let [input (-> {:harness (name harness-kw)
|
|
141
|
+
:bypassPermissions true}
|
|
142
|
+
(cond->
|
|
143
|
+
(:model opts) (assoc :model (:model opts))
|
|
144
|
+
(:prompt opts) (assoc :prompt (:prompt opts))
|
|
145
|
+
(:session-id opts) (assoc :sessionId (:session-id opts))
|
|
146
|
+
(:resume? opts) (assoc :resume true)
|
|
147
|
+
(:cwd opts) (assoc :cwd (:cwd opts))
|
|
148
|
+
(:reasoning opts) (assoc :reasoning (:reasoning opts)))
|
|
149
|
+
(add-extra-args harness-kw opts)
|
|
150
|
+
json/generate-string)
|
|
151
|
+
{:keys [exit out err]} (process/sh ["agent-cli" "build" "--input" "-"]
|
|
152
|
+
{:in input :out :string :err :string})]
|
|
153
|
+
(when-not (zero? exit)
|
|
154
|
+
(throw (ex-info (str "agent-cli: " (str/trim err)) {:exit exit})))
|
|
155
|
+
(let [argv (:argv (json/parse-string out true))]
|
|
156
|
+
(vec (cons (resolve-binary! (first argv)) (rest argv))))))
|
|
157
|
+
|
|
158
|
+
(defn process-stdin
|
|
159
|
+
"Return the :in value for process/sh.
|
|
160
|
+
:close → \"\" (close stdin immediately to prevent hang).
|
|
161
|
+
:prompt → the prompt text (claude delivers prompt via stdin)."
|
|
162
|
+
[harness-kw prompt]
|
|
163
|
+
(let [{:keys [stdin]} (get-config harness-kw)]
|
|
164
|
+
(case stdin
|
|
165
|
+
:prompt prompt
|
|
166
|
+
:close "")))
|
|
167
|
+
|
|
168
|
+
;; =============================================================================
|
|
169
|
+
;; Session Management
|
|
170
|
+
;; =============================================================================
|
|
171
|
+
|
|
172
|
+
(defn make-session-id
|
|
173
|
+
"Generate initial session-id based on harness strategy.
|
|
174
|
+
:uuid → random UUID string.
|
|
175
|
+
:extracted → nil (will be parsed from output after first run).
|
|
176
|
+
:implicit → nil (harness manages sessions by cwd, e.g. gemini)."
|
|
177
|
+
[harness-kw]
|
|
178
|
+
(let [{:keys [session]} (get-config harness-kw)]
|
|
179
|
+
(when (= session :uuid)
|
|
180
|
+
(str/lower-case (str (java.util.UUID/randomUUID))))))
|
|
181
|
+
|
|
182
|
+
;; =============================================================================
|
|
183
|
+
;; Output Parsing
|
|
184
|
+
;; =============================================================================
|
|
185
|
+
|
|
186
|
+
(defn parse-output
|
|
187
|
+
"Parse agent output. For :ndjson harnesses, extracts session-id and text.
|
|
188
|
+
For :plain, returns output as-is.
|
|
189
|
+
Returns {:output string, :session-id string}."
|
|
190
|
+
[harness-kw raw-output session-id]
|
|
191
|
+
(let [{:keys [output]} (get-config harness-kw)]
|
|
192
|
+
(if (= output :ndjson)
|
|
193
|
+
(let [parsed (parse-opencode-run-output raw-output)]
|
|
194
|
+
{:output (or (:text parsed) raw-output)
|
|
195
|
+
:session-id (or (:session-id parsed) session-id)})
|
|
196
|
+
{:output raw-output
|
|
197
|
+
:session-id session-id})))
|
|
198
|
+
|
|
199
|
+
;; =============================================================================
|
|
200
|
+
;; Probe / Health Check — delegates to agent-cli check
|
|
201
|
+
;; =============================================================================
|
|
202
|
+
|
|
203
|
+
(defn check-available
|
|
204
|
+
"Check if harness CLI binary is available on PATH via agent-cli."
|
|
205
|
+
[harness-kw]
|
|
206
|
+
(try
|
|
207
|
+
(let [{:keys [exit out]} (process/sh ["agent-cli" "check" (name harness-kw)]
|
|
208
|
+
{:out :string :err :string})]
|
|
209
|
+
(when (zero? exit)
|
|
210
|
+
(:available (json/parse-string out true))))
|
|
211
|
+
(catch Exception _ false)))
|
|
212
|
+
|
|
213
|
+
(defn build-probe-cmd
|
|
214
|
+
"Build minimal command to test if a harness+model is accessible.
|
|
215
|
+
Delegates to build-cmd with a '[_HIDE_TEST_] say ok' probe prompt."
|
|
216
|
+
[harness-kw model]
|
|
217
|
+
(build-cmd harness-kw {:model model :prompt "[_HIDE_TEST_] say ok"}))
|
|
@@ -286,6 +286,8 @@
|
|
|
286
286
|
(defn- ensure-dir! [path]
|
|
287
287
|
(.mkdirs (io/file path)))
|
|
288
288
|
|
|
289
|
+
;; DEAD CODE: save-run-log!, run-once!, run-loop! are unreachable since
|
|
290
|
+
;; cmd-run/cmd-loop simple-mode was retired 2026-02-17. Delete in a future pass.
|
|
289
291
|
(defn save-run-log!
|
|
290
292
|
"Save run results to JSONL file"
|
|
291
293
|
[state]
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
(ns agentnet.runs
|
|
2
|
-
"Structured persistence for swarm runs.
|
|
2
|
+
"Structured persistence for swarm runs — event-sourced immutable logs.
|
|
3
3
|
|
|
4
4
|
Layout:
|
|
5
5
|
runs/{swarm-id}/
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
started.json — swarm start event (PID, worker configs, planner)
|
|
7
|
+
stopped.json — swarm stop event (reason, optional error)
|
|
8
|
+
cycles/
|
|
9
|
+
{worker-id}-c{N}.json — per-cycle event log (one complete work unit)
|
|
8
10
|
reviews/
|
|
9
|
-
{worker-id}-
|
|
11
|
+
{worker-id}-c{N}-r{round}.json — per-cycle review log
|
|
10
12
|
|
|
13
|
+
All state is derived by reading event logs. No mutable summary files.
|
|
11
14
|
Written as JSON for easy consumption by claude-web-view and CLI.
|
|
12
15
|
All writes are atomic (write to .tmp, rename) to avoid partial reads."
|
|
13
16
|
(:require [clojure.java.io :as io]
|
|
@@ -26,8 +29,12 @@
|
|
|
26
29
|
(defn- reviews-dir [swarm-id]
|
|
27
30
|
(str (run-dir swarm-id) "/reviews"))
|
|
28
31
|
|
|
32
|
+
(defn- cycles-dir [swarm-id]
|
|
33
|
+
(str (run-dir swarm-id) "/cycles"))
|
|
34
|
+
|
|
29
35
|
(defn- ensure-run-dirs! [swarm-id]
|
|
30
|
-
(.mkdirs (io/file (reviews-dir swarm-id)))
|
|
36
|
+
(.mkdirs (io/file (reviews-dir swarm-id)))
|
|
37
|
+
(.mkdirs (io/file (cycles-dir swarm-id))))
|
|
31
38
|
|
|
32
39
|
;; =============================================================================
|
|
33
40
|
;; Atomic Write
|
|
@@ -43,17 +50,18 @@
|
|
|
43
50
|
(.renameTo tmp f)))
|
|
44
51
|
|
|
45
52
|
;; =============================================================================
|
|
46
|
-
;;
|
|
53
|
+
;; Started Event — written at swarm start
|
|
47
54
|
;; =============================================================================
|
|
48
55
|
|
|
49
|
-
(defn write-
|
|
50
|
-
"Write the
|
|
51
|
-
Contains: start time, worker configs, planner output, swarm metadata."
|
|
56
|
+
(defn write-started!
|
|
57
|
+
"Write the started event when a swarm begins.
|
|
58
|
+
Contains: start time, PID, worker configs, planner output, swarm metadata."
|
|
52
59
|
[swarm-id {:keys [workers planner-config reviewer-config config-file]}]
|
|
53
60
|
(ensure-run-dirs! swarm-id)
|
|
54
|
-
(write-json! (str (run-dir swarm-id) "/
|
|
61
|
+
(write-json! (str (run-dir swarm-id) "/started.json")
|
|
55
62
|
{:swarm-id swarm-id
|
|
56
63
|
:started-at (str (java.time.Instant/now))
|
|
64
|
+
:pid (.pid (java.lang.ProcessHandle/current))
|
|
57
65
|
:config-file config-file
|
|
58
66
|
:workers (mapv (fn [w]
|
|
59
67
|
{:id (:id w)
|
|
@@ -74,20 +82,33 @@
|
|
|
74
82
|
:model (:model reviewer-config)
|
|
75
83
|
:prompts (:prompts reviewer-config)})}))
|
|
76
84
|
|
|
85
|
+
;; =============================================================================
|
|
86
|
+
;; Stopped Event — written at swarm end
|
|
87
|
+
;; =============================================================================
|
|
88
|
+
|
|
89
|
+
(defn write-stopped!
|
|
90
|
+
"Write the stopped event when a swarm exits cleanly."
|
|
91
|
+
[swarm-id reason & {:keys [error]}]
|
|
92
|
+
(write-json! (str (run-dir swarm-id) "/stopped.json")
|
|
93
|
+
{:swarm-id swarm-id
|
|
94
|
+
:stopped-at (str (java.time.Instant/now))
|
|
95
|
+
:reason (name reason)
|
|
96
|
+
:error error}))
|
|
97
|
+
|
|
77
98
|
;; =============================================================================
|
|
78
99
|
;; Review Log — written after each review round
|
|
79
100
|
;; =============================================================================
|
|
80
101
|
|
|
81
102
|
(defn write-review-log!
|
|
82
|
-
"Write a review log for one
|
|
103
|
+
"Write a review log for one cycle of a worker.
|
|
83
104
|
Contains: verdict, round number, full reviewer output, diff file list."
|
|
84
|
-
[swarm-id worker-id
|
|
105
|
+
[swarm-id worker-id cycle round
|
|
85
106
|
{:keys [verdict output diff-files]}]
|
|
86
107
|
(ensure-run-dirs! swarm-id)
|
|
87
|
-
(let [filename (format "%s-
|
|
108
|
+
(let [filename (format "%s-c%d-r%d.json" worker-id cycle round)]
|
|
88
109
|
(write-json! (str (reviews-dir swarm-id) "/" filename)
|
|
89
110
|
{:worker-id worker-id
|
|
90
|
-
:
|
|
111
|
+
:cycle cycle
|
|
91
112
|
:round round
|
|
92
113
|
:verdict (name verdict)
|
|
93
114
|
:timestamp (str (java.time.Instant/now))
|
|
@@ -95,36 +116,30 @@
|
|
|
95
116
|
:diff-files (vec diff-files)})))
|
|
96
117
|
|
|
97
118
|
;; =============================================================================
|
|
98
|
-
;;
|
|
119
|
+
;; Cycle Log — written per cycle for real-time dashboard visibility
|
|
99
120
|
;; =============================================================================
|
|
100
121
|
|
|
101
|
-
(defn write-
|
|
102
|
-
"Write
|
|
103
|
-
Contains:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
:finished-at (str (java.time.Instant/now))
|
|
123
|
-
:total-workers (count worker-results)
|
|
124
|
-
:total-completed total-completed
|
|
125
|
-
:total-iterations total-iterations
|
|
126
|
-
:status-counts statuses
|
|
127
|
-
:workers per-worker})))
|
|
122
|
+
(defn write-cycle-log!
|
|
123
|
+
"Write a cycle event log for a worker.
|
|
124
|
+
Contains: outcome, timing, claimed task IDs, recycled tasks, session-id.
|
|
125
|
+
session-id links to the Claude CLI conversation on disk for debugging.
|
|
126
|
+
Written at cycle end so dashboards can track progress in real-time."
|
|
127
|
+
[swarm-id worker-id cycle
|
|
128
|
+
{:keys [outcome duration-ms claimed-task-ids recycled-tasks
|
|
129
|
+
error-snippet review-rounds session-id]}]
|
|
130
|
+
(when swarm-id
|
|
131
|
+
(let [filename (format "%s-c%d.json" worker-id cycle)]
|
|
132
|
+
(write-json! (str (cycles-dir swarm-id) "/" filename)
|
|
133
|
+
{:worker-id worker-id
|
|
134
|
+
:cycle cycle
|
|
135
|
+
:outcome (name outcome)
|
|
136
|
+
:timestamp (str (java.time.Instant/now))
|
|
137
|
+
:duration-ms duration-ms
|
|
138
|
+
:claimed-task-ids (or claimed-task-ids [])
|
|
139
|
+
:recycled-tasks (or recycled-tasks [])
|
|
140
|
+
:error-snippet error-snippet
|
|
141
|
+
:review-rounds (or review-rounds 0)
|
|
142
|
+
:session-id session-id}))))
|
|
128
143
|
|
|
129
144
|
;; =============================================================================
|
|
130
145
|
;; Read helpers (for cmd-status, dashboards)
|
|
@@ -140,17 +155,17 @@
|
|
|
140
155
|
(sort-by #(.lastModified %) >)
|
|
141
156
|
(mapv #(.getName %))))))
|
|
142
157
|
|
|
143
|
-
(defn read-
|
|
144
|
-
"Read
|
|
158
|
+
(defn read-started
|
|
159
|
+
"Read started.json for a swarm."
|
|
145
160
|
[swarm-id]
|
|
146
|
-
(let [f (io/file (str (run-dir swarm-id) "/
|
|
161
|
+
(let [f (io/file (str (run-dir swarm-id) "/started.json"))]
|
|
147
162
|
(when (.exists f)
|
|
148
163
|
(json/parse-string (slurp f) true))))
|
|
149
164
|
|
|
150
|
-
(defn read-
|
|
151
|
-
"Read
|
|
165
|
+
(defn read-stopped
|
|
166
|
+
"Read stopped.json for a swarm. Returns nil if still running."
|
|
152
167
|
[swarm-id]
|
|
153
|
-
(let [f (io/file (str (run-dir swarm-id) "/
|
|
168
|
+
(let [f (io/file (str (run-dir swarm-id) "/stopped.json"))]
|
|
154
169
|
(when (.exists f)
|
|
155
170
|
(json/parse-string (slurp f) true))))
|
|
156
171
|
|
|
@@ -163,3 +178,13 @@
|
|
|
163
178
|
(filter #(str/ends-with? (.getName %) ".json"))
|
|
164
179
|
(sort-by #(.getName %))
|
|
165
180
|
(mapv (fn [f] (json/parse-string (slurp f) true)))))))
|
|
181
|
+
|
|
182
|
+
(defn list-cycles
|
|
183
|
+
"List all cycle logs for a swarm, sorted by filename."
|
|
184
|
+
[swarm-id]
|
|
185
|
+
(let [d (io/file (cycles-dir swarm-id))]
|
|
186
|
+
(when (.exists d)
|
|
187
|
+
(->> (.listFiles d)
|
|
188
|
+
(filter #(str/ends-with? (.getName %) ".json"))
|
|
189
|
+
(sort-by #(.getName %))
|
|
190
|
+
(mapv (fn [f] (json/parse-string (slurp f) true)))))))
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
;; Enum Validators
|
|
41
41
|
;; =============================================================================
|
|
42
42
|
|
|
43
|
-
(def agent-types #{:codex :claude :opencode})
|
|
43
|
+
(def agent-types #{:codex :claude :opencode :gemini})
|
|
44
44
|
(def agent-roles #{:proposer :reviewer :cto})
|
|
45
45
|
(def task-statuses #{:pending :in-progress :review :approved :merged :failed :blocked})
|
|
46
46
|
(def worktree-statuses #{:available :busy :dirty :stale})
|
|
@@ -123,6 +123,35 @@
|
|
|
123
123
|
(when-let [task (first (list-pending))]
|
|
124
124
|
(claim-task! task)))
|
|
125
125
|
|
|
126
|
+
(defn claim-by-id!
|
|
127
|
+
"Claim a pending task by its :id string. Atomically moves pending → current.
|
|
128
|
+
Returns {:status :claimed|:already-taken|:not-found, :id id}."
|
|
129
|
+
[task-id]
|
|
130
|
+
(let [match (first (filter #(= (:id %) task-id) (list-pending)))]
|
|
131
|
+
(if match
|
|
132
|
+
(if (claim-task! match)
|
|
133
|
+
{:status :claimed :id task-id}
|
|
134
|
+
{:status :already-taken :id task-id})
|
|
135
|
+
;; Not in pending — check if already in current (raced)
|
|
136
|
+
(if (some #(= (:id %) task-id) (list-current))
|
|
137
|
+
{:status :already-taken :id task-id}
|
|
138
|
+
{:status :not-found :id task-id}))))
|
|
139
|
+
|
|
140
|
+
(defn claim-by-ids!
|
|
141
|
+
"Claim multiple tasks by ID. Returns vector of result maps."
|
|
142
|
+
[task-ids]
|
|
143
|
+
(mapv claim-by-id! task-ids))
|
|
144
|
+
|
|
145
|
+
(defn complete-by-ids!
|
|
146
|
+
"Move tasks from current → complete by ID. Framework-owned completion.
|
|
147
|
+
Returns vector of completed task IDs."
|
|
148
|
+
[task-ids]
|
|
149
|
+
(let [id-set (set task-ids)
|
|
150
|
+
to-complete (filter #(id-set (:id %)) (list-current))]
|
|
151
|
+
(doseq [task to-complete]
|
|
152
|
+
(complete-task! task))
|
|
153
|
+
(mapv :id to-complete)))
|
|
154
|
+
|
|
126
155
|
;; =============================================================================
|
|
127
156
|
;; Task Creation
|
|
128
157
|
;; =============================================================================
|
|
@@ -163,6 +192,24 @@
|
|
|
163
192
|
(defn current-count [] (count (list-task-files CURRENT_DIR)))
|
|
164
193
|
(defn complete-count [] (count (list-task-files COMPLETE_DIR)))
|
|
165
194
|
|
|
195
|
+
(defn current-task-ids
|
|
196
|
+
"Return a set of task IDs currently in current/."
|
|
197
|
+
[]
|
|
198
|
+
(->> (list-task-files CURRENT_DIR)
|
|
199
|
+
(keep read-task-file)
|
|
200
|
+
(map :id)
|
|
201
|
+
set))
|
|
202
|
+
|
|
203
|
+
(defn recycle-tasks!
|
|
204
|
+
"Move specific tasks from current/ back to pending/ by ID.
|
|
205
|
+
Returns vector of recycled task IDs."
|
|
206
|
+
[task-ids]
|
|
207
|
+
(let [current (list-current)
|
|
208
|
+
to-recycle (filter #(task-ids (:id %)) current)]
|
|
209
|
+
(doseq [task to-recycle]
|
|
210
|
+
(unclaim-task! task))
|
|
211
|
+
(mapv :id to-recycle)))
|
|
212
|
+
|
|
166
213
|
(defn all-complete?
|
|
167
214
|
"True if no pending or current tasks"
|
|
168
215
|
[]
|