@nbardy/oompa 0.7.0 → 0.7.2

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 CHANGED
@@ -85,7 +85,7 @@ This repo has a fleshed out version of the idea. The oompa loompas are organized
85
85
  "workers": [
86
86
  {"model": "claude:opus", "prompt": ["config/prompts/planner.md"], "iterations": 5, "count": 1},
87
87
  {"model": "codex:gpt-5.3-codex:medium", "prompt": ["config/prompts/executor.md"], "iterations": 10, "count": 2, "can_plan": false},
88
- {"model": "opencode:openai/gpt-5", "prompt": ["config/prompts/executor.md"], "iterations": 10, "count": 1, "can_plan": false}
88
+ {"model": "opencode:opencode/kimi-k2.5-free", "prompt": ["config/prompts/executor.md"], "iterations": 10, "count": 1, "can_plan": false}
89
89
  ]
90
90
  }
91
91
  ```
@@ -93,17 +93,18 @@ This repo has a fleshed out version of the idea. The oompa loompas are organized
93
93
  This spawns:
94
94
  - **1 planner** (opus) — reads spec, explores codebase, creates/refines tasks
95
95
  - **2 codex executors** (gpt-5.3-codex, medium reasoning) — claims and executes tasks fast
96
- - **1 opencode executor** (openai/gpt-5) — same task loop via `opencode run`
96
+ - **1 opencode executor** (opencode/kimi-k2.5-free) — same task loop via `opencode run`
97
97
 
98
98
  #### Worker fields
99
99
 
100
100
  | Field | Required | Description |
101
101
  |-------|----------|-------------|
102
- | `model` | yes | `harness:model` or `harness:model:reasoning` (e.g. `codex:gpt-5.3-codex:medium`, `claude:opus`, `opencode:openai/gpt-5`) |
102
+ | `model` | yes | `harness:model` or `harness:model:reasoning` (e.g. `codex:gpt-5.3-codex:medium`, `claude:opus`, `opencode:opencode/kimi-k2.5-free`) |
103
103
  | `prompt` | no | String or array of paths — concatenated into one prompt |
104
104
  | `iterations` | no | Max iterations per worker (default: 10) |
105
105
  | `count` | no | Number of workers with this config (default: 1) |
106
106
  | `can_plan` | no | If `false`, worker waits for tasks before starting (default: `true`) |
107
+ | `max_wait_for_tasks` | no | Max seconds a `can_plan: false` worker waits for queue work (default: `600`) |
107
108
 
108
109
  #### Composable prompts
109
110
 
@@ -113,7 +114,7 @@ This spawns:
113
114
  {
114
115
  "workers": [
115
116
  {"model": "claude:opus-4.5", "prompt": ["prompts/base.md", "prompts/architect.md"], "count": 1},
116
- {"model": "opencode:openai/gpt-5-mini", "prompt": ["prompts/base.md", "prompts/frontend.md"], "count": 2},
117
+ {"model": "opencode:opencode/kimi-k2.5-free", "prompt": ["prompts/base.md", "prompts/frontend.md"], "count": 2},
117
118
  {"model": "codex:codex-5.2-mini", "prompt": ["prompts/base.md", "prompts/backend.md"], "count": 2}
118
119
  ]
119
120
  }
@@ -121,6 +122,23 @@ This spawns:
121
122
 
122
123
  Every worker automatically gets task management instructions injected above your prompts. Your prompts just say *what* the worker should do — the framework handles *how* tasks work.
123
124
 
125
+ #### Prompt includes
126
+
127
+ Prompts support `#oompa_directive:include_file "path/to/file.md"` lines.
128
+
129
+ Use it to share common instructions across roles without copying content.
130
+ Paths are resolved relative to the prompt file containing the directive.
131
+
132
+ Example:
133
+
134
+ ```md
135
+ #oompa_directive:include_file "config/prompts/_agent_scope_rules.md"
136
+
137
+ You are an executor. Focus on minimal changes and complete tasks.
138
+ ```
139
+
140
+ The included file is inlined during prompt load, with a short header noting the injected source.
141
+
124
142
  ### Task System
125
143
 
126
144
  Workers self-organize via the filesystem. Tasks live at the project root and are shared across all worktrees:
@@ -174,7 +192,10 @@ cd oompa
174
192
  echo "Build a simple todo API with CRUD endpoints" > spec.md
175
193
 
176
194
  # Run the swarm
177
- ./swarm.bb swarm
195
+ ./swarm.bb run
196
+
197
+ # Run detached with startup validation (fails fast if startup fails)
198
+ ./swarm.bb run --detach --config oompa.json
178
199
  ```
179
200
 
180
201
  ## Install (npm)
@@ -195,7 +216,12 @@ oompa swarm
195
216
  ## Commands
196
217
 
197
218
  ```bash
