@nbardy/oompa 0.6.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 CHANGED
@@ -39,7 +39,7 @@ This repo has a fleshed out version of the idea. The oompa loompas are organized
39
39
 
40
40
  - **Different worker types** — small models for fast execution, big models for planning
41
41
  - **Separate review model** — use a smart model to check work before merging
42
- - **Mixed harnesses** — combine Claude and Codex workers in one swarm
42
+ - **Mixed harnesses** — combine Claude, Codex, and Opencode workers in one swarm
43
43
  - **Self-directed tasks** — workers create and claim tasks from shared folders
44
44
 
45
45
  ### Architecture
@@ -84,20 +84,22 @@ This repo has a fleshed out version of the idea. The oompa loompas are organized
84
84
  {
85
85
  "workers": [
86
86
  {"model": "claude:opus", "prompt": ["config/prompts/planner.md"], "iterations": 5, "count": 1},
87
- {"model": "codex:gpt-5.3-codex:medium", "prompt": ["config/prompts/executor.md"], "iterations": 10, "count": 3, "can_plan": false}
87
+ {"model": "codex:gpt-5.3-codex:medium", "prompt": ["config/prompts/executor.md"], "iterations": 10, "count": 2, "can_plan": false},
88
+ {"model": "opencode:opencode/kimi-k2.5-free", "prompt": ["config/prompts/executor.md"], "iterations": 10, "count": 1, "can_plan": false}
88
89
  ]
89
90
  }
90
91
  ```
91
92
 
92
93
  This spawns:
93
94
  - **1 planner** (opus) — reads spec, explores codebase, creates/refines tasks
94
- - **3 executors** (gpt-5.3-codex, medium reasoning) — claims and executes tasks fast
95
+ - **2 codex executors** (gpt-5.3-codex, medium reasoning) — claims and executes tasks fast
96
+ - **1 opencode executor** (opencode/kimi-k2.5-free) — same task loop via `opencode run`
95
97
 
96
98
  #### Worker fields
97
99
 
98
100
  | Field | Required | Description |
99
101
  |-------|----------|-------------|
100
- | `model` | yes | `harness:model` or `harness:model:reasoning` (e.g. `codex:gpt-5.3-codex:medium`, `claude:opus`) |
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`) |
101
103
  | `prompt` | no | String or array of paths — concatenated into one prompt |
102
104
  | `iterations` | no | Max iterations per worker (default: 10) |
103
105
  | `count` | no | Number of workers with this config (default: 1) |
@@ -111,7 +113,7 @@ This spawns:
111
113
  {
112
114
  "workers": [
113
115
  {"model": "claude:opus-4.5", "prompt": ["prompts/base.md", "prompts/architect.md"], "count": 1},
114
- {"model": "codex:codex-5.2-mini", "prompt": ["prompts/base.md", "prompts/frontend.md"], "count": 2},
116
+ {"model": "opencode:opencode/kimi-k2.5-free", "prompt": ["prompts/base.md", "prompts/frontend.md"], "count": 2},
115
117
  {"model": "codex:codex-5.2-mini", "prompt": ["prompts/base.md", "prompts/backend.md"], "count": 2}
116
118
  ]
117
119
  }
@@ -119,6 +121,23 @@ This spawns:
119
121
 
120
122
  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.
121
123
 
124
+ #### Prompt includes
125
+
126
+ Prompts support `#oompa_directive:include_file "path/to/file.md"` lines.
127
+
128
+ Use it to share common instructions across roles without copying content.
129
+ Paths are resolved relative to the prompt file containing the directive.
130
+
131
+ Example:
132
+
133
+ ```md
134
+ #oompa_directive:include_file "config/prompts/_agent_scope_rules.md"
135
+
136
+ You are an executor. Focus on minimal changes and complete tasks.
137
+ ```
138
+
139
+ The included file is inlined during prompt load, with a short header noting the injected source.
140
+
122
141
  ### Task System
123
142
 
124
143
  Workers self-organize via the filesystem. Tasks live at the project root and are shared across all worktrees:
@@ -202,21 +221,28 @@ oompa help # Show all commands
202
221
 
203
222
  `./swarm.bb ...` works the same when running from a source checkout.
204
223
 
224
+ ## Opencode Harness
225
+
226
+ `opencode` workers use one-shot `opencode run --format json` calls with the same worker prompt tagging:
227
+
228
+ - First prompt line still starts with `[oompa:<swarmId>:<workerId>]`
229
+ - `-m/--model` is passed when a worker model is configured
230
+ - First iteration starts without `--session`; the worker captures `sessionID` from that exact run output
231
+ - On resumed iterations, workers pass `-s/--session <captured-id> --continue`
232
+ - Oompa does not call `opencode session list` to guess a "latest" session
233
+ - Worker completion markers (`COMPLETE_AND_READY_FOR_MERGE`, `__DONE__`) are evaluated from extracted text events, preserving existing done/merge behavior
234
+ - Optional attach mode: set `OOMPA_OPENCODE_ATTACH` (or `OPENCODE_ATTACH`) to add `--attach <url>`
235
+
205
236
  ## Worker Conversation Persistence
