@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/README.md +13 -1
- package/deploy/ove.service +16 -0
- package/package.json +1 -1
- package/src/config.test.ts +6 -0
- package/src/config.ts +5 -1
- package/src/flows.test.ts +1 -1
- package/src/handlers.ts +75 -70
- package/.dockerignore +0 -7
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/pages.yml +0 -33
- package/.github/workflows/publish.yml +0 -45
- package/Dockerfile +0 -37
- package/bun.lock +0 -503
- package/bunfig.toml +0 -2
- package/docker-compose.yml +0 -15
- package/docs/examples.md +0 -247
- package/docs/favicon.ico +0 -0
- package/docs/index.html +0 -980
- package/docs/logo.png +0 -0
- package/docs/plans/2026-02-21-codex-runner-design.md +0 -51
- package/docs/plans/2026-02-21-codex-runner-plan.md +0 -475
- package/docs/plans/2026-02-22-repo-autodiscovery-design.md +0 -98
- package/docs/plans/2026-02-22-repo-autodiscovery-plan.md +0 -826
- package/docs/plans/2026-02-23-conversation-repo-memory.md +0 -272
- package/docs/plans/2026-02-25-landing-page-harmonization-design.md +0 -89
- package/docs/plans/2026-02-25-landing-page-harmonization-plan.md +0 -604
- package/logo.png +0 -0
- package/public/logo.png +0 -0
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.
|