@lovenyberg/ove 0.5.2 → 0.6.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/docs/logo.png DELETED
Binary file
@@ -1,51 +0,0 @@
1
- # Codex Runner Design
2
-
3
- Add OpenAI Codex CLI as a second agent runner alongside Claude Code CLI.
4
-
5
- ## Config
6
-
7
- Global default runner + per-repo override:
8
-
9
- ```json
10
- {
11
- "runner": { "name": "claude" },
12
- "repos": {
13
- "my-app": {
14
- "url": "git@github.com:user/my-app.git",
15
- "defaultBranch": "main",
16
- "runner": { "name": "codex", "model": "o3" }
17
- }
18
- }
19
- }
20
- ```
21
-
22
- `RunnerConfig`: `{ name: "claude" | "codex"; model?: string }`. Defaults to `"claude"` if omitted.
23
-
24
- ## New Types
25
-
26
- `RunnerConfig` added to `config.ts` on both `Config` (global) and `RepoConfig` (per-repo). `RunOptions` gets optional `model` field.
27
-
28
- ## CodexRunner (`src/runners/codex.ts`)
29
-
30
- Implements `AgentRunner`. Spawns `codex exec --json --yolo --skip-git-repo-check --ephemeral -C <workDir> "prompt"`.
31
-
32
- Key differences from ClaudeRunner:
33
- - No `--max-turns` (exec mode runs one turn, unlimited tool calls)
34
- - No `--mcp-config` flag (Codex reads MCP from `~/.codex/config.toml`)
35
- - JSONL output format (one JSON object per line) instead of Claude's stream-json
36
- - Uses `CODEX_API_KEY` / `OPENAI_API_KEY` env vars
37
-
38
- JSONL event mapping to `StatusEvent`:
39
- - `item.completed` + `agent_message` -> `{ kind: "text" }` + final output
40
- - `item.started` + `command_execution` -> `{ kind: "tool", tool: "shell" }`
41
- - `item.started` + `file_change` -> `{ kind: "tool", tool: "file_change" }`
42
- - `item.started` + `mcp_tool_call` -> `{ kind: "tool", tool: <name> }`
43
- - `turn.failed` -> error output
44
-
45
- ## Runner Selection (`index.ts`)
46
-
47
- Factory function creates runners by name. Per-task resolution: repo config -> global default -> "claude". Runners cached by name.
48
-
49
- ## Unchanged
50
-
51
- `AgentRunner` interface, `RunResult`, `StatusEvent`, queue, router, adapters — all untouched.
@@ -1,475 +0,0 @@
1
- # Codex Runner Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Add OpenAI Codex CLI as a second agent runner, selectable per-repo or globally.
6
-
7
- **Architecture:** New `CodexRunner` class implements the existing `AgentRunner` interface. Config gains a `runner` field (global + per-repo). A factory function in `index.ts` resolves which runner to use per task.
8
-
9
- **Tech Stack:** Bun + TypeScript, `codex` CLI (npm: `@openai/codex`), JSONL stream parsing.
10
-
11
- ---
12
-
13
- ### Task 1: Add `RunnerConfig` type and `model` to `RunOptions`
14
-
15
- **Files:**
16
- - Modify: `src/runner.ts:1-4` (add model to RunOptions)
17
- - Modify: `src/config.ts:1-36` (add RunnerConfig, add to Config and RepoConfig)
18
-
19
- **Step 1: Add `model` to `RunOptions` in `src/runner.ts`**
20
-
21
- ```typescript
22
- export interface RunOptions {
23
- maxTurns: number;
24
- mcpConfigPath?: string;
25
- model?: string;
26
- }
27
- ```
28
-
29
- **Step 2: Add `RunnerConfig` and wire it into config types in `src/config.ts`**
30
-
31
- Add the type:
32
-
33
- ```typescript
34
- export interface RunnerConfig {
35
- name: string;
36
- model?: string;
37
- }
38
- ```
39
-
40
- Add `runner?: RunnerConfig` to `RepoConfig`:
41
-
42
- ```typescript
43
- export interface RepoConfig {
44
- url: string;
45
- defaultBranch: string;
46
- runner?: RunnerConfig;
47
- }
48
- ```
49
-
50
- Add `runner?: RunnerConfig` to `Config`:
51
-
52
- ```typescript
53
- export interface Config {
54
- repos: Record<string, RepoConfig>;
55
- users: Record<string, UserConfig>;
56
- claude: {
57
- maxTurns: number;
58
- };
59
- reposDir: string;
60
- mcpServers?: Record<string, McpServerConfig>;
61
- cron?: CronTaskConfig[];
62
- runner?: RunnerConfig;
63
- }
64
- ```
65
-
66
- **Step 3: Parse `runner` in `loadConfig`**
67
-
68
- In the `loadConfig` function, add to the return object:
69
-
70
- ```typescript
71
- runner: raw.runner,
72
- ```
73
-
74
- **Step 4: Preserve `runner` in `saveConfig`**
75
-
76
- In `saveConfig`, add to the merged object:
77
-
78
- ```typescript
79
- if (config.runner) merged.runner = config.runner;
80
- ```
81
-
82
- **Step 5: Run tests**
83
-
84
- Run: `cd /home/love/code/seenthis/dev-agent && bun test`
85
- Expected: All existing tests pass (no tests break from adding optional fields).
86
-
87
- **Step 6: Commit**
88
-
89
- ```bash
90
- git add src/runner.ts src/config.ts
91
- git commit -m "feat: add RunnerConfig type and model to RunOptions"
92
- ```
93
-
94
- ---
95
-
96
- ### Task 2: Create `CodexRunner`
97
-
98
- **Files:**
99
- - Create: `src/runners/codex.ts`
100
- - Create: `src/runners/codex.test.ts`
101
-
102
- **Step 1: Write the failing test in `src/runners/codex.test.ts`**
103
-
104
- ```typescript
105
- import { describe, it, expect } from "bun:test";
106
- import { CodexRunner, summarizeCodexItem } from "./codex";
107
-
108
- describe("CodexRunner", () => {
109
- const runner = new CodexRunner();
110
-
111
- it("has correct name", () => {
112
- expect(runner.name).toBe("codex");
113
- });
114
-
115
- it("builds correct args for a prompt", () => {
116
- const args = runner.buildArgs("fix the bug", "/tmp/work", {
117
- maxTurns: 25,
118
- });
119
- expect(args).toContain("exec");
120
- expect(args).toContain("--json");
121
- expect(args).toContain("--dangerously-bypass-approvals-and-sandbox");
122
- expect(args).toContain("--skip-git-repo-check");
123
- expect(args).toContain("--ephemeral");
124
- expect(args).toContain("-C");
125
- expect(args).toContain("/tmp/work");
126
- expect(args).toContain("fix the bug");
127
- });
128
-
129
- it("includes model flag when provided", () => {
130
- const args = runner.buildArgs("test", "/tmp/work", {
131
- maxTurns: 25,
132
- model: "o3",
133
- });
134
- expect(args).toContain("-m");
135
- expect(args).toContain("o3");
136
- });
137
-
138
- it("omits model flag when not provided", () => {
139
- const args = runner.buildArgs("test", "/tmp/work", { maxTurns: 25 });
140
- expect(args).not.toContain("-m");
141
- });
142
-
143
- it("ignores mcpConfigPath (not supported by codex CLI)", () => {
144
- const args = runner.buildArgs("test", "/tmp/work", {
145
- maxTurns: 25,
146
- mcpConfigPath: "/tmp/mcp.json",
147
- });
148
- expect(args).not.toContain("--mcp-config");
149
- expect(args).not.toContain("/tmp/mcp.json");
150
- });
151
- });
152
-
153
- describe("summarizeCodexItem", () => {
154
- it("summarizes command_execution", () => {
155
- expect(
156
- summarizeCodexItem({ type: "command_execution", command: "bun test" })
157
- ).toEqual({ tool: "shell", input: "bun test" });
158
- });
159
-
160
- it("summarizes file_change with paths", () => {
161
- const result = summarizeCodexItem({
162
- type: "file_change",
163
- changes: [
164
- { path: "src/a.ts", kind: "update" },
165
- { path: "src/b.ts", kind: "add" },
166
- ],
167
- });
168
- expect(result).toEqual({ tool: "file_change", input: "src/a.ts, src/b.ts" });
169
- });
170
-
171
- it("summarizes mcp_tool_call", () => {
172
- const result = summarizeCodexItem({
173
- type: "mcp_tool_call",
174
- tool: "search",
175
- arguments: '{"q":"hello"}',
176
- });
177
- expect(result).toEqual({ tool: "search", input: '{"q":"hello"}' });
178
- });
179
-
180
- it("returns null for agent_message", () => {
181
- expect(
182
- summarizeCodexItem({ type: "agent_message", text: "done" })
183
- ).toBeNull();
184
- });
185
-
186
- it("returns null for unknown types", () => {
187
- expect(summarizeCodexItem({ type: "reasoning", text: "thinking" })).toBeNull();
188
- });
189
- });
190
- ```
191
-
192
- **Step 2: Run tests to verify they fail**
193
-
194
- Run: `cd /home/love/code/seenthis/dev-agent && bun test src/runners/codex.test.ts`
195
- Expected: FAIL — module not found.
196
-
197
- **Step 3: Write `src/runners/codex.ts`**
198
-
199
- ```typescript
200
- import type {
201
- AgentRunner,
202
- RunOptions,
203
- RunResult,
204
- StatusCallback,
205
- } from "../runner";
206
- import { logger } from "../logger";
207
- import { which } from "bun";
208
- import { realpathSync } from "node:fs";
209
-
210
- export function summarizeCodexItem(
211
- item: any
212
- ): { tool: string; input: string } | null {
213
- if (!item) return null;
214
- switch (item.type) {
215
- case "command_execution":
216
- return { tool: "shell", input: item.command || "" };
217
- case "file_change": {
218
- const paths = (item.changes || [])
219
- .map((c: any) => c.path)
220
- .filter(Boolean)
221
- .join(", ");
222
- return { tool: "file_change", input: paths };
223
- }
224
- case "mcp_tool_call":
225
- return {
226
- tool: item.tool || "mcp",
227
- input:
228
- typeof item.arguments === "string"
229
- ? item.arguments
230
- : JSON.stringify(item.arguments || "").slice(0, 80),
231
- };
232
- default:
233
- return null;
234
- }
235
- }
236
-
237
- export class CodexRunner implements AgentRunner {
238
- name = "codex";
239
- private codexPath: string;
240
-
241
- constructor() {
242
- const found = which("codex");
243
- this.codexPath = found ? realpathSync(found) : "codex";
244
- }
245
-
246
- buildArgs(prompt: string, workDir: string, opts: RunOptions): string[] {
247
- const args = [
248
- "exec",
249
- "--json",
250
- "--dangerously-bypass-approvals-and-sandbox",
251
- "--skip-git-repo-check",
252
- "--ephemeral",
253
- "-C",
254
- workDir,
255
- ];
256
- if (opts.model) args.push("-m", opts.model);
257
- args.push(prompt);
258
- return args;
259
- }
260
-
261
- async run(
262
- prompt: string,
263
- workDir: string,
264
- opts: RunOptions,
265
- onStatus?: StatusCallback
266
- ): Promise<RunResult> {
267
- const args = this.buildArgs(prompt, workDir, opts);
268
- const startTime = Date.now();
269
- logger.info("starting codex task", {
270
- workDir,
271
- model: opts.model,
272
- codexPath: this.codexPath,
273
- });
274
-
275
- const proc = Bun.spawn([this.codexPath, ...args], {
276
- cwd: workDir,
277
- stdout: "pipe",
278
- stderr: "pipe",
279
- });
280
-
281
- let lastAgentMessage: string | null = null;
282
- let errorMessage: string | null = null;
283
- const decoder = new TextDecoder();
284
- const reader = proc.stdout.getReader();
285
-
286
- try {
287
- let buffer = "";
288
- while (true) {
289
- const { done, value } = await reader.read();
290
- if (done) break;
291
- buffer += decoder.decode(value, { stream: true });
292
- const lines = buffer.split("\n");
293
- buffer = lines.pop() || "";
294
- for (const line of lines) {
295
- if (!line.trim()) continue;
296
- try {
297
- const event = JSON.parse(line);
298
- if (
299
- event.type === "item.completed" &&
300
- event.item?.type === "agent_message"
301
- ) {
302
- lastAgentMessage = event.item.text || "";
303
- if (onStatus) onStatus({ kind: "text", text: lastAgentMessage });
304
- }
305
- if (event.type === "item.started" && event.item) {
306
- const summary = summarizeCodexItem(event.item);
307
- if (summary && onStatus) {
308
- onStatus({ kind: "tool", ...summary });
309
- }
310
- }
311
- if (event.type === "turn.failed") {
312
- errorMessage = event.error?.message || "Turn failed";
313
- }
314
- } catch {}
315
- }
316
- }
317
- } finally {
318
- reader.releaseLock();
319
- }
320
-
321
- const exitCode = await proc.exited;
322
- const durationMs = Date.now() - startTime;
323
-
324
- if (exitCode !== 0) {
325
- const stderr = await new Response(proc.stderr).text();
326
- const output = errorMessage || stderr || "Codex task failed";
327
- logger.error("codex task failed", { exitCode, output, durationMs });
328
- return { success: false, output, durationMs };
329
- }
330
-
331
- const finalOutput =
332
- lastAgentMessage || "Task completed (no output)";
333
- logger.info("codex task completed", { durationMs });
334
- return { success: true, output: finalOutput, durationMs };
335
- }
336
- }
337
- ```
338
-
339
- **Step 4: Run tests to verify they pass**
340
-
341
- Run: `cd /home/love/code/seenthis/dev-agent && bun test src/runners/codex.test.ts`
342
- Expected: All tests PASS.
343
-
344
- **Step 5: Run all tests**
345
-
346
- Run: `cd /home/love/code/seenthis/dev-agent && bun test`
347
- Expected: All tests pass.
348
-
349
- **Step 6: Commit**
350
-
351
- ```bash
352
- git add src/runners/codex.ts src/runners/codex.test.ts
353
- git commit -m "feat: add CodexRunner for OpenAI Codex CLI"
354
- ```
355
-
356
- ---
357
-
358
- ### Task 3: Add runner factory and per-task runner selection in `index.ts`
359
-
360
- **Files:**
361
- - Modify: `src/index.ts:5` (add CodexRunner import)
362
- - Modify: `src/index.ts:45` (replace hardcoded runner with factory)
363
- - Modify: `src/index.ts:262-278` (discuss mode — resolve runner)
364
- - Modify: `src/index.ts:429-487` (processTask — resolve runner per repo)
365
-
366
- **Step 1: Add import and runner factory**
367
-
368
- Add import at top of `src/index.ts`:
369
-
370
- ```typescript
371
- import { CodexRunner } from "./runners/codex";
372
- ```
373
-
374
- Replace line 45 (`const runner: AgentRunner = new ClaudeRunner();`) with:
375
-
376
- ```typescript
377
- const runners = new Map<string, AgentRunner>();
378
-
379
- function getRunner(name: string = "claude"): AgentRunner {
380
- let r = runners.get(name);
381
- if (!r) {
382
- switch (name) {
383
- case "codex":
384
- r = new CodexRunner();
385
- break;
386
- case "claude":
387
- default:
388
- r = new ClaudeRunner();
389
- break;
390
- }
391
- runners.set(name, r);
392
- }
393
- return r;
394
- }
395
-
396
- function getRunnerForRepo(repo: string): AgentRunner {
397
- const repoRunner = config.repos[repo]?.runner;
398
- const globalRunner = config.runner;
399
- const name = repoRunner?.name || globalRunner?.name || "claude";
400
- return getRunner(name);
401
- }
402
-
403
- function getRunnerOptsForRepo(repo: string, baseOpts: RunOptions): RunOptions {
404
- const repoRunner = config.repos[repo]?.runner;
405
- const globalRunner = config.runner;
406
- const model = repoRunner?.model || globalRunner?.model;
407
- return model ? { ...baseOpts, model } : baseOpts;
408
- }
409
- ```
410
-
411
- **Step 2: Update discuss mode (around line 269)**
412
-
413
- Change `runner.run(` to use the factory. The discuss mode doesn't have a specific repo, so use the global default:
414
-
415
- ```typescript
416
- const discussRunner = getRunner(config.runner?.name);
417
- const result = await discussRunner.run(
418
- ```
419
-
420
- **Step 3: Update `processTask` (around line 472)**
421
-
422
- Replace the `runner.run(` call with per-repo resolution:
423
-
424
- ```typescript
425
- const taskRunner = getRunnerForRepo(task.repo);
426
- const runOpts = getRunnerOptsForRepo(task.repo, {
427
- maxTurns: config.claude.maxTurns,
428
- mcpConfigPath,
429
- });
430
-
431
- const result = await taskRunner.run(
432
- task.prompt,
433
- workDir,
434
- runOpts,
435
- (event: StatusEvent) => {
436
- ```
437
-
438
- **Step 4: Update the startup log (around line 559)**
439
-
440
- Change `runner: runner.name` to show the default runner:
441
-
442
- ```typescript
443
- logger.info("ove starting", { chatAdapters: adapters.length, eventAdapters: eventAdapters.length, runner: config.runner?.name || "claude" });
444
- ```
445
-
446
- **Step 5: Run all tests**
447
-
448
- Run: `cd /home/love/code/seenthis/dev-agent && bun test`
449
- Expected: All tests pass.
450
-
451
- **Step 6: Commit**
452
-
453
- ```bash
454
- git add src/index.ts
455
- git commit -m "feat: add runner factory with per-repo runner selection"
456
- ```
457
-
458
- ---
459
-
460
- ### Task 4: Verify end-to-end
461
-
462
- **Step 1: Check TypeScript compilation**
463
-
464
- Run: `cd /home/love/code/seenthis/dev-agent && bun build src/index.ts --no-bundle --outdir /tmp/ove-check 2>&1 | head -20`
465
- Expected: No type errors.
466
-
467
- **Step 2: Run full test suite**
468
-
469
- Run: `cd /home/love/code/seenthis/dev-agent && bun test`
470
- Expected: All tests pass.
471
-
472
- **Step 3: Verify codex binary is available (optional)**
473
-
474
- Run: `which codex`
475
- Expected: Path to codex binary, or empty if not installed. (Runner gracefully falls back to `"codex"` string — will fail at runtime with a clear error if not installed.)
@@ -1,98 +0,0 @@
1
- # Auto-Discovery Repo Management Design
2
-
3
- ## Goal
4
-
5
- Scale Ove from manually configured repos to auto-discovering 50-100+ repos via GitHub, with on-demand cloning and Claude-powered repo resolution.
6
-
7
- ## Repo Storage
8
-
9
- Move repo registry from config.json to SQLite. New `repos` table:
10
-
11
- ```sql
12
- CREATE TABLE repos (
13
- name TEXT PRIMARY KEY,
14
- url TEXT NOT NULL,
15
- owner TEXT,
16
- default_branch TEXT DEFAULT 'main',
17
- source TEXT NOT NULL, -- "github-sync" | "manual" | "config"
18
- excluded INTEGER DEFAULT 0,
19
- last_synced_at TEXT
20
- );
21
- ```
22
-
23
- ## GitHub Sync
24
-
25
- On startup and every 30 min (configurable), run `gh repo list` to discover repos. New repos inserted, existing ones updated. Non-blocking — Ove starts immediately, sync runs in background.
26
-
27
- Config adds optional `github` section:
28
-
29
- ```json
30
- {
31
- "github": {
32
- "syncInterval": 1800000,
33
- "orgs": ["seenthis-ab", "jacksoncage"]
34
- }
35
- }
36
- ```
37
-
38
- If `orgs` omitted, syncs all repos the `gh` user has access to.
39
-
40
- ## User Access
41
-
42
- Wildcard support: `"repos": ["*"]` means access to all discovered repos. Existing per-repo lists still work.
43
-
44
- ## Repo Resolution
45
-
46
- When user doesn't specify a repo explicitly:
47
-
48
- 1. Router regex — explicit `on <repo>` works as fast path
49
- 2. Claude resolves — inject user's repo list into the prompt, Claude picks the right repo or asks
50
-
51
- Repo list injected as: `Available repos: repo-a, repo-b, ...`
52
-
53
- ## Config Changes
54
-
55
- `config.json` repos become overrides only:
56
-
57
- ```json
58
- {
59
- "repos": {
60
- "infra-salming-ai": {
61
- "runner": { "name": "codex" },
62
- "defaultBranch": "develop"
63
- },
64
- "old-legacy-thing": {
65
- "excluded": true
66
- }
67
- },
68
- "users": {
69
- "telegram:8518556027": { "name": "love", "repos": ["*"] }
70
- },
71
- "claude": { "maxTurns": 10 },
72
- "github": {
73
- "syncInterval": 1800000,
74
- "orgs": ["seenthis-ab", "jacksoncage"]
75
- }
76
- }
77
- ```
78
-
79
- Repos no longer need `url` — auto-discovered repos have it in SQLite. Only specify overrides (custom branch, runner, exclusion). `url` still works for non-GitHub repos.
80
-
81
- ## Clone Strategy
82
-
83
- On-demand — clone only when a task first targets a repo. Existing `cloneIfNeeded` handles this. No change needed.
84
-
85
- ## Migration
86
-
87
- On first run, existing config.json repos get inserted into SQLite with `source: "config"`. No breaking change.
88
-
89
- ## Components
90
-
91
- - `src/repo-registry.ts` — new SQLite-backed repo store (sync, getAll, getByName, isExcluded)
92
- - `src/config.ts` — add github config, update types, wildcard auth
93
- - `src/router.ts` — remove single-repo fallback
94
- - `src/index.ts` — wire up registry, inject repo list into prompts, start background sync
95
-
96
- ## Unchanged
97
-
98
- Queue, runners, adapters, worktrees, task processing — all untouched.