@lovenyberg/ove 0.2.2 → 0.4.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/.claude/skills/create-issue/SKILL.md +24 -0
- package/.claude/skills/review-pr/SKILL.md +37 -0
- package/.claude/skills/ship/SKILL.md +22 -0
- package/.env.example +5 -0
- package/CLAUDE.md +20 -0
- package/README.md +93 -17
- package/docs/examples.md +11 -52
- package/docs/index.html +40 -2
- package/docs/plans/2026-02-23-conversation-repo-memory.md +272 -0
- package/package.json +1 -1
- package/public/favicon.ico +0 -0
- package/public/index.html +424 -36
- package/public/logo.png +0 -0
- package/public/status.html +519 -0
- package/public/trace.html +973 -0
- package/src/adapters/cli.ts +16 -1
- package/src/adapters/debounce.test.ts +57 -0
- package/src/adapters/debounce.ts +42 -0
- package/src/adapters/discord.ts +18 -13
- package/src/adapters/github.ts +38 -1
- package/src/adapters/http.test.ts +7 -1
- package/src/adapters/http.ts +227 -47
- package/src/adapters/slack.ts +18 -13
- package/src/adapters/telegram.ts +22 -20
- package/src/adapters/types.ts +11 -0
- package/src/adapters/whatsapp.ts +40 -2
- package/src/config.ts +4 -1
- package/src/flows.test.ts +126 -0
- package/src/handlers.ts +571 -0
- package/src/index.ts +85 -649
- package/src/queue.ts +44 -11
- package/src/repo-registry.ts +23 -5
- package/src/router.ts +8 -2
- package/src/runners/claude.ts +27 -6
- package/src/runners/codex.ts +23 -2
- package/src/schedules.ts +13 -3
- package/src/sessions.ts +8 -2
- package/src/trace.ts +71 -0
- package/src/worker.ts +235 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Conversation-Aware Repo Resolution
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** When a user sends a follow-up message without specifying a repo, Ove should remember which repo they were talking about from recent conversation context.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Two-layer fallback added to the repo resolution chain in `handleTaskMessage`: (1) check the user's most recent task for its repo (cheap DB query), (2) feed conversation history to the LLM resolver prompt so it can infer the repo from context. No new tables or schema changes.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Bun, bun:sqlite, bun:test, TypeScript
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Add `lastRepoForUser` helper to handlers
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Modify: `src/handlers.ts:396-446`
|
|
17
|
+
|
|
18
|
+
**Step 1: Write the failing test**
|
|
19
|
+
|
|
20
|
+
Add to `src/flows.test.ts`:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
describe("Conversation-aware repo resolution", () => {
|
|
24
|
+
it("derives lastRepo from recent task history", async () => {
|
|
25
|
+
const db = new Database(":memory:");
|
|
26
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
27
|
+
const queue = new TaskQueue(db);
|
|
28
|
+
|
|
29
|
+
// Simulate a completed task on "iris"
|
|
30
|
+
const taskId = queue.enqueue({
|
|
31
|
+
userId: "telegram:U1",
|
|
32
|
+
repo: "iris",
|
|
33
|
+
prompt: "check the roadmap",
|
|
34
|
+
});
|
|
35
|
+
queue.dequeue();
|
|
36
|
+
queue.complete(taskId, "Here's the roadmap...");
|
|
37
|
+
|
|
38
|
+
// The user's last task repo should be "iris"
|
|
39
|
+
const recent = queue.listByUser("telegram:U1", 1);
|
|
40
|
+
expect(recent.length).toBe(1);
|
|
41
|
+
expect(recent[0].repo).toBe("iris");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Step 2: Run test to verify it passes**
|
|
47
|
+
|
|
48
|
+
Run: `bun test src/flows.test.ts --test-name-pattern "derives lastRepo"`
|
|
49
|
+
Expected: PASS (this is testing existing queue behavior — sanity check)
|
|
50
|
+
|
|
51
|
+
**Step 3: Add lastRepo fallback in handleTaskMessage**
|
|
52
|
+
|
|
53
|
+
In `src/handlers.ts`, inside `handleTaskMessage`, after the single-repo check (line ~411) and before the LLM resolver (line ~418), add:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// Check last task's repo as context fallback
|
|
57
|
+
const recentTasks = deps.queue.listByUser(msg.userId, 1);
|
|
58
|
+
const lastRepo = recentTasks[0]?.repo;
|
|
59
|
+
if (lastRepo && repoNames.includes(lastRepo)) {
|
|
60
|
+
parsed.repo = lastRepo;
|
|
61
|
+
logger.info("repo resolved from recent task", { resolved: lastRepo, userText: parsed.rawText.slice(0, 80) });
|
|
62
|
+
} else {
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This goes inside the `else if (repoNames.length > 1)` block, between the single-repo check and the LLM resolver. The full block becomes:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
if (repoNames.length === 1) {
|
|
69
|
+
parsed.repo = repoNames[0];
|
|
70
|
+
} else if (repoNames.length === 0) {
|
|
71
|
+
const reply = "No repos discovered yet. Set one up with `init repo <name> <git-url>` or configure GitHub sync.";
|
|
72
|
+
await msg.reply(reply);
|
|
73
|
+
return;
|
|
74
|
+
} else {
|
|
75
|
+
// Try last task's repo first (cheap)
|
|
76
|
+
const recentTasks = deps.queue.listByUser(msg.userId, 1);
|
|
77
|
+
const lastRepo = recentTasks[0]?.repo;
|
|
78
|
+
if (lastRepo && repoNames.includes(lastRepo)) {
|
|
79
|
+
parsed.repo = lastRepo;
|
|
80
|
+
logger.info("repo resolved from recent task", { resolved: lastRepo, userText: parsed.rawText.slice(0, 80) });
|
|
81
|
+
} else {
|
|
82
|
+
// Resolve repo via LLM call (existing code, moved into else branch)
|
|
83
|
+
const repoList = repoNames.join(", ");
|
|
84
|
+
// ... existing LLM resolver code ...
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Step 4: Run all tests**
|
|
90
|
+
|
|
91
|
+
Run: `bun test src/flows.test.ts`
|
|
92
|
+
Expected: PASS
|
|
93
|
+
|
|
94
|
+
**Step 5: Commit**
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git add src/handlers.ts src/flows.test.ts
|
|
98
|
+
git commit -m "feat: resolve repo from recent task history for follow-up messages"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### Task 2: Enhance LLM resolver with conversation history
|
|
104
|
+
|
|
105
|
+
**Files:**
|
|
106
|
+
- Modify: `src/handlers.ts:418-433`
|
|
107
|
+
|
|
108
|
+
**Step 1: Write the failing test**
|
|
109
|
+
|
|
110
|
+
Add to `src/flows.test.ts`:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
describe("LLM resolver with conversation history", () => {
|
|
114
|
+
it("buildResolverPrompt includes conversation history", () => {
|
|
115
|
+
const history = [
|
|
116
|
+
{ role: "user", content: "check the roadmap on iris", timestamp: "" },
|
|
117
|
+
{ role: "assistant", content: "Here's the iris roadmap...", timestamp: "" },
|
|
118
|
+
{ role: "user", content: "what about tomorrow's plan", timestamp: "" },
|
|
119
|
+
];
|
|
120
|
+
const currentText = "what about tomorrow's plan";
|
|
121
|
+
const repoList = "iris, docs, my-app";
|
|
122
|
+
|
|
123
|
+
// Test the prompt format includes history
|
|
124
|
+
const historyContext = history.length > 0
|
|
125
|
+
? "Recent conversation:\n" + history.map(m => `${m.role}: ${m.content}`).join("\n") + "\n\n"
|
|
126
|
+
: "";
|
|
127
|
+
const prompt = `You are a repo-name resolver. ${historyContext}The user's latest message:\n"${currentText}"\n\nAvailable repos: ${repoList}\n\nRespond with ONLY the repo name that best matches their request. Nothing else — just the exact repo name from the list. If you cannot determine which repo, respond with "UNKNOWN".`;
|
|
128
|
+
|
|
129
|
+
expect(prompt).toContain("Recent conversation:");
|
|
130
|
+
expect(prompt).toContain("check the roadmap on iris");
|
|
131
|
+
expect(prompt).toContain("Here's the iris roadmap");
|
|
132
|
+
expect(prompt).toContain("what about tomorrow's plan");
|
|
133
|
+
expect(prompt).toContain("iris, docs, my-app");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Step 2: Run test to verify it passes**
|
|
139
|
+
|
|
140
|
+
Run: `bun test src/flows.test.ts --test-name-pattern "buildResolverPrompt"`
|
|
141
|
+
Expected: PASS (testing the prompt format)
|
|
142
|
+
|
|
143
|
+
**Step 3: Update LLM resolver in handleTaskMessage to include history**
|
|
144
|
+
|
|
145
|
+
In `src/handlers.ts`, modify the LLM resolver block. Change the resolver prompt from:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const resolvePrompt = `You are a repo-name resolver. The user said:\n"${parsed.rawText}"\n\nAvailable repos: ${repoList}\n\nRespond with ONLY the repo name that best matches their request. Nothing else — just the exact repo name from the list. If you cannot determine which repo, respond with "UNKNOWN".`;
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
To:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const history = deps.sessions.getHistory(msg.userId, 6);
|
|
155
|
+
const historyContext = history.length > 1
|
|
156
|
+
? "Recent conversation:\n" + history.slice(0, -1).map(m => `${m.role}: ${m.content}`).join("\n") + "\n\n"
|
|
157
|
+
: "";
|
|
158
|
+
const resolvePrompt = `You are a repo-name resolver. ${historyContext}The user's latest message:\n"${parsed.rawText}"\n\nAvailable repos: ${repoList}\n\nRespond with ONLY the repo name that best matches their request. Consider the conversation context if the current message doesn't mention a specific repo. Nothing else — just the exact repo name from the list. If you cannot determine which repo, respond with "UNKNOWN".`;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Step 4: Run all tests**
|
|
162
|
+
|
|
163
|
+
Run: `bun test src/flows.test.ts`
|
|
164
|
+
Expected: PASS
|
|
165
|
+
|
|
166
|
+
**Step 5: Commit**
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
git add src/handlers.ts src/flows.test.ts
|
|
170
|
+
git commit -m "feat: feed conversation history to LLM repo resolver"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### Task 3: Integration test for the full follow-up flow
|
|
176
|
+
|
|
177
|
+
**Files:**
|
|
178
|
+
- Modify: `src/flows.test.ts`
|
|
179
|
+
|
|
180
|
+
**Step 1: Write the integration test**
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
describe("Full follow-up conversation flow", () => {
|
|
184
|
+
it("follow-up message without repo uses last task's repo", () => {
|
|
185
|
+
const db = new Database(":memory:");
|
|
186
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
187
|
+
const queue = new TaskQueue(db);
|
|
188
|
+
const sessions = new SessionStore(db);
|
|
189
|
+
|
|
190
|
+
// Simulate conversation: user talked about iris
|
|
191
|
+
sessions.addMessage("telegram:U1", "user", "check the roadmap on iris");
|
|
192
|
+
sessions.addMessage("telegram:U1", "assistant", "Here's the iris roadmap...");
|
|
193
|
+
sessions.addMessage("telegram:U1", "user", "what about tomorrow's plan");
|
|
194
|
+
|
|
195
|
+
// Simulate a completed task on iris
|
|
196
|
+
const taskId = queue.enqueue({
|
|
197
|
+
userId: "telegram:U1",
|
|
198
|
+
repo: "iris",
|
|
199
|
+
prompt: "check the roadmap",
|
|
200
|
+
});
|
|
201
|
+
queue.dequeue();
|
|
202
|
+
queue.complete(taskId, "Here's the roadmap...");
|
|
203
|
+
|
|
204
|
+
// Now a follow-up: "what about tomorrow" — no repo mentioned
|
|
205
|
+
const parsed = parseMessage("what about tomorrow's plan");
|
|
206
|
+
expect(parsed.type).toBe("free-form");
|
|
207
|
+
expect(parsed.repo).toBeUndefined(); // Router can't find repo in text
|
|
208
|
+
|
|
209
|
+
// But the last task was on iris
|
|
210
|
+
const recentTasks = queue.listByUser("telegram:U1", 1);
|
|
211
|
+
expect(recentTasks[0].repo).toBe("iris");
|
|
212
|
+
|
|
213
|
+
// And the conversation history mentions iris
|
|
214
|
+
const history = sessions.getHistory("telegram:U1", 6);
|
|
215
|
+
expect(history.some(m => m.content.includes("iris"))).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("explicit repo in message overrides last task repo", () => {
|
|
219
|
+
const db = new Database(":memory:");
|
|
220
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
221
|
+
const queue = new TaskQueue(db);
|
|
222
|
+
|
|
223
|
+
// Last task was on iris
|
|
224
|
+
const taskId = queue.enqueue({
|
|
225
|
+
userId: "telegram:U1",
|
|
226
|
+
repo: "iris",
|
|
227
|
+
prompt: "check roadmap",
|
|
228
|
+
});
|
|
229
|
+
queue.dequeue();
|
|
230
|
+
queue.complete(taskId, "done");
|
|
231
|
+
|
|
232
|
+
// But new message explicitly says "on docs"
|
|
233
|
+
const parsed = parseMessage("check the tests on docs");
|
|
234
|
+
expect(parsed.repo).toBe("docs"); // Regex hint takes priority
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Step 2: Run test**
|
|
240
|
+
|
|
241
|
+
Run: `bun test src/flows.test.ts --test-name-pattern "follow-up"`
|
|
242
|
+
Expected: PASS
|
|
243
|
+
|
|
244
|
+
**Step 3: Commit**
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
git add src/flows.test.ts
|
|
248
|
+
git commit -m "test: add integration tests for conversation-aware repo resolution"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### Task 4: Verify full test suite passes
|
|
254
|
+
|
|
255
|
+
**Step 1: Run all tests**
|
|
256
|
+
|
|
257
|
+
Run: `bun test`
|
|
258
|
+
Expected: All tests pass
|
|
259
|
+
|
|
260
|
+
**Step 2: Manual verification (optional)**
|
|
261
|
+
|
|
262
|
+
If running locally, send a message to Ove via Telegram:
|
|
263
|
+
1. "check the roadmap on iris" → should resolve to iris
|
|
264
|
+
2. "what about tomorrow's plan" → should resolve to iris (from recent task)
|
|
265
|
+
3. "check tests on docs" → should resolve to docs (explicit override)
|
|
266
|
+
|
|
267
|
+
**Step 3: Final commit if needed**
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
git add -A
|
|
271
|
+
git commit -m "feat: conversation-aware repo resolution for follow-up messages"
|
|
272
|
+
```
|
package/package.json
CHANGED
|
Binary file
|