@lovenyberg/ove 0.7.0 → 0.9.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.
@@ -0,0 +1,553 @@
1
+ import { describe, it, expect, beforeEach } from "bun:test";
2
+ import { Database } from "bun:sqlite";
3
+ import { createMessageHandler, createEventHandler, OVE_PERSONA, type HandlerDeps } from "./handlers";
4
+ import { TaskQueue } from "./queue";
5
+ import { SessionStore } from "./sessions";
6
+ import { ScheduleStore } from "./schedules";
7
+ import { RepoRegistry } from "./repo-registry";
8
+ import { TraceStore } from "./trace";
9
+ import type { IncomingMessage, IncomingEvent, EventAdapter } from "./adapters/types";
10
+ import type { AgentRunner } from "./runner";
11
+ import type { Config } from "./config";
12
+
13
+
14
+ // --- Helpers ---
15
+
16
+ function makeConfig(overrides?: Partial<Config>): Config {
17
+ return {
18
+ repos: { "my-app": { url: "git@github.com:org/my-app.git", defaultBranch: "main" } },
19
+ users: { "slack:U123": { name: "testuser", repos: ["my-app"] } },
20
+ claude: { maxTurns: 25 },
21
+ reposDir: "/tmp/test-repos",
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ function makeDeps(overrides?: Partial<HandlerDeps>): HandlerDeps {
27
+ const db = new Database(":memory:");
28
+ db.run("PRAGMA journal_mode = WAL");
29
+ const queue = new TaskQueue(db);
30
+ const sessions = new SessionStore(db);
31
+ const schedules = new ScheduleStore(db);
32
+ const repoRegistry = new RepoRegistry(db);
33
+ const trace = new TraceStore(db);
34
+
35
+ repoRegistry.upsert({
36
+ name: "my-app",
37
+ url: "git@github.com:org/my-app.git",
38
+ source: "config",
39
+ defaultBranch: "main",
40
+ });
41
+
42
+ const stubRunner: AgentRunner = {
43
+ name: "stub",
44
+ run: async (prompt: string) => ({
45
+ success: true,
46
+ output: "stub output",
47
+ durationMs: 100,
48
+ }),
49
+ };
50
+
51
+ return {
52
+ config: makeConfig(),
53
+ queue,
54
+ sessions,
55
+ schedules,
56
+ repoRegistry,
57
+ trace,
58
+ pendingReplies: new Map(),
59
+ pendingEventReplies: new Map(),
60
+ runningProcesses: new Map(),
61
+ getRunner: () => stubRunner,
62
+ getRunnerForRepo: () => stubRunner,
63
+ getRepoInfo: (name: string) => {
64
+ if (name === "my-app") return { url: "git@github.com:org/my-app.git", defaultBranch: "main" };
65
+ return null;
66
+ },
67
+ ...overrides,
68
+ };
69
+ }
70
+
71
+ function makeMessage(text: string, userId = "slack:U123", platform = "slack"): IncomingMessage & { replies: string[]; statuses: string[] } {
72
+ const replies: string[] = [];
73
+ const statuses: string[] = [];
74
+ return {
75
+ userId,
76
+ platform,
77
+ text,
78
+ replies,
79
+ statuses,
80
+ reply: async (t: string) => { replies.push(t); },
81
+ updateStatus: async (t: string) => { statuses.push(t); },
82
+ };
83
+ }
84
+
85
+ // --- Tests ---
86
+
87
+ describe("handleSetMode", () => {
88
+ let deps: HandlerDeps;
89
+
90
+ beforeEach(() => {
91
+ deps = makeDeps();
92
+ });
93
+
94
+ it("sets assistant mode and replies with Swedish flair", async () => {
95
+ const handler = createMessageHandler(deps);
96
+ const msg = makeMessage("mode assistant");
97
+
98
+ await handler(msg);
99
+
100
+ expect(deps.sessions.getMode("slack:U123")).toBe("assistant");
101
+ expect(msg.replies.length).toBe(1);
102
+ expect(msg.replies[0]).toContain("Assistant mode");
103
+ });
104
+
105
+ it("sets strict mode and replies accordingly", async () => {
106
+ const handler = createMessageHandler(deps);
107
+ deps.sessions.setMode("slack:U123", "assistant");
108
+
109
+ const msg = makeMessage("mode strict");
110
+ await handler(msg);
111
+
112
+ expect(deps.sessions.getMode("slack:U123")).toBe("strict");
113
+ expect(msg.replies.length).toBe(1);
114
+ expect(msg.replies[0]).toContain("code mode");
115
+ });
116
+
117
+ it("stores mode reply in session history", async () => {
118
+ const handler = createMessageHandler(deps);
119
+ const msg = makeMessage("mode assistant");
120
+
121
+ await handler(msg);
122
+
123
+ const history = deps.sessions.getHistory("slack:U123");
124
+ // Should have user message + assistant reply
125
+ expect(history.length).toBe(2);
126
+ expect(history[0].role).toBe("user");
127
+ expect(history[0].content).toBe("mode assistant");
128
+ expect(history[1].role).toBe("assistant");
129
+ expect(history[1].content).toContain("Assistant mode");
130
+ });
131
+
132
+ it("rejects invalid mode values with error message", async () => {
133
+ const handler = createMessageHandler(deps);
134
+ // parseMessage won't match "mode banana" as set-mode — it will be free-form.
135
+ // But we can still test via the handler by checking what parseMessage routes it to.
136
+ // Since parseMessage only matches "assistant" and "strict", "mode banana" becomes free-form.
137
+ // Let's verify the router rejects it at the parse level.
138
+ const { parseMessage } = await import("./router");
139
+ const parsed = parseMessage("mode banana");
140
+ expect(parsed.type).toBe("free-form"); // Not set-mode — router rejects invalid modes
141
+ });
142
+
143
+ it("natural language triggers assistant mode", async () => {
144
+ const handler = createMessageHandler(deps);
145
+ const msg = makeMessage("assistant mode");
146
+
147
+ await handler(msg);
148
+
149
+ expect(deps.sessions.getMode("slack:U123")).toBe("assistant");
150
+ expect(msg.replies[0]).toContain("Assistant mode");
151
+ });
152
+
153
+ it("natural language triggers strict mode", async () => {
154
+ const handler = createMessageHandler(deps);
155
+ deps.sessions.setMode("slack:U123", "assistant");
156
+
157
+ const msg = makeMessage("back to normal");
158
+ await handler(msg);
159
+
160
+ expect(deps.sessions.getMode("slack:U123")).toBe("strict");
161
+ expect(msg.replies[0]).toContain("code mode");
162
+ });
163
+ });
164
+
165
+ describe("getPersona (tested via createMessageHandler discuss path)", () => {
166
+ it("uses base OVE_PERSONA in strict mode (default)", async () => {
167
+ const deps = makeDeps({
168
+ config: makeConfig({
169
+ repos: {},
170
+ users: { "slack:U123": { name: "testuser", repos: [] } },
171
+ }),
172
+ });
173
+
174
+ // In strict mode, discuss prompt should contain OVE_PERSONA but NOT the addendum
175
+ let capturedPrompt = "";
176
+ const capturingRunner: AgentRunner = {
177
+ name: "capture",
178
+ run: async (prompt: string) => {
179
+ capturedPrompt = prompt;
180
+ return { success: true, output: "response", durationMs: 50 };
181
+ },
182
+ };
183
+ deps.getRunner = () => capturingRunner;
184
+
185
+ const handler = createMessageHandler(deps);
186
+ const msg = makeMessage("discuss testing strategies");
187
+ await handler(msg);
188
+
189
+ expect(capturedPrompt).toContain("grumpy");
190
+ expect(capturedPrompt).not.toContain("IMPORTANT MODE OVERRIDE");
191
+ });
192
+
193
+ it("appends ASSISTANT_ADDENDUM in assistant mode", async () => {
194
+ const deps = makeDeps({
195
+ config: makeConfig({
196
+ repos: {},
197
+ users: { "slack:U123": { name: "testuser", repos: [] } },
198
+ }),
199
+ });
200
+
201
+ let capturedPrompt = "";
202
+ const capturingRunner: AgentRunner = {
203
+ name: "capture",
204
+ run: async (prompt: string) => {
205
+ capturedPrompt = prompt;
206
+ return { success: true, output: "response", durationMs: 50 };
207
+ },
208
+ };
209
+ deps.getRunner = () => capturingRunner;
210
+
211
+ // Set assistant mode before sending the discuss message
212
+ deps.sessions.setMode("slack:U123", "assistant");
213
+
214
+ const handler = createMessageHandler(deps);
215
+ const msg = makeMessage("discuss testing strategies");
216
+ await handler(msg);
217
+
218
+ expect(capturedPrompt).toContain("grumpy");
219
+ expect(capturedPrompt).toContain("IMPORTANT MODE OVERRIDE");
220
+ expect(capturedPrompt).toContain("general-purpose assistant");
221
+ });
222
+ });
223
+
224
+ describe("createEventHandler reads mode for event.userId", () => {
225
+ it("uses assistant persona when event user is in assistant mode", async () => {
226
+ const deps = makeDeps();
227
+ let capturedPrompt = "";
228
+
229
+ // Track enqueued task prompts by intercepting the queue
230
+ const originalEnqueue = deps.queue.enqueue.bind(deps.queue);
231
+ deps.queue.enqueue = (input) => {
232
+ capturedPrompt = input.prompt;
233
+ return originalEnqueue(input);
234
+ };
235
+
236
+ // Set assistant mode for the user
237
+ deps.sessions.setMode("slack:U123", "assistant");
238
+
239
+ const handler = createEventHandler(deps);
240
+ const event: IncomingEvent = {
241
+ eventId: "evt-1",
242
+ userId: "slack:U123",
243
+ platform: "github",
244
+ source: { type: "pr", repo: "org/my-app", number: 5 },
245
+ text: "review PR #5 on my-app",
246
+ };
247
+
248
+ const adapter: EventAdapter = {
249
+ start: async () => {},
250
+ stop: async () => {},
251
+ respondToEvent: async () => {},
252
+ };
253
+
254
+ await handler(event, adapter);
255
+
256
+ expect(capturedPrompt).toContain("IMPORTANT MODE OVERRIDE");
257
+ expect(capturedPrompt).toContain("grumpy");
258
+ });
259
+
260
+ it("uses strict persona by default for event user", async () => {
261
+ const deps = makeDeps();
262
+ let capturedPrompt = "";
263
+
264
+ const originalEnqueue = deps.queue.enqueue.bind(deps.queue);
265
+ deps.queue.enqueue = (input) => {
266
+ capturedPrompt = input.prompt;
267
+ return originalEnqueue(input);
268
+ };
269
+
270
+ const handler = createEventHandler(deps);
271
+ const event: IncomingEvent = {
272
+ eventId: "evt-2",
273
+ userId: "slack:U123",
274
+ platform: "github",
275
+ source: { type: "pr", repo: "org/my-app", number: 5 },
276
+ text: "review PR #5 on my-app",
277
+ };
278
+
279
+ const adapter: EventAdapter = {
280
+ start: async () => {},
281
+ stop: async () => {},
282
+ respondToEvent: async () => {},
283
+ };
284
+
285
+ await handler(event, adapter);
286
+
287
+ expect(capturedPrompt).not.toContain("IMPORTANT MODE OVERRIDE");
288
+ expect(capturedPrompt).toContain("grumpy");
289
+ });
290
+ });
291
+
292
+ describe("integration: set mode then send message verifies prompt contains addendum", () => {
293
+ it("set mode → send discuss → prompt sent to runner has ASSISTANT_ADDENDUM", async () => {
294
+ const deps = makeDeps({
295
+ config: makeConfig({
296
+ repos: {},
297
+ users: { "slack:U123": { name: "testuser", repos: [] } },
298
+ }),
299
+ });
300
+
301
+ let capturedPrompt = "";
302
+ const capturingRunner: AgentRunner = {
303
+ name: "capture",
304
+ run: async (prompt: string) => {
305
+ capturedPrompt = prompt;
306
+ return { success: true, output: "Here's my help!", durationMs: 50 };
307
+ },
308
+ };
309
+ deps.getRunner = () => capturingRunner;
310
+
311
+ const handler = createMessageHandler(deps);
312
+
313
+ // Step 1: set assistant mode
314
+ const modeMsg = makeMessage("mode assistant");
315
+ await handler(modeMsg);
316
+ expect(deps.sessions.getMode("slack:U123")).toBe("assistant");
317
+
318
+ // Step 2: send a discuss-type message (routes to handleDiscuss which calls runner directly)
319
+ const chatMsg = makeMessage("discuss best Italian restaurants nearby");
320
+ await handler(chatMsg);
321
+
322
+ // The prompt sent to the runner should contain the addendum
323
+ expect(capturedPrompt).toContain("IMPORTANT MODE OVERRIDE");
324
+ expect(capturedPrompt).toContain("willing to help with ANY request");
325
+ expect(capturedPrompt).toContain("grumpy");
326
+ });
327
+
328
+ it("set strict mode → send discuss → prompt has NO addendum", async () => {
329
+ const deps = makeDeps({
330
+ config: makeConfig({
331
+ repos: {},
332
+ users: { "slack:U123": { name: "testuser", repos: [] } },
333
+ }),
334
+ });
335
+
336
+ let capturedPrompt = "";
337
+ const capturingRunner: AgentRunner = {
338
+ name: "capture",
339
+ run: async (prompt: string) => {
340
+ capturedPrompt = prompt;
341
+ return { success: true, output: "I only do code.", durationMs: 50 };
342
+ },
343
+ };
344
+ deps.getRunner = () => capturingRunner;
345
+
346
+ const handler = createMessageHandler(deps);
347
+
348
+ // Step 1: explicitly set strict mode (should be default, but be explicit)
349
+ const modeMsg = makeMessage("mode strict");
350
+ await handler(modeMsg);
351
+ expect(deps.sessions.getMode("slack:U123")).toBe("strict");
352
+
353
+ // Step 2: send a discuss-type message
354
+ const chatMsg = makeMessage("discuss architecture patterns");
355
+ await handler(chatMsg);
356
+
357
+ expect(capturedPrompt).not.toContain("IMPORTANT MODE OVERRIDE");
358
+ expect(capturedPrompt).toContain("grumpy");
359
+ });
360
+
361
+ it("task enqueue for repo-bound message includes addendum in assistant mode", async () => {
362
+ const deps = makeDeps();
363
+
364
+ // Set assistant mode
365
+ deps.sessions.setMode("slack:U123", "assistant");
366
+
367
+ const handler = createMessageHandler(deps);
368
+ const msg = makeMessage("review PR #10 on my-app");
369
+ await handler(msg);
370
+
371
+ // The task was enqueued — check the prompt in pendingReplies
372
+ // pendingReplies maps taskId → msg, but we need the prompt from the queue
373
+ const tasks = deps.queue.listByUser("slack:U123", 1);
374
+ expect(tasks.length).toBe(1);
375
+ expect(tasks[0].prompt).toContain("IMPORTANT MODE OVERRIDE");
376
+ expect(tasks[0].prompt).toContain("grumpy");
377
+ });
378
+ });
379
+
380
+ describe("mode toggle round-trip", () => {
381
+ it("toggling between modes reflects correctly in persona", async () => {
382
+ const deps = makeDeps({
383
+ config: makeConfig({
384
+ repos: {},
385
+ users: { "slack:U123": { name: "testuser", repos: [] } },
386
+ }),
387
+ });
388
+
389
+ const prompts: string[] = [];
390
+ const capturingRunner: AgentRunner = {
391
+ name: "capture",
392
+ run: async (prompt: string) => {
393
+ prompts.push(prompt);
394
+ return { success: true, output: "ok", durationMs: 50 };
395
+ },
396
+ };
397
+ deps.getRunner = () => capturingRunner;
398
+
399
+ const handler = createMessageHandler(deps);
400
+
401
+ // Start in strict → discuss → no addendum
402
+ const msg1 = makeMessage("discuss testing");
403
+ await handler(msg1);
404
+ expect(prompts[0]).not.toContain("IMPORTANT MODE OVERRIDE");
405
+
406
+ // Toggle to assistant
407
+ const modeMsg = makeMessage("mode assistant");
408
+ await handler(modeMsg);
409
+
410
+ // Discuss again → addendum present
411
+ const msg2 = makeMessage("discuss testing again");
412
+ await handler(msg2);
413
+ expect(prompts[1]).toContain("IMPORTANT MODE OVERRIDE");
414
+
415
+ // Toggle back to strict
416
+ const strictMsg = makeMessage("mode strict");
417
+ await handler(strictMsg);
418
+
419
+ // Discuss again → no addendum
420
+ const msg3 = makeMessage("discuss testing one more time");
421
+ await handler(msg3);
422
+ expect(prompts[2]).not.toContain("IMPORTANT MODE OVERRIDE");
423
+ });
424
+ });
425
+
426
+ describe("handleSetMode stores user message in history", () => {
427
+ it("user's mode command appears in history even for invalid routing", async () => {
428
+ const deps = makeDeps();
429
+ const handler = createMessageHandler(deps);
430
+
431
+ // Valid mode command
432
+ const msg = makeMessage("mode assistant");
433
+ await handler(msg);
434
+
435
+ const history = deps.sessions.getHistory("slack:U123");
436
+ expect(history[0].role).toBe("user");
437
+ expect(history[0].content).toBe("mode assistant");
438
+ });
439
+ });
440
+
441
+ describe("OVE_PERSONA export", () => {
442
+ it("OVE_PERSONA contains expected personality traits", () => {
443
+ expect(OVE_PERSONA).toContain("grumpy");
444
+ expect(OVE_PERSONA).toContain("Swedish");
445
+ expect(OVE_PERSONA).toContain("Ove");
446
+ expect(OVE_PERSONA).toContain("meticulous");
447
+ });
448
+ });
449
+
450
+ describe("clear command resets mode to strict", () => {
451
+ it("after setting assistant mode, clear resets mode back to strict", async () => {
452
+ const deps = makeDeps();
453
+ const handler = createMessageHandler(deps);
454
+
455
+ // Set assistant mode
456
+ const modeMsg = makeMessage("mode assistant");
457
+ await handler(modeMsg);
458
+ expect(deps.sessions.getMode("slack:U123")).toBe("assistant");
459
+
460
+ // Send clear command
461
+ const clearMsg = makeMessage("clear");
462
+ await handler(clearMsg);
463
+
464
+ // Mode should be back to strict (default) since clear deletes the mode row
465
+ expect(deps.sessions.getMode("slack:U123")).toBe("strict");
466
+ expect(clearMsg.replies[0]).toContain("Conversation cleared");
467
+ });
468
+
469
+ it("after clear, discuss prompt no longer contains ASSISTANT_ADDENDUM", async () => {
470
+ const deps = makeDeps({
471
+ config: makeConfig({
472
+ repos: {},
473
+ users: { "slack:U123": { name: "testuser", repos: [] } },
474
+ }),
475
+ });
476
+
477
+ let capturedPrompt = "";
478
+ const capturingRunner: AgentRunner = {
479
+ name: "capture",
480
+ run: async (prompt: string) => {
481
+ capturedPrompt = prompt;
482
+ return { success: true, output: "response", durationMs: 50 };
483
+ },
484
+ };
485
+ deps.getRunner = () => capturingRunner;
486
+
487
+ const handler = createMessageHandler(deps);
488
+
489
+ // Set assistant mode
490
+ const modeMsg = makeMessage("mode assistant");
491
+ await handler(modeMsg);
492
+ expect(deps.sessions.getMode("slack:U123")).toBe("assistant");
493
+
494
+ // Clear session
495
+ const clearMsg = makeMessage("clear");
496
+ await handler(clearMsg);
497
+
498
+ // Send discuss message — should NOT have addendum since mode was cleared
499
+ const chatMsg = makeMessage("discuss something fun");
500
+ await handler(chatMsg);
501
+
502
+ expect(capturedPrompt).toContain("grumpy");
503
+ expect(capturedPrompt).not.toContain("IMPORTANT MODE OVERRIDE");
504
+ });
505
+ });
506
+
507
+ describe("multi-user mode isolation", () => {
508
+ it("User A in assistant mode does not affect User B in strict mode", async () => {
509
+ const deps = makeDeps({
510
+ config: makeConfig({
511
+ repos: {},
512
+ users: {
513
+ "slack:U123": { name: "userA", repos: [] },
514
+ "slack:U456": { name: "userB", repos: [] },
515
+ },
516
+ }),
517
+ });
518
+
519
+ const prompts: { user: string; prompt: string }[] = [];
520
+ const capturingRunner: AgentRunner = {
521
+ name: "capture",
522
+ run: async (prompt: string) => {
523
+ prompts.push({ user: "unknown", prompt });
524
+ return { success: true, output: "response", durationMs: 50 };
525
+ },
526
+ };
527
+ deps.getRunner = () => capturingRunner;
528
+
529
+ const handler = createMessageHandler(deps);
530
+
531
+ // User A sets assistant mode
532
+ const modeMsg = makeMessage("mode assistant", "slack:U123");
533
+ await handler(modeMsg);
534
+ expect(deps.sessions.getMode("slack:U123")).toBe("assistant");
535
+
536
+ // User B stays in default (strict) mode
537
+ expect(deps.sessions.getMode("slack:U456")).toBe("strict");
538
+
539
+ // User A sends discuss message — should have ASSISTANT_ADDENDUM
540
+ const msgA = makeMessage("discuss best pizza places", "slack:U123");
541
+ await handler(msgA);
542
+
543
+ expect(prompts[0].prompt).toContain("IMPORTANT MODE OVERRIDE");
544
+ expect(prompts[0].prompt).toContain("grumpy");
545
+
546
+ // User B sends discuss message — should NOT have ASSISTANT_ADDENDUM
547
+ const msgB = makeMessage("discuss architecture patterns", "slack:U456");
548
+ await handler(msgB);
549
+
550
+ expect(prompts[1].prompt).not.toContain("IMPORTANT MODE OVERRIDE");
551
+ expect(prompts[1].prompt).toContain("grumpy");
552
+ });
553
+ });
package/src/handlers.ts CHANGED
@@ -6,7 +6,7 @@ import { parseSchedule } from "./schedule-parser";
6
6
  import { logger } from "./logger";
7
7
  import type { Config } from "./config";
8
8
  import type { TaskQueue, Task } from "./queue";
9
- import type { SessionStore } from "./sessions";
9
+ import type { SessionStore, UserMode } from "./sessions";
10
10
  import type { ScheduleStore } from "./schedules";
11
11
  import type { RepoRegistry } from "./repo-registry";
12
12
  import type { IncomingMessage, EventAdapter, IncomingEvent } from "./adapters/types";
@@ -42,6 +42,19 @@ Keep the personality subtle in code output — don't let it interfere with code
42
42
 
43
43
  export { OVE_PERSONA };
44
44
 
45
+ const ASSISTANT_ADDENDUM = `IMPORTANT MODE OVERRIDE: You are currently in "assistant mode". The user has asked you to be a general-purpose assistant. While you keep your Ove personality (grumble, be direct, sprinkle Swedish), you are now willing to help with ANY request — not just code. This includes:
46
+ - Sending reminders, drafting emails/messages
47
+ - Answering general knowledge questions
48
+ - Helping with personal tasks, recommendations
49
+ - Anything the user asks
50
+
51
+ You still grumble about it ("Fan, nu ska jag vara sekreterare också...") but you DO the task. If you genuinely cannot do something (no tool/integration available), explain what would be needed rather than just refusing.`;
52
+
53
+ function getPersona(userId: string, deps: HandlerDeps): string {
54
+ const mode = deps.sessions.getMode(userId);
55
+ return mode === "assistant" ? OVE_PERSONA + "\n\n" + ASSISTANT_ADDENDUM : OVE_PERSONA;
56
+ }
57
+
45
58
  const PLATFORM_FORMAT_HINTS: Record<string, string> = {
46
59
  telegram: "Format output for Telegram: use *bold* for emphasis, `code` for inline code, ```code blocks```. No markdown tables. Use simple bulleted lists with • instead. Keep it concise.",
47
60
  slack: "Format output for Slack: use *bold*, no markdown tables. Use simple bulleted lists with • instead. Keep it concise.",
@@ -147,6 +160,26 @@ async function handleClear(msg: IncomingMessage, deps: HandlerDeps) {
147
160
  await msg.reply("Conversation cleared.");
148
161
  }
149
162
 
163
+ async function handleSetMode(msg: IncomingMessage, args: Record<string, any>, deps: HandlerDeps) {
164
+ const mode = args.mode;
165
+ if (mode !== "assistant" && mode !== "strict") {
166
+ await msg.reply(`Unknown mode "${String(mode)}". Use "mode assistant" or "mode strict".`);
167
+ return;
168
+ }
169
+ try {
170
+ deps.sessions.setMode(msg.userId, mode);
171
+ } catch (err) {
172
+ logger.error("failed to set user mode", { userId: msg.userId, mode, error: String(err) });
173
+ await msg.reply("Failed to save mode. Try again.");
174
+ return;
175
+ }
176
+ const reply = mode === "assistant"
177
+ ? "Mja, fine. Assistant mode. Jag hjälper dig med vad du vill. Men klaga inte om resultatet."
178
+ : "Back to code mode. Äntligen. Riktigt arbete.";
179
+ await msg.reply(reply);
180
+ deps.sessions.addMessage(msg.userId, "assistant", reply);
181
+ }
182
+
150
183
  async function handleStatus(msg: IncomingMessage, deps: HandlerDeps) {
151
184
  const userTasks = deps.queue.listByUser(msg.userId, 5);
152
185
  const running = userTasks.find((t) => t.status === "running");
@@ -199,6 +232,8 @@ async function handleHelp(msg: IncomingMessage, deps: HandlerDeps) {
199
232
  "• tasks — see running and pending tasks",
200
233
  "• cancel <id> — kill a running or pending task",
201
234
  "• trace [task-id] — see what happened step by step",
235
+ "• mode assistant — I'll (reluctantly) help with anything",
236
+ "• mode strict — back to code-only (default)",
202
237
  "• status / history / clear",
203
238
  "• <task> every day/weekday at <time> [on <repo>] — schedule a recurring task",
204
239
  "• list schedules — see your scheduled tasks",
@@ -383,7 +418,7 @@ async function handleSchedule(msg: IncomingMessage, parsedRepo: string | undefin
383
418
  }
384
419
 
385
420
  async function handleDiscuss(msg: IncomingMessage, parsed: ParsedMessage, history: { role: string; content: string }[], deps: HandlerDeps) {
386
- const prompt = buildContextualPrompt(parsed, history, OVE_PERSONA);
421
+ const prompt = buildContextualPrompt(parsed, history, getPersona(msg.userId, deps));
387
422
 
388
423
  await msg.updateStatus("Thinking...");
389
424
 
@@ -413,13 +448,14 @@ async function handleDiscuss(msg: IncomingMessage, parsed: ParsedMessage, histor
413
448
 
414
449
  async function handleCreateProject(msg: IncomingMessage, parsed: ParsedMessage, history: { role: string; content: string }[], deps: HandlerDeps) {
415
450
  const projectName = parsed.args.name;
416
- const prompt = buildContextualPrompt(parsed, history, OVE_PERSONA);
451
+ const prompt = buildContextualPrompt(parsed, history, getPersona(msg.userId, deps));
417
452
 
418
453
  const taskId = deps.queue.enqueue({
419
454
  userId: msg.userId,
420
455
  repo: projectName,
421
456
  prompt,
422
457
  taskType: "create-project",
458
+ priority: parsed.priority,
423
459
  });
424
460
 
425
461
  deps.pendingReplies.set(taskId, msg);
@@ -485,12 +521,13 @@ async function handleTaskMessage(msg: IncomingMessage, parsed: ParsedMessage, de
485
521
  }
486
522
 
487
523
  const history = deps.sessions.getHistory(msg.userId, 6);
488
- const prompt = buildContextualPrompt(parsed, history, OVE_PERSONA);
524
+ const prompt = buildContextualPrompt(parsed, history, getPersona(msg.userId, deps));
489
525
 
490
526
  const taskId = deps.queue.enqueue({
491
527
  userId: msg.userId,
492
528
  repo: parsed.repo,
493
529
  prompt,
530
+ priority: parsed.priority,
494
531
  });
495
532
 
496
533
  deps.pendingReplies.set(taskId, msg);
@@ -499,7 +536,7 @@ async function handleTaskMessage(msg: IncomingMessage, parsed: ParsedMessage, de
499
536
  if (stats.running > 0 || stats.pending > 1) {
500
537
  await msg.reply(`Queued — ${stats.pending} task${stats.pending > 1 ? "s" : ""} ahead.`);
501
538
  }
502
- logger.info("task enqueued", { taskId, repo: parsed.repo, type: parsed.type });
539
+ logger.info("task enqueued", { taskId, repo: parsed.repo, type: parsed.type, priority: parsed.priority });
503
540
  }
504
541
 
505
542
  export function createMessageHandler(deps: HandlerDeps): (msg: IncomingMessage) => Promise<void> {
@@ -515,6 +552,7 @@ export function createMessageHandler(deps: HandlerDeps): (msg: IncomingMessage)
515
552
  "help": () => handleHelp(msg, deps),
516
553
  "list-tasks": () => handleListTasks(msg, deps),
517
554
  "cancel-task": () => handleCancelTask(msg, parsed.args, deps),
555
+ "set-mode": () => handleSetMode(msg, parsed.args, deps),
518
556
  "trace": () => handleTrace(msg, parsed.args, deps),
519
557
  "list-schedules": () => handleListSchedules(msg, deps),
520
558
  "remove-schedule": () => handleRemoveSchedule(msg, parsed.args, deps),
@@ -580,14 +618,15 @@ export function createEventHandler(deps: HandlerDeps): (event: IncomingEvent, ad
580
618
  return;
581
619
  }
582
620
 
583
- const prompt = buildContextualPrompt(parsed, [], OVE_PERSONA);
621
+ const prompt = buildContextualPrompt(parsed, [], getPersona(event.userId, deps));
584
622
  const taskId = deps.queue.enqueue({
585
623
  userId: event.userId,
586
624
  repo: parsed.repo,
587
625
  prompt,
626
+ priority: parsed.priority,
588
627
  });
589
628
 
590
629
  deps.pendingEventReplies.set(taskId, { adapter, event });
591
- logger.info("event task enqueued", { taskId, eventId: event.eventId, repo: parsed.repo });
630
+ logger.info("event task enqueued", { taskId, eventId: event.eventId, repo: parsed.repo, priority: parsed.priority });
592
631
  };
593
632
  }
package/src/index.ts CHANGED
@@ -213,6 +213,7 @@ async function main() {
213
213
  if (ea instanceof HttpApiAdapter) {
214
214
  ea.setMessageHandler(handleMessage);
215
215
  ea.setAdapters(adapters, eventAdapters);
216
+ ea.setRunningProcesses(runningProcesses);
216
217
  }
217
218
  await ea.start((event) => handleEvent(event, ea));
218
219
  }