206
237
 
207
- If `codex-persist` is available, each worker writes its prompt/response messages
208
- to a per-worker session file for external UIs (for example worker panes in
209
- `claude-web-view`).
238
+ Workers rely on each CLI's native session persistence (no custom mirror writer):
210
239
 
211
- - Session ID: random lowercase UUID per iteration (one file per iteration)
212
- - First user message tag format: `[oompa:<swarmId>:<workerId>]`
213
- - CWD passed to `codex-persist` is the worker worktree absolute path
214
- - Codex workers use `codex-persist` writes; Claude workers use native `--session-id`
240
+ - Codex: native rollouts under `~/.codex/sessions/YYYY/MM/DD/*.jsonl`
241
+ - Claude: native project sessions under `~/.claude/projects/*/*.jsonl`
242
+ - Opencode: native session store managed by `opencode`
215
243
 
216
- Resolution order for the CLI command:
217
- 1. `CODEX_PERSIST_BIN` (if set)
218
- 2. `codex-persist` on `PATH`
219
- 3. `node ~/git/codex-persist/dist/cli.js`
244
+ Oompa still tags the first prompt line with `[oompa:<swarmId>:<workerId>]`
245
+ so downstream UIs can identify and group worker conversations.
220
246
 
221
247
  ## Requirements
222
248
 
@@ -226,6 +252,7 @@ Resolution order for the CLI command:
226
252
  - One of:
