@nbardy/oompa 0.7.1 → 0.7.3

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.
@@ -8,6 +8,21 @@
8
8
  (defn now-ms []
9
9
  (System/currentTimeMillis))
10
10
 
11
+ (defn- normalize-priority
12
+ "Coerce :priority to a sortable number.
13
+ Accepts: integer, \"P1\"/\"P2\" strings, numeric strings, nil.
14
+ Unknown/nil → 1000 (low priority)."
15
+ [p]
16
+ (cond
17
+ (number? p) p
18
+ (nil? p) 1000
19
+ (string? p)
20
+ (let [s (str/upper-case (str/trim p))]
21
+ (cond
22
+ (str/starts-with? s "P") (try (Integer/parseInt (subs s 1)) (catch Exception _ 1000))
23
+ :else (try (Integer/parseInt s) (catch Exception _ 1000))))
24
+ :else 1000))
25
+
11
26
  (defn format-ago
12
27
  "Return human-readable relative time string for epoch milliseconds."
13
28
  [^long ts-ms]
@@ -50,7 +65,7 @@
50
65
 
51
66
  (defn- queue-lines [tasks]
52
67
  (->> tasks
53
- (sort-by (juxt (comp (fnil identity 1000) :priority) :id))
68
+ (sort-by (juxt (comp normalize-priority :priority) :id))
54
69
  (map (fn [{:keys [id summary]}]
55
70
  (format "`%s` • %s" id summary)))))
56
71
 
@@ -82,7 +97,7 @@
82
97
 
83
98
  (defn- backlog-entries [tasks]
84
99
  (->> tasks
85
- (sort-by (juxt (comp (fnil identity 1000) :priority) :id))
100
+ (sort-by (juxt (comp normalize-priority :priority) :id))
86
101
  (map #(select-keys % [:id :summary]))
87
102
  (remove #(nil? (:id %)))
88
103
  (take 7)
@@ -34,12 +34,12 @@
34
34
  (throw (ex-info (str "Binary not found on PATH: " name) {:binary name}))))))
35
35
 
36
36
  ;; =============================================================================
37
- ;; Opencode Output Parsing (harness-specific, lives here not in agent-cli)
37
+ ;; NDJSON Output Parsing (harness-specific, lives here not in agent-cli)
38
38
  ;; =============================================================================
39
39
 
40
- (defn- parse-opencode-run-output
41
- "Parse `opencode run --format json` NDJSON output.
42
- Returns {:session-id string|nil, :text string|nil}."
40
+ (defn- parse-unified-jsonl-output
41
+ "Parse unified JSONL emitted by `agent-cli run`.
42
+ Returns {:session-id string|nil, :text string|nil, :warning string|nil}."
43
43
  [s]
44
44
  (let [raw (or s "")
45
45
  events (->> (str/split-lines raw)
@@ -49,23 +49,47 @@
49
49
  (catch Exception _
50
50
  nil))))
51
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))
52
+ event-types (->> events (keep :type) distinct (take 8) vec)
53
+ session-id (some->> events
54
+ (keep #(when (= "session.started" (:type %))
55
+ (:sessionId %)))
56
+ last)
58
57
  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 ""))]
58
+ (keep #(when (= "text.delta" (:type %)) (:text %)))
59
+ (remove str/blank?)
60
+ (str/join ""))
61
+ stderr-text (->> events
62
+ (keep #(when (= "stderr" (:type %)) (:text %)))
63
+ (remove str/blank?)
64
+ (str/join ""))
65
+ errors (->> events
66
+ (keep #(when (= "error" (:type %)) (:message %)))
67
+ (remove str/blank?)
68
+ vec)
69
+ warning (cond
70
+ (and (not (str/blank? raw)) (empty? events))
71
+ "agent-cli returned non-empty output, but no unified JSONL events were parsed."
72
+
73
+ (seq errors)
74
+ (str "agent-cli reported error events: " (str/join " | " (take 3 errors)))
75
+
76
+ (and (seq events) (str/blank? text))
77
+ (str "agent-cli returned unified events, but no text deltas were extracted"
78
+ " (types=" (if (seq event-types)
79
+ (str/join "," event-types)
80
+ "unknown")
81
+ ").")
82
+
83
+ :else nil)]
67
84
  {:session-id session-id
68
- :text (when-not (str/blank? text) text)}))
85
+ :text (cond
86
+ (not (str/blank? text)) text
87
+ (seq errors) (str/join "\n" errors)
88
+ (not (str/blank? stderr-text)) stderr-text
89
+ :else nil)
90
+ :warning warning
91
+ :raw-snippet (when-not (str/blank? raw)
92
+ (subs raw 0 (min 400 (count raw))))}))
69
93
 
