@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 ADDED
@@ -0,0 +1,197 @@
1
+ # oompa
2
+
3
+ ![Oompa Loompas building code](docs/resources/oompa-banner.png)
4
+
5
+ *Getting your ralphs to work together*
6
+
7
+ ---
8
+
9
+ ## The Minimal Idea
10
+
11
+ From the [Oompa Loompas blog post](notes/2025-01-instant_software_blog_draft.md) — the simplest multi-agent swarm:
12
+
13
+ **oompa_loompas.sh** (7 lines):
14
+ ```bash
15
+ #!/bin/bash
16
+ for w in $(seq 1 ${WORKERS:-3}); do
17
+ (for i in $(seq 1 ${ITERATIONS:-5}); do
18
+ wt=".w${w}-i${i}"
19
+ git worktree add $wt -b $wt 2>/dev/null
20
+ { echo "Worktree: $wt"; cat prompts/worker.md; } | claude -p -
21
+ done) &
22
+ done; wait
23
+ ```
24
+
25
+ **prompts/worker.md** (3 lines):
26
+ ```
27
+ Goal: Match spec.md
28
+ Process: Create/claim tasks in tasks/{pending,in_progress,complete}.md
29
+ Method: Isolate changes to your worktree, commit and merge when complete
30
+ ```
31
+
32
+ That's it. Parallel agents with worktree isolation.
33
+
34
+ ---
35
+
36
+ ## The Full Version
37
+
38
+ This repo has a fleshed out version of the idea. The oompa loompas are organized by a more sophisticated Clojure harness, enabling advanced features:
39
+
40
+ - **Different worker types** — small models for fast execution, big models for planning
41
+ - **Separate review model** — use a smart model to check work before merging
42
+ - **Mixed harnesses** — combine Claude and Codex workers in one swarm
43
+ - **Self-directed tasks** — workers create and claim tasks from shared folders
44
+
45
+ ### Architecture
46
+
47
+ ```
48
+ ┌─────────────────────────────────────────────────────────────────────┐
49
+ │ SELF-DIRECTED WORKERS │
50
+ │ │
51
+ │ tasks/pending/*.edn ──→ Worker claims ──→ tasks/current/*.edn │
52
+ │ ▲ │ │
53
+ │ │ ▼ │
54
+ │ │ Execute in worktree │
55
+ │ │ │ │
56
+ │ │ ▼ │
57
+ │ │ Commit changes │
58
+ │ │ │ │
59
+ │ │ ▼ │
60
+ │ │ ┌─────────────────────┐ │
61
+ │ │ │ REVIEWER checks │ │
62
+ │ │ │ (review_model) │ │
63
+ │ │ └──────────┬──────────┘ │
64
+ │ │ ┌─────┴─────┐ │
65
+ │ │ ▼ ▼ │
66
+ │ │ Approved Rejected │
67
+ │ │ │ │ │
68
+ │ │ ▼ └──────┐ │
69
+ │ │ Merge to │ │
70
+ │ │ main │ │
71
+ │ │ │ ▼ │
72
+ │ │ │ Fix & retry ──→ Reviewer │
73
+ │ │ │ │
74
+ │ └─── Create tasks ◄──┘ │
75
+ │ │
76
+ │ Exit when: __DONE__ token emitted │
77
+ └─────────────────────────────────────────────────────────────────────┘
78
+ ```
79
+
80
+ ### Configuration
81
+
82
+ **oompa.json**:
83
+ ```json
84
+ {
85
+ "review_model": "codex:codex-5.2",
86
+ "workers": [
87
+ {"model": "claude:opus-4.5", "iterations": 5, "count": 1, "prompt": "config/prompts/planner.md"},
88
+ {"model": "codex:codex-5.2-mini", "iterations": 10, "count": 3, "prompt": "config/prompts/executor.md"}
89
+ ]
90
+ }
91
+ ```
92
+
93
+ This spawns:
94
+ - **1 planner** (opus) — creates tasks, doesn't write code
95
+ - **3 executors** (mini) — claims and executes tasks fast
96
+ - **Reviews** done by codex-5.2 before any merge
97
+
98
+ ### Task System
99
+
100
+ Workers self-organize via filesystem:
101
+
102
+ ```
103
+ tasks/
104
+ ├── pending/*.edn # unclaimed tasks
105
+ ├── current/*.edn # in progress
106
+ └── complete/*.edn # done
107
+ ```
108
+
109
+ Workers can:
110
+ - **Claim tasks**: `mv pending/task.edn current/`
111
+ - **Complete tasks**: `mv current/task.edn complete/`
112
+ - **Create tasks**: write new `.edn` to `pending/`
113
+
114
+ ### Prompts
115
+
116
+ Three built-in worker types:
117
+
118
+ | Prompt | Role | Creates Tasks? | Executes Tasks? |
119
+ |--------|------|----------------|-----------------|
120
+ | `worker.md` | Hybrid | ✓ | ✓ |
121
+ | `planner.md` | Planner | ✓ | ✗ |
122
+ | `executor.md` | Executor | ✗ | ✓ |
123
+
124
+ ---
125
+
126
+ ## Quick Start
127
+
128
+ ```bash
129
+ # Clone
130
+ git clone https://github.com/nbardy/oompa.git
131
+ cd oompa
132
+
133
+ # Check backends
134
+ ./swarm.bb check
135
+
136
+ # Create a spec
137
+ echo "Build a simple todo API with CRUD endpoints" > spec.md
138
+
139
+ # Run the swarm
140
+ ./swarm.bb swarm
141
+ ```
142
+
143
+ ## Install (npm)
144
+
145
+ ```bash
146
+ # Run without installing globally
147
+ npx @nbardy/oompa check
148
+ npx @nbardy/oompa swarm
149
+ ```
150
+
151
+ ```bash
152
+ # Or install globally
153
+ npm install -g @nbardy/oompa
154
+ oompa check
155
+ oompa swarm
156
+ ```
157
+
158
+ ## Commands
159
+
160
+ ```bash
161
+ oompa swarm [file] # Run from oompa.json (default)
162
+ oompa tasks # Show task status
163
+ oompa check # Check available backends
164
+ oompa cleanup # Remove worktrees
165
+ oompa help # Show all commands
166
+ ```
167
+
168
+ `./swarm.bb ...` works the same when running from a source checkout.
169
+
170
+ ## Worker Conversation Persistence
171
+
172
+ If `codex-persist` is available, each worker writes its prompt/response messages
173
+ to a per-worker session file for external UIs (for example worker panes in
174
+ `claude-web-view`).
175
+
176
+ - Session ID: random lowercase UUID per iteration (one file per iteration)
177
+ - First user message tag format: `[oompa:<swarmId>:<workerId>]`
178
+ - CWD passed to `codex-persist` is the worker worktree absolute path
179
+ - Codex workers use `codex-persist` writes; Claude workers use native `--session-id`
180
+
181
+ Resolution order for the CLI command:
182
+ 1. `CODEX_PERSIST_BIN` (if set)
183
+ 2. `codex-persist` on `PATH`
184
+ 3. `node ~/git/codex-persist/dist/cli.js`
185
+
186
+ ## Requirements
187
+
188
+ - Node.js 18+ (only for npm wrapper / npx usage)
189
+ - [Babashka](https://github.com/babashka/babashka) (bb)
190
+ - Git 2.5+ (for worktrees)
191
+ - One of:
192
+ - [Claude CLI](https://github.com/anthropics/claude-cli)
193
+ - [Codex CLI](https://github.com/openai/codex)
194
+
195
+ ## License
196
+
197
+ MIT
@@ -0,0 +1,293 @@
1
+ (ns agentnet.agent
2
+ "Unified agent execution abstraction.
3
+
4
+ Supports multiple agent backends (Codex, Claude) with consistent interface.
5
+ Each agent type has different CLI syntax but produces same output format.
6
+
7
+ Design:
8
+ - Agents are pure functions: Prompt -> AgentResult
9
+ - Side effects (file changes) happen in worktree
10
+ - Timeouts and retries handled at this layer
11
+ - Output parsing extracts structured feedback
12
+
13
+ Supported Backends:
14
+ :codex - OpenAI Codex CLI (codex exec)
15
+ :claude - Anthropic Claude CLI (claude -p)"
16
+ (:require [agentnet.schema :as schema]
17
+ [babashka.process :as process]
18
+ [clojure.java.io :as io]
19
+ [clojure.string :as str]))
20
+
21
+ ;; =============================================================================
22
+ ;; Function Specs
23
+ ;; =============================================================================
24
+
25
+ ;; invoke : AgentConfig, AgentRole, Prompt, Worktree -> AgentResult
26
+ ;; Execute agent with prompt in worktree context
27
+
28
+ ;; build-prompt : Task, Context, AgentRole -> Prompt
29
+ ;; Construct prompt from task, context, and role template
30
+
31
+ ;; parse-output : String, AgentRole -> ParsedOutput
32
+ ;; Extract structured data from agent output
33
+
34
+ ;; =============================================================================
35
+ ;; Types (documentation)
36
+ ;; =============================================================================
37
+
38
+ ;; AgentResult:
39
+ ;; {:exit int
40
+ ;; :stdout string
41
+ ;; :stderr string
42
+ ;; :duration-ms int
43
+ ;; :timed-out? boolean}
44
+
45
+ ;; ParsedOutput:
46
+ ;; {:verdict :approved|:needs-changes|:rejected
47
+ ;; :comments [string]
48
+ ;; :files-changed [string]
49
+ ;; :error string}
50
+
51
+ ;; =============================================================================
52
+ ;; Agent Backend Implementations
53
+ ;; =============================================================================
54
+
55
+ (defmulti build-command
56
+ "Build CLI command for agent type"
57
+ (fn [agent-type _config _prompt _cwd] agent-type))
58
+
59
+ (defmethod build-command :codex
60
+ [_ {:keys [model sandbox timeout-seconds]} prompt cwd]
61
+ (cond-> ["codex" "exec" "--full-auto" "--skip-git-repo-check"]
62
+ model (into ["--model" model])
63
+ cwd (into ["-C" cwd])
64
+ sandbox (into ["--sandbox" (name sandbox)])
65
+ true (conj "--" prompt)))
66
+
67
+ (defmethod build-command :claude
68
+ [_ {:keys [model timeout-seconds]} prompt cwd]
69
+ ;; Claude uses stdin for prompt via -p flag
70
+ (cond-> ["claude" "-p"]
71
+ model (into ["--model" model])
72
+ true (conj "--dangerously-skip-permissions")))
73
+
74
+ (defmethod build-command :default
75
+ [agent-type _ _ _]
76
+ (throw (ex-info (str "Unknown agent type: " agent-type)
77
+ {:agent-type agent-type})))
78
+
79
+ ;; =============================================================================
80
+ ;; Process Execution
81
+ ;; =============================================================================
82
+
83
+ (defn- now-ms []
84
+ (System/currentTimeMillis))
85
+
86
+ (defn- truncate [s limit]
87
+ (if (and s (> (count s) limit))
88
+ (str (subs s 0 limit) "...[truncated]")
89
+ s))
90
+
91
+ (defn- run-process
92
+ "Execute command with timeout, return AgentResult"
93
+ [{:keys [cmd cwd stdin timeout-ms]}]
94
+ (let [start (now-ms)
95
+ timeout (or timeout-ms 300000) ; 5 min default
96
+ opts (cond-> {:out :string
97
+ :err :string
98
+ :timeout timeout}
99
+ cwd (assoc :dir cwd)
100
+ stdin (assoc :in stdin))
101
+ result (try
102
+ (process/sh cmd opts)
103
+ (catch java.util.concurrent.TimeoutException _
104
+ {:exit -1 :out "" :err "Timeout exceeded" :timed-out true})
105
+ (catch Exception e
106
+ {:exit -1 :out "" :err (.getMessage e)}))]
107
+ {:exit (:exit result)
108
+ :stdout (truncate (:out result) 10000)
109
+ :stderr (truncate (:err result) 5000)
110
+ :duration-ms (- (now-ms) start)
111
+ :timed-out? (boolean (:timed-out result))}))
112
+
113
+ ;; =============================================================================
114
+ ;; Output Parsing
115
+ ;; =============================================================================
116
+
117
+ (defn- extract-verdict
118
+ "Extract review verdict from agent output"
119
+ [output]
120
+ (cond
121
+ (re-find #"(?i)\bAPPROVED\b" output) :approved
122
+ (re-find #"(?i)\bREJECTED\b" output) :rejected
123
+ (re-find #"(?i)\bNEEDS[_-]?CHANGES\b" output) :needs-changes
124
+ (re-find #"(?i)\bFIX\s*:" output) :needs-changes
125
+ (re-find #"(?i)\bVIOLATION\s*:" output) :needs-changes
126
+ :else nil))
127
+
128
+ (defn done-signal?
129
+ "Check if output contains __DONE__ signal"
130
+ [output]
131
+ (boolean (re-find #"__DONE__" (or output ""))))
132
+
133
+ (defn- extract-comments
134
+ "Extract bullet-point comments from output"
135
+ [output]
136
+ (->> (str/split-lines output)
137
+ (filter #(re-find #"^\s*[-*]\s+" %))
138
+ (map #(str/replace % #"^\s*[-*]\s+" ""))
139
+ (map str/trim)
140
+ (remove str/blank?)
141
+ vec))
142
+
143
+ (defn- extract-files-changed
144
+ "Extract list of files that were modified"
145
+ [output]
146
+ (->> (re-seq #"(?m)^[AMD]\s+(.+)$" output)
147
+ (map second)
148
+ vec))
149
+
150
+ (defn parse-output
151
+ "Parse agent output into structured format"
152
+ [output role]
153
+ (case role
154
+ :reviewer
155
+ {:verdict (or (extract-verdict output) :needs-changes)
156
+ :comments (extract-comments output)}
157
+
158
+ :proposer
159
+ {:files-changed (extract-files-changed output)
160
+ :comments (extract-comments output)}
161
+
162
+ ;; default
163
+ {:comments (extract-comments output)}))
164
+
165
+ ;; =============================================================================
166
+ ;; Main API
167
+ ;; =============================================================================
168
+
169
+ (defn invoke
170
+ "Execute agent with prompt in worktree context.
171
+
172
+ Arguments:
173
+ config - AgentConfig with :type, :model, :sandbox, :timeout-seconds
174
+ role - :proposer, :reviewer, or :cto
175
+ prompt - String prompt to send to agent
176
+ worktree - Worktree map with :path
177
+
178
+ Returns AgentResult with :exit, :stdout, :stderr, :duration-ms, :timed-out?"
179
+ [{:keys [type] :as config} role prompt worktree]
180
+ (schema/assert-valid schema/valid-agent-config? config "AgentConfig")
181
+ (let [cwd (:path worktree)
182
+ cmd (build-command type config prompt cwd)
183
+ ;; For Claude, prompt goes via stdin
184
+ stdin (when (= type :claude) prompt)
185
+ timeout-ms (* 1000 (or (:timeout-seconds config) 300))]
186
+ (run-process {:cmd cmd
187
+ :cwd cwd
188
+ :stdin stdin
189
+ :timeout-ms timeout-ms})))
190
+
191
+ (defn invoke-and-parse
192
+ "Execute agent and parse output into structured format"
193
+ [config role prompt worktree]
194
+ (let [result (invoke config role prompt worktree)
195
+ parsed (when (zero? (:exit result))
196
+ (parse-output (:stdout result) role))]
197
+ (assoc result :parsed parsed)))
198
+
199
+ ;; =============================================================================
200
+ ;; Prompt Building
201
+ ;; =============================================================================
202
+
203
+ (defn- load-template
204
+ "Load prompt template from config/prompts/"
205
+ [role]
206
+ (let [filename (str "config/prompts/" (name role) ".md")
207
+ f (io/file filename)]
208
+ (when (.exists f)
209
+ (slurp f))))
210
+
211
+ (defn load-custom-prompt
212
+ "Load a custom prompt file. Returns content or nil."
213
+ [path]
214
+ (when path
215
+ (let [f (io/file path)]
216
+ (when (.exists f)
217
+ (slurp f)))))
218
+
219
+ (defn- tokenize
220
+ "Replace {tokens} in template with values from context map"
221
+ [template tokens]
222
+ (reduce (fn [acc [k v]]
223
+ (str/replace acc
224
+ (re-pattern (java.util.regex.Pattern/quote
225
+ (str "{" (name k) "}")))
226
+ (str v)))
227
+ template
228
+ tokens))
229
+
230
+ (defn build-prompt
231
+ "Build prompt from task, context, and role.
232
+
233
+ Arguments:
234
+ task - Task map with :id, :summary, :targets
235
+ context - Context map with :queue_md, :recent_files_md, etc.
236
+ role - :proposer, :reviewer, or :cto
237
+ opts - {:custom-prompt \"path/to/prompt.md\"}
238
+
239
+ Returns prompt string ready for agent"
240
+ ([task context role] (build-prompt task context role {}))
241
+ ([task context role {:keys [custom-prompt]}]
242
+ (let [template (or (load-custom-prompt custom-prompt)
243
+ (load-template role)
244
+ (load-template :engineer)) ; fallback
245
+ tokens (merge context
246
+ {:task_id (:id task)
247
+ :summary (:summary task)
248
+ :targets (str/join ", " (or (:targets task) ["*"]))
249
+ :mode_hint (if (= role :reviewer) "review" "propose")})]
250
+ (if template
251
+ (tokenize template tokens)
252
+ ;; Fallback: simple prompt
253
+ (str "Task: " (:summary task) "\n"
254
+ "Targets: " (:targets task) "\n"
255
+ "Role: " (name role))))))
256
+
257
+ ;; =============================================================================
258
+ ;; Agent Health Check
259
+ ;; =============================================================================
260
+
261
+ (defn check-available
262
+ "Check if agent backend is available"
263
+ [agent-type]
264
+ (let [cmd (case agent-type
265
+ :codex ["codex" "--version"]
266
+ :claude ["claude" "--version"]
267
+ ["echo" "unknown"])]
268
+ (try
269
+ (let [{:keys [exit]} (process/sh cmd {:out :string :err :string})]
270
+ (zero? exit))
271
+ (catch Exception _
272
+ false))))
273
+
274
+ (defn select-backend
275
+ "Select first available backend from preference list"
276
+ [preferences]
277
+ (first (filter check-available preferences)))
278
+
279
+ ;; =============================================================================
280
+ ;; Convenience Wrappers
281
+ ;; =============================================================================
282
+
283
+ (defn propose!
284
+ "Run proposer agent on task in worktree"
285
+ [config task context worktree]
286
+ (let [prompt (build-prompt task context :proposer)]
287
+ (invoke-and-parse config :proposer prompt worktree)))
288
+
289
+ (defn review!
290
+ "Run reviewer agent on changes in worktree"
291
+ [config task context worktree]
292
+ (let [prompt (build-prompt task context :reviewer)]
293
+ (invoke-and-parse config :reviewer prompt worktree)))