227
253
  - [Claude CLI](https://github.com/anthropics/claude-cli)
228
254
  - [Codex CLI](https://github.com/openai/codex)
255
+ - [Opencode CLI](https://github.com/sst/opencode)
229
256
 
230
257
  ## License
231
258
 
@@ -12,7 +12,8 @@
12
12
 
13
13
  Supported Backends:
14
14
  :codex - OpenAI Codex CLI (codex exec)
15
- :claude - Anthropic Claude CLI (claude -p)"
15
+ :claude - Anthropic Claude CLI (claude -p)
16
+ :opencode - opencode CLI (opencode run)"
16
17
  (:require [agentnet.schema :as schema]
17
18
  [babashka.process :as process]
18
19
  [clojure.java.io :as io]
@@ -71,6 +72,28 @@
71
72
  model (into ["--model" model])
72
73
  true (conj "--dangerously-skip-permissions")))
73
74
 
75
+ (defn- opencode-attach-url
76
+ "Optional opencode server URL for run --attach mode."
77
+ []
78
+ (let [url (or (System/getenv "OOMPA_OPENCODE_ATTACH")
79
+ (System/getenv "OPENCODE_ATTACH"))]
80
+ (when (and url (not (str/blank? url)))
81
+ url)))
82
+
83
+ (defmethod build-command :opencode
84
+ [_ {:keys [model]} prompt cwd]
85
+ (let [attach (opencode-attach-url)]
86
+ (cond-> ["opencode" "run"]
87
+ model (into ["-m" model])
88
+ attach (into ["--attach" attach])
89
+ true (conj prompt))))
90
+
91
+ (defmethod build-command :gemini
92
+ [_ {:keys [model]} prompt cwd]
93
+ (cond-> ["gemini" "--yolo"]
94
+ model (into ["-m" model])
95
+ true (into ["-p" prompt])))
96
+
74
97
  (defmethod build-command :default
75
98
  [agent-type _ _ _]
76
99
  (throw (ex-info (str "Unknown agent type: " agent-type)
@@ -108,7 +131,102 @@
108
131
  :stdout (truncate (:out result) 10000)
109
132
  :stderr (truncate (:err result) 5000)
110
133
  :duration-ms (- (now-ms) start)
111
- :timed-out? (boolean (:timed-out result))}))
134
+ :timed-out? (boolean (:timed-out result))}))
135
+
136
+ ;; =============================================================================
137
+ ;; Prompt Loading Helpers
138
+ ;; =============================================================================
139
+
140
+ (defn- file-canonical-path
141
+ "Resolve a path for cache keys and cycle detection."
142
+ [path]
143
+ (try
144
+ (.getCanonicalPath (io/file path))
145
+ (catch Exception _
146
+ path)))
147
+
148
+ (def ^:private prompt-file-cache
149
+ "Cache for prompt include expansion."
150
+ (atom {}))
151
+
152
+ (def ^:private include-directive-pattern
153
+ #"(?m)^\s*#oompa_directive:include_file\s+\"([^\"]+)\"\s*$")
154
+
155
+ (defn- read-file-cached
156
+ "Read a prompt file once and cache by canonical path."
157
+ [path]
158
+ (when path
159
+ (if-let [cached (get @prompt-file-cache path)]
160
+ cached
161
+ (let [f (io/file path)]
162
+ (when (.exists f)
163
+ (let [content (slurp f)]
164
+ (swap! prompt-file-cache assoc path content)
165
+ content))))))
166
+
167
+ (defn- resolve-include-path
168
+ "Resolve an include path relative to the file that declares it."
169
+ [source-path include-path]
170
+ (let [source-file (io/file source-path)
171
+ base-dir (.getParentFile source-file)]
172
+ (if (or (str/starts-with? include-path "/")
173
+ (and (> (count include-path) 1)
174
+ (= (nth include-path 1) \:)) ; Windows drive letter
175
+ (str/starts-with? include-path "~"))
176
+ include-path
177
+ (if base-dir
178
+ (str (io/file base-dir include-path))
179
+ include-path))))
180
+
181
+ (defn- expand-includes
182
+ "Expand #oompa_directive:include_file directives recursively.
183
+
184
+ Directive syntax:
185
+ #oompa_directive:include_file \"relative/or/absolute/path.md\"
186
+
187
+ Includes are resolved relative to the prompt file containing the directive.
188
+ Cycles are guarded by a simple visited-set."
189
+ ([raw source-path]
190
+ (expand-includes raw source-path #{}))
191
+ ([raw source-path visited]
192
+ (let [source-canonical (file-canonical-path source-path)
193
+ lines (str/split-lines (or raw ""))
194
+ visited' (conj visited source-canonical)]
195
+ (str/join
196
+ "\n"
197
+ (mapcat
198
+ (fn [line]
199
+ (if-let [match (re-matches include-directive-pattern line)]
200
+ (let [include-target (second match)
201
+ include-path (resolve-include-path source-canonical include-target)
202
+ include-canonical (file-canonical-path include-path)
203
+ included (and (not (str/blank? include-path))
204
+ (read-file-cached include-canonical))]
205
+ (cond
206
+ (str/blank? include-target)
207
+ ["[oompa] Empty include target in prompt directive"]
208
+
209
+ (contains? visited' include-canonical)
210
+ [(format "[oompa] Skipping already-included file: \"%s\"" include-target)]
211
+
212
+ (not included)
213
+ [(format "[oompa] Could not include \"%s\"" include-target)]
214
+
215
+ :else
216
+ (cons (format "We have included the content of file: \"%s\" below"
217
+ include-target)
218
+ (str/split-lines
219
+ (expand-includes included include-canonical visited')))))
220
+ [line]))
221
+ lines)))))
222
+
223
+ (defn- load-prompt-file
224
+ "Load a prompt file and expand include directives."
225
+ [path]
226
+ (when path
227
+ (when-let [f (io/file path)]
228
+ (when (.exists f)
229
+ (expand-includes (slurp f) (file-canonical-path path))))))
112
230
 
113
231
  ;; =============================================================================
114
232
  ;; Output Parsing
@@ -135,6 +253,17 @@
135
253
  [output]
136
254
  (boolean (re-find #"COMPLETE_AND_READY_FOR_MERGE" (or output ""))))
137
255
 
256
+ (defn parse-claim-signal
257
+ "Extract task IDs from CLAIM(...) signal in output.
258
+ Returns vector of task ID strings, or nil if no CLAIM signal found.
259
+ Format: CLAIM(task-001, task-003, task-005)"
260
+ [output]
261
+ (when-let [match (re-find #"CLAIM\(([^)]+)\)" (or output ""))]
262
+ (->> (str/split (second match) #",")
263
+ (map str/trim)
264
+ (remove str/blank?)
265
+ vec)))
266
+
138
267
  (defn- extract-comments
139
268
  "Extract bullet-point comments from output"
140
269
  [output]
@@ -211,7 +340,7 @@
211
340
  (let [filename (str "config/prompts/" (name role) ".md")
212
341
  f (io/file filename)]
213
342
  (when (.exists f)
214
- (slurp f))))
343
+ (load-prompt-file filename))))
215
344
 
216
345
  (defn load-custom-prompt
217
346
  "Load a custom prompt file. Returns content or nil."
@@ -219,10 +348,11 @@
219
348
  (when path
220
349
  (let [f (io/file path)]
221
350
  (when (.exists f)
222
- (slurp f)))))
351
+ (load-prompt-file path)))))
223
352
 
224
- (defn- tokenize
225
- "Replace {tokens} in template with values from context map"
353
+ (defn tokenize
354
+ "Replace {tokens} in template with values from context map.
355
+ Keys can be keywords or strings; values are stringified."
226
356
  [template tokens]
227
357
  (reduce (fn [acc [k v]]
228
358
  (str/replace acc
@@ -269,12 +399,19 @@
269
399
  (let [cmd (case agent-type
270
400
  :codex ["codex" "--version"]
271
401
  :claude ["claude" "--version"]
402
+ :opencode ["opencode" "--version"]
403
+ :gemini ["gemini" "--version"]
272
404
  ["echo" "unknown"])]
273
405
  (try
274
406
  (let [{:keys [exit]} (process/sh cmd {:out :string :err :string})]
275
407
  (zero? exit))
276
408
  (catch Exception _
277
- false))))
409
+ ;; Some CLIs (like gemini) may error on --version due to config issues
410
+ ;; but still exist on PATH. Fall back to `which`.
411
+ (try
412
+ (let [{:keys [exit]} (process/sh ["which" (first cmd)] {:out :string :err :string})]
413
+ (zero? exit))
414
+ (catch Exception _ false))))))
278
415
 
279
416
  (defn select-backend
280
417
  "Select first available backend from preference list"