70
94
  ;; =============================================================================
71
95
  ;; Env Helpers
@@ -90,11 +114,29 @@
90
114
  ;; :session - session ID strategy (:uuid, :extracted, :implicit)
91
115
  ;; :output - output format (:plain or :ndjson)
92
116
 
117
+ (def ^:private gemini-behavior
118
+ {:stdin :close :session :implicit :output :ndjson})
119
+
93
120
  (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}})
121
+ (merge
122
+ {:codex {:stdin :close :session :uuid :output :plain}
123
+ :claude {:stdin :prompt :session :uuid :output :plain}
124
+ :opencode {:stdin :close :session :extracted :output :ndjson}
125
+ :gemini gemini-behavior}
126
+ {:gemini1 gemini-behavior
127
+ :gemini2 gemini-behavior
128
+ :gemini3 gemini-behavior}))
129
+
130
+ (defn- gemini-alias?
131
+ [harness-kw]
132
+ (and (keyword? harness-kw)
133
+ (re-matches #"^gemini\\d+$" (name harness-kw))))
134
+
135
+ (defn valid-harness?
136
+ "True for explicit registry entries and any `geminiNN` alias."
137
+ [harness-kw]
138
+ (or (contains? (set (keys registry)) harness-kw)
139
+ (gemini-alias? harness-kw)))
98
140
 
99
141
  ;; =============================================================================
100
142
  ;; Registry Access
@@ -104,6 +146,7 @@
104
146
  "Look up harness config. Throws on unknown harness (no silent fallback)."
105
147
  [harness-kw]
106
148
  (or (get registry harness-kw)
149
+ (when (gemini-alias? harness-kw) gemini-behavior)
107
150
  (throw (ex-info (str "Unknown harness: " harness-kw
108
151
  ". Known: " (str/join ", " (map name (keys registry))))
109
152
  {:harness harness-kw}))))
@@ -131,11 +174,7 @@
131
174
 
132
175
  (defn build-cmd
133
176
  "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."
177
+ Used for probe/debug flows. Execution should prefer `run-command!`."
139
178
  [harness-kw opts]
140
179
  (let [input (-> {:harness (name harness-kw)
141
180
  :bypassPermissions true}
@@ -165,6 +204,26 @@
165
204
  :prompt prompt
166
205
  :close "")))
167
206
 
207
+ (defn run-command!
208
+ "Execute a harness through `agent-cli run`, which emits unified JSONL events."
209
+ [harness-kw opts]
210
+ (let [input (-> {:harness (name harness-kw)
211
+ :mode "conversation"
212
+ :prompt (:prompt opts)
213
+ :cwd (:cwd opts)
214
+ :yolo true}
215
+ (cond->
216
+ (:model opts) (assoc :model (:model opts))
217
+ (and (:session-id opts) (not (:resume? opts))) (assoc :sessionId (:session-id opts))
218
+ (and (:session-id opts) (:resume? opts)) (assoc :resumeSessionId (:session-id opts))
219
+ (:reasoning opts) (assoc :reasoningEffort (:reasoning opts))
220
+ (or (= harness-kw :gemini) (gemini-alias? harness-kw))
221
+ (assoc :debugRawEvents true))
222
+ (add-extra-args harness-kw opts)
223
+ json/generate-string)]
224
+ (process/sh ["agent-cli" "run" "--input" "-"]
225
+ {:in input :out :string :err :string})))
226
+
168
227
  ;; =============================================================================
169
228
  ;; Session Management
170
229
  ;; =============================================================================
@@ -184,17 +243,14 @@
184
243
  ;; =============================================================================
185
244
 
186
245
  (defn parse-output
187
- "Parse agent output. For :ndjson harnesses, extracts session-id and text.
188
- For :plain, returns output as-is.
246
+ "Parse unified JSONL output from `agent-cli run`.
189
247
  Returns {:output string, :session-id string}."
190
248
  [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})))
249
+ (let [parsed (parse-unified-jsonl-output raw-output)]
250
+ {:output (or (:text parsed) raw-output)
251
+ :session-id (or (:session-id parsed) session-id)
252
+ :warning (:warning parsed)
253
+ :raw-snippet (:raw-snippet parsed)}))
198
254
 