198
- oompa swarm [file] # Run from oompa.json (default)
219
+ oompa run [file] # Run from config (defaults: oompa.json, oompa/oompa.json)
220
+ oompa run --detach --config oompa.json
221
+ oompa swarm [file] # Direct swarm command (foreground)
222
+ oompa list # List 20 most recent swarms
223
+ oompa list --all # Full swarm history
224
+ oompa view [swarm-id] # Per-worker runtime status (default: latest swarm)
199
225
  oompa tasks # Show task status
200
226
  oompa check # Check available backends
201
227
  oompa cleanup # Remove worktrees
@@ -88,10 +88,39 @@
88
88
  attach (into ["--attach" attach])
89
89
  true (conj prompt))))
90
90
 
91
+ (defn- gemini-cmd
92
+ [agent-type model prompt]
93
+ (cond-> [(name agent-type) "--yolo"]
94
+ model (into ["-m" model])
95
+ true (into ["-p" prompt])))
96
+
97
+ (defn- gemini-alias?
98
+ [agent-type]
99
+ (and (keyword? agent-type)
100
+ (re-matches #"^gemini\\d+$" (name agent-type))))
101
+
102
+ (defmethod build-command :gemini
103
+ [_ {:keys [model]} prompt cwd]
104
+ (gemini-cmd :gemini model prompt))
105
+
106
+ (defmethod build-command :gemini1
107
+ [agent-type {:keys [model]} prompt cwd]
108
+ (gemini-cmd agent-type model prompt))
109
+
110
+ (defmethod build-command :gemini2
111
+ [agent-type {:keys [model]} prompt cwd]
112
+ (gemini-cmd agent-type model prompt))
113
+
114
+ (defmethod build-command :gemini3
115
+ [agent-type {:keys [model]} prompt cwd]
116
+ (gemini-cmd agent-type model prompt))
117
+
91
118
  (defmethod build-command :default
92
- [agent-type _ _ _]
93
- (throw (ex-info (str "Unknown agent type: " agent-type)
94
- {:agent-type agent-type})))
119
+ [agent-type {:keys [model]} prompt _]
120
+ (if (gemini-alias? agent-type)
121
+ (gemini-cmd agent-type model prompt)
122
+ (throw (ex-info (str "Unknown agent type: " agent-type)
123
+ {:agent-type agent-type}))))
95
124
 
96
125
  ;; =============================================================================
97
126
  ;; Process Execution
@@ -125,7 +154,102 @@
125
154
  :stdout (truncate (:out result) 10000)
126
155
  :stderr (truncate (:err result) 5000)
127
156
  :duration-ms (- (now-ms) start)
128
- :timed-out? (boolean (:timed-out result))}))
157
+ :timed-out? (boolean (:timed-out result))}))
158
+
159
+ ;; =============================================================================
160
+ ;; Prompt Loading Helpers
161
+ ;; =============================================================================
162
+
163
+ (defn- file-canonical-path
164
+ "Resolve a path for cache keys and cycle detection."
165
+ [path]
166
+ (try
167
+ (.getCanonicalPath (io/file path))
168
+ (catch Exception _
169
+ path)))
170
+
171
+ (def ^:private prompt-file-cache
172
+ "Cache for prompt include expansion."
173
+ (atom {}))
174
+
175
+ (def ^:private include-directive-pattern
176
+ #"(?m)^\s*#oompa_directive:include_file\s+\"([^\"]+)\"\s*$")
177
+
178
+ (defn- read-file-cached
179
+ "Read a prompt file once and cache by canonical path."
180
+ [path]
181
+ (when path
182
+ (if-let [cached (get @prompt-file-cache path)]
183
+ cached
184
+ (let [f (io/file path)]
185
+ (when (.exists f)
186
+ (let [content (slurp f)]
187
+ (swap! prompt-file-cache assoc path content)
188
+ content))))))
189
+
190
+ (defn- resolve-include-path
191
+ "Resolve an include path relative to the file that declares it."
192
+ [source-path include-path]
193
+ (let [source-file (io/file source-path)
194
+ base-dir (.getParentFile source-file)]
195
+ (if (or (str/starts-with? include-path "/")
196
+ (and (> (count include-path) 1)
197
+ (= (nth include-path 1) \:)) ; Windows drive letter
198
+ (str/starts-with? include-path "~"))
199
+ include-path
200
+ (if base-dir
201
+ (str (io/file base-dir include-path))
202
+ include-path))))
203
+
204
+ (defn- expand-includes
205
+ "Expand #oompa_directive:include_file directives recursively.
206
+
207
+ Directive syntax:
208
+ #oompa_directive:include_file \"relative/or/absolute/path.md\"
209
+
210
+ Includes are resolved relative to the prompt file containing the directive.
211
+ Cycles are guarded by a simple visited-set."
212
+ ([raw source-path]
213
+ (expand-includes raw source-path #{}))
214
+ ([raw source-path visited]
215
+ (let [source-canonical (file-canonical-path source-path)
216
+ lines (str/split-lines (or raw ""))
217
+ visited' (conj visited source-canonical)]
218
+ (str/join
219
+ "\n"
220
+ (mapcat
221
+ (fn [line]
222
+ (if-let [match (re-matches include-directive-pattern line)]
223
+ (let [include-target (second match)
224
+ include-path (resolve-include-path source-canonical include-target)
225
+ include-canonical (file-canonical-path include-path)
226
+ included (and (not (str/blank? include-path))
227
+ (read-file-cached include-canonical))]
228
+ (cond
229
+ (str/blank? include-target)
230
+ ["[oompa] Empty include target in prompt directive"]
231
+
232
+ (contains? visited' include-canonical)
233
+ [(format "[oompa] Skipping already-included file: \"%s\"" include-target)]
234
+
235
+ (not included)
236
+ [(format "[oompa] Could not include \"%s\"" include-target)]
237
+
238
+ :else
239
+ (cons (format "We have included the content of file: \"%s\" below"
240
+ include-target)
241
+ (str/split-lines
242
+ (expand-includes included include-canonical visited')))))
243
+ [line]))
244
+ lines)))))
245
+
246
+ (defn- load-prompt-file
247
+ "Load a prompt file and expand include directives."
248
+ [path]
249
+ (when path
250
+ (when-let [f (io/file path)]
251
+ (when (.exists f)
252
+ (expand-includes (slurp f) (file-canonical-path path))))))
129
253
 
130
254
  ;; =============================================================================
131
255
  ;; Output Parsing
@@ -152,6 +276,17 @@
152
276
  [output]
153
277
  (boolean (re-find #"COMPLETE_AND_READY_FOR_MERGE" (or output ""))))
154
278
 
279
+ (defn parse-claim-signal
280
+ "Extract task IDs from CLAIM(...) signal in output.
281
+ Returns vector of task ID strings, or nil if no CLAIM signal found.
282
+ Format: CLAIM(task-001, task-003, task-005)"
283
+ [output]
284
+ (when-let [match (re-find #"CLAIM\(([^)]+)\)" (or output ""))]
285
+ (->> (str/split (second match) #",")
286
+ (map str/trim)
287
+ (remove str/blank?)
288
+ vec)))
289
+
155
290
  (defn- extract-comments
156
291
  "Extract bullet-point comments from output"
157
292
  [output]
@@ -228,7 +363,7 @@
228
363
  (let [filename (str "config/prompts/" (name role) ".md")
229
364
  f (io/file filename)]
230
365
  (when (.exists f)
231
- (slurp f))))
366
+ (load-prompt-file filename))))
232
367
 
233
368
  (defn load-custom-prompt
234
369
  "Load a custom prompt file. Returns content or nil."
@@ -236,10 +371,11 @@
236
371
  (when path
237
372
  (let [f (io/file path)]
238
373
  (when (.exists f)
239
- (slurp f)))))
374
+ (load-prompt-file path)))))
240
375
 
241
- (defn- tokenize
242
- "Replace {tokens} in template with values from context map"
376
+ (defn tokenize
377
+ "Replace {tokens} in template with values from context map.
378
+ Keys can be keywords or strings; values are stringified."
243
379
  [template tokens]
244
380
  (reduce (fn [acc [k v]]
245
381
  (str/replace acc
@@ -283,16 +419,23 @@
283
419
  (defn check-available
284
420
  "Check if agent backend is available"
285
421
  [agent-type]
286
- (let [cmd (case agent-type
287
- :codex ["codex" "--version"]
288
- :claude ["claude" "--version"]
289
- :opencode ["opencode" "--version"]
290
- ["echo" "unknown"])]
422
+ (let [cmd (cond
423
+ (= :codex agent-type) ["codex" "--version"]
424
+ (= :claude agent-type) ["claude" "--version"]
425
+ (= :opencode agent-type) ["opencode" "--version"]
426
+ (= :gemini agent-type) ["gemini" "--version"]
427
+ (gemini-alias? agent-type) [(name agent-type) "--version"]
428
+ :else ["echo" "unknown"])]
291
429
  (try
292
430
  (let [{:keys [exit]} (process/sh cmd {:out :string :err :string})]
293
431
  (zero? exit))
294
432
  (catch Exception _
295
- false))))
433
+ ;; Some CLIs (like gemini) may error on --version due to config issues
434
+ ;; but still exist on PATH. Fall back to `which`.
435
+ (try
436
+ (let [{:keys [exit]} (process/sh ["which" (first cmd)] {:out :string :err :string})]
437
+ (zero? exit))
438
+ (catch Exception _ false))))))
296
439
 
297
440
  (defn select-backend
298
441
  "Select first available backend from preference list"