199
255
  ;; =============================================================================
200
256
  ;; Probe / Health Check — delegates to agent-cli check
@@ -66,8 +66,10 @@
66
66
  :workers (mapv (fn [w]
67
67
  {:id (:id w)
68
68
  :harness (name (:harness w))
69
- :model (:model w)
70
- :reasoning (:reasoning w)
69
+ :model (:model w)
70
+ :reasoning (:reasoning w)
71
+ :runs (:runs w)
72
+ :max-cycles (:max-cycles w)
71
73
  :iterations (:iterations w)
72
74
  :can-plan (:can-plan w)
73
75
  :prompts (:prompts w)})
@@ -103,7 +105,7 @@
103
105
  "Write a review log for one cycle of a worker.
104
106
  Contains: verdict, round number, full reviewer output, diff file list."
105
107
  [swarm-id worker-id cycle round
106
- {:keys [verdict output diff-files]}]
108
+ {:keys [verdict output diff-files duration-ms]}]
107
109
  (ensure-run-dirs! swarm-id)
108
110
  (let [filename (format "%s-c%d-r%d.json" worker-id cycle round)]
109
111
  (write-json! (str (reviews-dir swarm-id) "/" filename)
@@ -113,6 +115,7 @@
113
115
  :verdict (name verdict)
114
116
  :timestamp (str (java.time.Instant/now))
115
117
  :output output
118
+ :duration-ms (or duration-ms 0)
116
119
  :diff-files (vec diff-files)})))
117
120
 
118
121
  ;; =============================================================================
@@ -125,13 +128,14 @@
125
128
  session-id links to the Claude CLI conversation on disk for debugging.
126
129
  Written at cycle end so dashboards can track progress in real-time."
127
130
  [swarm-id worker-id cycle
128
- {:keys [outcome duration-ms claimed-task-ids recycled-tasks
129
- error-snippet review-rounds session-id]}]
131
+ {:keys [run outcome duration-ms claimed-task-ids recycled-tasks
132
+ error-snippet review-rounds session-id timing-ms]}]
130
133
  (when swarm-id
131
134
  (let [filename (format "%s-c%d.json" worker-id cycle)]
132
135
  (write-json! (str (cycles-dir swarm-id) "/" filename)
133
136
  {:worker-id worker-id
134
137
  :cycle cycle
138
+ :run (or run 1)
135
139
  :outcome (name outcome)
136
140
  :timestamp (str (java.time.Instant/now))
137
141
  :duration-ms duration-ms
@@ -139,7 +143,8 @@
139
143
  :recycled-tasks (or recycled-tasks [])
140
144
  :error-snippet error-snippet
141
145
  :review-rounds (or review-rounds 0)
142
- :session-id session-id}))))
146
+ :session-id session-id
147
+ :timing-ms timing-ms}))))
143
148
 
144
149
  ;; =============================================================================
145
150
  ;; Read helpers (for cmd-status, dashboards)
@@ -48,7 +48,14 @@
48
48
  (def merge-strategies #{:fast-forward :no-ff :squash :rebase})
49
49
  (def conflict-resolutions #{:ours :theirs :manual :abort})
50
50
 
51
- (defn agent-type? [x] (contains? agent-types x))
51
+ (defn- gemini-indexed-account?
52
+ [x]
53
+ (and (keyword? x)
54
+ (re-matches #"^gemini\d+$" (name x))))
55
+
56
+ (defn agent-type? [x]
57
+ (or (contains? agent-types x)
58
+ (gemini-indexed-account? x)))
52
59
  (defn agent-role? [x] (contains? agent-roles x))
53
60
  (defn task-status? [x] (contains? task-statuses x))
54
61
  (defn worktree-status? [x] (contains? worktree-statuses x))
@@ -210,6 +210,12 @@
210
210
  (unclaim-task! task))
211
211
  (mapv :id to-recycle)))
212
212
 
213
+ (defn recycle-all-current!
214
+ "Move every task from current/ back to pending/.
215
+ Returns vector of recycled task IDs."
216
+ []
217
+ (recycle-tasks! (current-task-ids)))
218
+
213
219
  (defn all-complete?
214
220
  "True if no pending or current tasks"
215
221
  []