@mandipadk7/kavi 0.1.1 → 0.1.3
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 +21 -5
- package/dist/adapters/claude.js +131 -8
- package/dist/adapters/shared.js +19 -3
- package/dist/daemon.js +689 -4
- package/dist/git.js +198 -2
- package/dist/main.js +327 -68
- package/dist/paths.js +1 -1
- package/dist/reviews.js +159 -0
- package/dist/rpc.js +262 -0
- package/dist/session.js +25 -0
- package/dist/task-artifacts.js +35 -2
- package/dist/tui.js +1960 -83
- package/package.json +4 -10
package/dist/daemon.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { consumeCommands } from "./command-queue.js";
|
|
2
5
|
import { buildPeerMessages as buildClaudePeerMessages, runClaudeTask } from "./adapters/claude.js";
|
|
3
6
|
import { buildPeerMessages as buildCodexPeerMessages, runCodexTask } from "./adapters/codex.js";
|
|
7
|
+
import { buildDecisionReplay } from "./adapters/shared.js";
|
|
8
|
+
import { listApprovalRequests, resolveApprovalRequest } from "./approvals.js";
|
|
4
9
|
import { addDecisionRecord, upsertPathClaim } from "./decision-ledger.js";
|
|
5
|
-
import { listWorktreeChangedPaths } from "./git.js";
|
|
10
|
+
import { getWorktreeDiffReview, listWorktreeChangedPaths } from "./git.js";
|
|
6
11
|
import { nowIso } from "./paths.js";
|
|
12
|
+
import { addReviewReply, addReviewNote, autoResolveReviewNotesForCompletedTask, linkReviewFollowUpTask, reviewNotesForTask, setReviewNoteStatus, updateReviewNote } from "./reviews.js";
|
|
7
13
|
import { buildAdHocTask, buildKickoffTasks } from "./router.js";
|
|
8
|
-
import { loadSessionRecord, recordEvent, saveSessionRecord } from "./session.js";
|
|
9
|
-
import { saveTaskArtifact } from "./task-artifacts.js";
|
|
14
|
+
import { loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord } from "./session.js";
|
|
15
|
+
import { loadTaskArtifact, saveTaskArtifact } from "./task-artifacts.js";
|
|
10
16
|
export class KaviDaemon {
|
|
11
17
|
paths;
|
|
12
18
|
session;
|
|
@@ -15,6 +21,10 @@ export class KaviDaemon {
|
|
|
15
21
|
processing = false;
|
|
16
22
|
interval = null;
|
|
17
23
|
stopResolver = null;
|
|
24
|
+
rpcServer = null;
|
|
25
|
+
clients = new Set();
|
|
26
|
+
subscribers = new Set();
|
|
27
|
+
mutationQueue = Promise.resolve();
|
|
18
28
|
constructor(paths){
|
|
19
29
|
this.paths = paths;
|
|
20
30
|
}
|
|
@@ -28,6 +38,7 @@ export class KaviDaemon {
|
|
|
28
38
|
await recordEvent(this.paths, this.session.id, "daemon.started", {
|
|
29
39
|
daemonPid: process.pid
|
|
30
40
|
});
|
|
41
|
+
await this.startRpcServer();
|
|
31
42
|
this.running = true;
|
|
32
43
|
void this.tick();
|
|
33
44
|
this.interval = setInterval(()=>{
|
|
@@ -37,6 +48,636 @@ export class KaviDaemon {
|
|
|
37
48
|
this.stopResolver = resolve;
|
|
38
49
|
});
|
|
39
50
|
}
|
|
51
|
+
async startRpcServer() {
|
|
52
|
+
await fs.mkdir(path.dirname(this.paths.socketPath), {
|
|
53
|
+
recursive: true
|
|
54
|
+
});
|
|
55
|
+
await fs.rm(this.paths.socketPath, {
|
|
56
|
+
force: true
|
|
57
|
+
}).catch(()=>{});
|
|
58
|
+
this.rpcServer = net.createServer((socket)=>{
|
|
59
|
+
this.clients.add(socket);
|
|
60
|
+
socket.setEncoding("utf8");
|
|
61
|
+
let buffer = "";
|
|
62
|
+
socket.on("data", (chunk)=>{
|
|
63
|
+
buffer += chunk;
|
|
64
|
+
while(true){
|
|
65
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
66
|
+
if (newlineIndex === -1) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
70
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
71
|
+
if (!line) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
let request;
|
|
75
|
+
try {
|
|
76
|
+
request = JSON.parse(line);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
this.writeRpc(socket, {
|
|
79
|
+
id: "parse-error",
|
|
80
|
+
error: {
|
|
81
|
+
message: error instanceof Error ? error.message : "Unable to parse RPC payload."
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
void this.handleRpcRequest(socket, request);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const cleanup = ()=>{
|
|
90
|
+
this.clients.delete(socket);
|
|
91
|
+
this.subscribers.delete(socket);
|
|
92
|
+
};
|
|
93
|
+
socket.on("error", cleanup);
|
|
94
|
+
socket.on("close", cleanup);
|
|
95
|
+
});
|
|
96
|
+
await new Promise((resolve, reject)=>{
|
|
97
|
+
this.rpcServer?.once("error", reject);
|
|
98
|
+
this.rpcServer?.listen(this.paths.socketPath, ()=>{
|
|
99
|
+
this.rpcServer?.off("error", reject);
|
|
100
|
+
resolve();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
writeRpc(socket, response, onWritten) {
|
|
105
|
+
socket.write(`${JSON.stringify(response)}\n`, onWritten);
|
|
106
|
+
}
|
|
107
|
+
writeNotification(socket, notification) {
|
|
108
|
+
socket.write(`${JSON.stringify(notification)}\n`);
|
|
109
|
+
}
|
|
110
|
+
async handleRpcRequest(socket, request) {
|
|
111
|
+
try {
|
|
112
|
+
const dispatch = await this.dispatchRpc(socket, request.method, request.params ?? {});
|
|
113
|
+
this.writeRpc(socket, {
|
|
114
|
+
id: request.id,
|
|
115
|
+
result: dispatch.result
|
|
116
|
+
}, ()=>{
|
|
117
|
+
if (dispatch.shutdownAfterResponse) {
|
|
118
|
+
void this.stopFromRpc();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
this.writeRpc(socket, {
|
|
123
|
+
id: request.id,
|
|
124
|
+
error: {
|
|
125
|
+
message: error instanceof Error ? error.message : String(error)
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async dispatchRpc(socket, method, params) {
|
|
131
|
+
switch(method){
|
|
132
|
+
case "ping":
|
|
133
|
+
return {
|
|
134
|
+
result: {
|
|
135
|
+
ok: true,
|
|
136
|
+
sessionId: this.session.id
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
case "snapshot":
|
|
140
|
+
return {
|
|
141
|
+
result: await this.buildSnapshot()
|
|
142
|
+
};
|
|
143
|
+
case "subscribe":
|
|
144
|
+
this.subscribers.add(socket);
|
|
145
|
+
return {
|
|
146
|
+
result: await this.buildSnapshot()
|
|
147
|
+
};
|
|
148
|
+
case "kickoff":
|
|
149
|
+
await this.kickoffFromRpc(params);
|
|
150
|
+
return {
|
|
151
|
+
result: {
|
|
152
|
+
ok: true
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
case "enqueueTask":
|
|
156
|
+
await this.enqueueRpcTask(params);
|
|
157
|
+
return {
|
|
158
|
+
result: {
|
|
159
|
+
ok: true
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
case "shutdown":
|
|
163
|
+
return {
|
|
164
|
+
result: {
|
|
165
|
+
ok: true
|
|
166
|
+
},
|
|
167
|
+
shutdownAfterResponse: true
|
|
168
|
+
};
|
|
169
|
+
case "resolveApproval":
|
|
170
|
+
await this.resolveApprovalFromRpc(params);
|
|
171
|
+
return {
|
|
172
|
+
result: {
|
|
173
|
+
ok: true
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
case "taskArtifact":
|
|
177
|
+
return {
|
|
178
|
+
result: await this.getTaskArtifactFromRpc(params)
|
|
179
|
+
};
|
|
180
|
+
case "events":
|
|
181
|
+
return {
|
|
182
|
+
result: await this.getEventsFromRpc(params)
|
|
183
|
+
};
|
|
184
|
+
case "worktreeDiff":
|
|
185
|
+
return {
|
|
186
|
+
result: await this.getWorktreeDiffFromRpc(params)
|
|
187
|
+
};
|
|
188
|
+
case "notifyExternalUpdate":
|
|
189
|
+
await this.publishSnapshot(typeof params.reason === "string" && params.reason.trim() ? params.reason.trim() : "external.update");
|
|
190
|
+
return {
|
|
191
|
+
result: {
|
|
192
|
+
ok: true
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
case "addReviewNote":
|
|
196
|
+
await this.addReviewNoteFromRpc(params);
|
|
197
|
+
return {
|
|
198
|
+
result: {
|
|
199
|
+
ok: true
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
case "updateReviewNote":
|
|
203
|
+
await this.updateReviewNoteFromRpc(params);
|
|
204
|
+
return {
|
|
205
|
+
result: {
|
|
206
|
+
ok: true
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
case "addReviewReply":
|
|
210
|
+
await this.addReviewReplyFromRpc(params);
|
|
211
|
+
return {
|
|
212
|
+
result: {
|
|
213
|
+
ok: true
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
case "setReviewNoteStatus":
|
|
217
|
+
await this.setReviewNoteStatusFromRpc(params);
|
|
218
|
+
return {
|
|
219
|
+
result: {
|
|
220
|
+
ok: true
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
case "enqueueReviewFollowUp":
|
|
224
|
+
await this.enqueueReviewFollowUpFromRpc(params);
|
|
225
|
+
return {
|
|
226
|
+
result: {
|
|
227
|
+
ok: true
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
default:
|
|
231
|
+
throw new Error(`Unknown RPC method: ${method}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async buildSnapshot() {
|
|
235
|
+
const session = await loadSessionRecord(this.paths);
|
|
236
|
+
const events = await readRecentEvents(this.paths, 30);
|
|
237
|
+
const approvals = await listApprovalRequests(this.paths, {
|
|
238
|
+
includeResolved: true
|
|
239
|
+
});
|
|
240
|
+
const worktreeDiffs = await Promise.all(session.worktrees.map(async (worktree)=>({
|
|
241
|
+
agent: worktree.agent,
|
|
242
|
+
paths: await listWorktreeChangedPaths(worktree.path, session.baseCommit).catch(()=>[])
|
|
243
|
+
})));
|
|
244
|
+
return {
|
|
245
|
+
session,
|
|
246
|
+
events,
|
|
247
|
+
approvals,
|
|
248
|
+
worktreeDiffs
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async runMutation(fn) {
|
|
252
|
+
const previous = this.mutationQueue;
|
|
253
|
+
let release = ()=>{};
|
|
254
|
+
this.mutationQueue = new Promise((resolve)=>{
|
|
255
|
+
release = resolve;
|
|
256
|
+
});
|
|
257
|
+
await previous;
|
|
258
|
+
try {
|
|
259
|
+
return await fn();
|
|
260
|
+
} finally{
|
|
261
|
+
release();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async enqueueRpcTask(params) {
|
|
265
|
+
await this.runMutation(async ()=>{
|
|
266
|
+
const prompt = typeof params.prompt === "string" ? params.prompt : "";
|
|
267
|
+
if (!prompt.trim()) {
|
|
268
|
+
throw new Error("enqueueTask requires a prompt.");
|
|
269
|
+
}
|
|
270
|
+
const owner = params.owner === "claude" ? "claude" : "codex";
|
|
271
|
+
const commandId = `rpc-${Date.now()}`;
|
|
272
|
+
const taskId = `task-${commandId}`;
|
|
273
|
+
const task = buildAdHocTask(owner, prompt, taskId, {
|
|
274
|
+
routeReason: typeof params.routeReason === "string" ? params.routeReason : null,
|
|
275
|
+
claimedPaths: Array.isArray(params.claimedPaths) ? params.claimedPaths.map((item)=>String(item)) : []
|
|
276
|
+
});
|
|
277
|
+
this.session.tasks.push(task);
|
|
278
|
+
addDecisionRecord(this.session, {
|
|
279
|
+
kind: "route",
|
|
280
|
+
agent: owner,
|
|
281
|
+
taskId,
|
|
282
|
+
summary: `Routed task to ${owner}`,
|
|
283
|
+
detail: typeof params.routeReason === "string" ? params.routeReason : `Task enqueued for ${owner}.`,
|
|
284
|
+
metadata: {
|
|
285
|
+
strategy: typeof params.routeStrategy === "string" ? params.routeStrategy : "unknown",
|
|
286
|
+
confidence: typeof params.routeConfidence === "number" ? params.routeConfidence : null,
|
|
287
|
+
claimedPaths: task.claimedPaths
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
upsertPathClaim(this.session, {
|
|
291
|
+
taskId,
|
|
292
|
+
agent: owner,
|
|
293
|
+
source: "route",
|
|
294
|
+
paths: task.claimedPaths,
|
|
295
|
+
note: task.routeReason
|
|
296
|
+
});
|
|
297
|
+
await saveSessionRecord(this.paths, this.session);
|
|
298
|
+
await recordEvent(this.paths, this.session.id, "task.enqueued", {
|
|
299
|
+
owner,
|
|
300
|
+
via: "rpc"
|
|
301
|
+
});
|
|
302
|
+
await this.publishSnapshot("task.enqueued");
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
async kickoffFromRpc(params) {
|
|
306
|
+
await this.runMutation(async ()=>{
|
|
307
|
+
const prompt = typeof params.prompt === "string" ? params.prompt : "";
|
|
308
|
+
if (!prompt.trim()) {
|
|
309
|
+
throw new Error("kickoff requires a prompt.");
|
|
310
|
+
}
|
|
311
|
+
this.session.goal = prompt;
|
|
312
|
+
this.session.tasks.push(...buildKickoffTasks(prompt));
|
|
313
|
+
await saveSessionRecord(this.paths, this.session);
|
|
314
|
+
await recordEvent(this.paths, this.session.id, "tasks.kickoff_enqueued", {
|
|
315
|
+
count: 2,
|
|
316
|
+
via: "rpc"
|
|
317
|
+
});
|
|
318
|
+
await this.publishSnapshot("tasks.kickoff_enqueued");
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async stopFromRpc() {
|
|
322
|
+
await this.runMutation(async ()=>{
|
|
323
|
+
this.session.status = "stopped";
|
|
324
|
+
this.running = false;
|
|
325
|
+
this.session.daemonHeartbeatAt = new Date().toISOString();
|
|
326
|
+
await saveSessionRecord(this.paths, this.session);
|
|
327
|
+
await recordEvent(this.paths, this.session.id, "daemon.stopped", {
|
|
328
|
+
via: "rpc"
|
|
329
|
+
});
|
|
330
|
+
if (this.interval) {
|
|
331
|
+
clearInterval(this.interval);
|
|
332
|
+
this.interval = null;
|
|
333
|
+
}
|
|
334
|
+
await this.closeRpcServer();
|
|
335
|
+
this.stopResolver?.();
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
async resolveApprovalFromRpc(params) {
|
|
339
|
+
await this.runMutation(async ()=>{
|
|
340
|
+
const requestId = typeof params.requestId === "string" ? params.requestId : "";
|
|
341
|
+
const decision = params.decision === "deny" ? "deny" : "allow";
|
|
342
|
+
const remember = params.remember === true;
|
|
343
|
+
if (!requestId) {
|
|
344
|
+
throw new Error("resolveApproval requires a requestId.");
|
|
345
|
+
}
|
|
346
|
+
const request = await resolveApprovalRequest(this.paths, requestId, decision, remember);
|
|
347
|
+
addDecisionRecord(this.session, {
|
|
348
|
+
kind: "approval",
|
|
349
|
+
agent: request.agent,
|
|
350
|
+
summary: `${decision === "allow" ? "Approved" : "Denied"} ${request.toolName}`,
|
|
351
|
+
detail: request.summary,
|
|
352
|
+
metadata: {
|
|
353
|
+
requestId: request.id,
|
|
354
|
+
remember,
|
|
355
|
+
toolName: request.toolName
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
await saveSessionRecord(this.paths, this.session);
|
|
359
|
+
await recordEvent(this.paths, this.session.id, "approval.resolved", {
|
|
360
|
+
requestId: request.id,
|
|
361
|
+
decision,
|
|
362
|
+
remember,
|
|
363
|
+
agent: request.agent,
|
|
364
|
+
toolName: request.toolName,
|
|
365
|
+
via: "rpc"
|
|
366
|
+
});
|
|
367
|
+
await this.publishSnapshot("approval.resolved");
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async getTaskArtifactFromRpc(params) {
|
|
371
|
+
const taskId = typeof params.taskId === "string" ? params.taskId : "";
|
|
372
|
+
if (!taskId) {
|
|
373
|
+
throw new Error("taskArtifact requires a taskId.");
|
|
374
|
+
}
|
|
375
|
+
return await loadTaskArtifact(this.paths, taskId);
|
|
376
|
+
}
|
|
377
|
+
async getEventsFromRpc(params) {
|
|
378
|
+
const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : 20;
|
|
379
|
+
return await readRecentEvents(this.paths, limit);
|
|
380
|
+
}
|
|
381
|
+
async getWorktreeDiffFromRpc(params) {
|
|
382
|
+
const agent = params.agent === "claude" ? "claude" : "codex";
|
|
383
|
+
const filePath = typeof params.filePath === "string" ? params.filePath : null;
|
|
384
|
+
const worktree = this.session.worktrees.find((item)=>item.agent === agent);
|
|
385
|
+
if (!worktree) {
|
|
386
|
+
throw new Error(`No managed worktree found for ${agent}.`);
|
|
387
|
+
}
|
|
388
|
+
return await getWorktreeDiffReview(agent, worktree.path, this.session.baseCommit, filePath);
|
|
389
|
+
}
|
|
390
|
+
async addReviewNoteFromRpc(params) {
|
|
391
|
+
await this.runMutation(async ()=>{
|
|
392
|
+
const agent = params.agent === "claude" ? "claude" : "codex";
|
|
393
|
+
const filePath = typeof params.filePath === "string" ? params.filePath.trim() : "";
|
|
394
|
+
const disposition = params.disposition === "approve" || params.disposition === "concern" || params.disposition === "question" ? params.disposition : "note";
|
|
395
|
+
const body = typeof params.body === "string" ? params.body.trim() : "";
|
|
396
|
+
const taskId = typeof params.taskId === "string" ? params.taskId : null;
|
|
397
|
+
const hunkIndex = typeof params.hunkIndex === "number" ? params.hunkIndex : null;
|
|
398
|
+
const hunkHeader = typeof params.hunkHeader === "string" ? params.hunkHeader : null;
|
|
399
|
+
if (!filePath) {
|
|
400
|
+
throw new Error("addReviewNote requires a filePath.");
|
|
401
|
+
}
|
|
402
|
+
if (!body) {
|
|
403
|
+
throw new Error("addReviewNote requires a note body.");
|
|
404
|
+
}
|
|
405
|
+
const note = addReviewNote(this.session, {
|
|
406
|
+
agent,
|
|
407
|
+
taskId,
|
|
408
|
+
filePath,
|
|
409
|
+
hunkIndex,
|
|
410
|
+
hunkHeader,
|
|
411
|
+
disposition,
|
|
412
|
+
body
|
|
413
|
+
});
|
|
414
|
+
addDecisionRecord(this.session, {
|
|
415
|
+
kind: "review",
|
|
416
|
+
agent,
|
|
417
|
+
taskId,
|
|
418
|
+
summary: note.summary,
|
|
419
|
+
detail: note.body,
|
|
420
|
+
metadata: {
|
|
421
|
+
filePath: note.filePath,
|
|
422
|
+
hunkIndex: note.hunkIndex,
|
|
423
|
+
hunkHeader: note.hunkHeader,
|
|
424
|
+
disposition: note.disposition,
|
|
425
|
+
reviewNoteId: note.id
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
await saveSessionRecord(this.paths, this.session);
|
|
429
|
+
if (taskId) {
|
|
430
|
+
await this.refreshTaskArtifactReviewNotes(taskId);
|
|
431
|
+
}
|
|
432
|
+
await recordEvent(this.paths, this.session.id, "review.note_added", {
|
|
433
|
+
reviewNoteId: note.id,
|
|
434
|
+
agent: note.agent,
|
|
435
|
+
taskId: note.taskId,
|
|
436
|
+
filePath: note.filePath,
|
|
437
|
+
hunkIndex: note.hunkIndex,
|
|
438
|
+
disposition: note.disposition
|
|
439
|
+
});
|
|
440
|
+
await this.publishSnapshot("review.note_added");
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async updateReviewNoteFromRpc(params) {
|
|
444
|
+
await this.runMutation(async ()=>{
|
|
445
|
+
const noteId = typeof params.noteId === "string" ? params.noteId : "";
|
|
446
|
+
const body = typeof params.body === "string" ? params.body.trim() : "";
|
|
447
|
+
if (!noteId) {
|
|
448
|
+
throw new Error("updateReviewNote requires a noteId.");
|
|
449
|
+
}
|
|
450
|
+
if (!body) {
|
|
451
|
+
throw new Error("updateReviewNote requires a note body.");
|
|
452
|
+
}
|
|
453
|
+
const note = updateReviewNote(this.session, noteId, {
|
|
454
|
+
body
|
|
455
|
+
});
|
|
456
|
+
if (!note) {
|
|
457
|
+
throw new Error(`Review note ${noteId} was not found.`);
|
|
458
|
+
}
|
|
459
|
+
addDecisionRecord(this.session, {
|
|
460
|
+
kind: "review",
|
|
461
|
+
agent: note.agent,
|
|
462
|
+
taskId: note.taskId,
|
|
463
|
+
summary: `Edited review note ${note.id}`,
|
|
464
|
+
detail: note.body,
|
|
465
|
+
metadata: {
|
|
466
|
+
reviewNoteId: note.id,
|
|
467
|
+
filePath: note.filePath,
|
|
468
|
+
hunkIndex: note.hunkIndex
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
await saveSessionRecord(this.paths, this.session);
|
|
472
|
+
if (note.taskId) {
|
|
473
|
+
await this.refreshTaskArtifactReviewNotes(note.taskId);
|
|
474
|
+
}
|
|
475
|
+
await recordEvent(this.paths, this.session.id, "review.note_updated", {
|
|
476
|
+
reviewNoteId: note.id,
|
|
477
|
+
taskId: note.taskId,
|
|
478
|
+
agent: note.agent,
|
|
479
|
+
filePath: note.filePath
|
|
480
|
+
});
|
|
481
|
+
await this.publishSnapshot("review.note_updated");
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
async addReviewReplyFromRpc(params) {
|
|
485
|
+
await this.runMutation(async ()=>{
|
|
486
|
+
const noteId = typeof params.noteId === "string" ? params.noteId : "";
|
|
487
|
+
const body = typeof params.body === "string" ? params.body.trim() : "";
|
|
488
|
+
if (!noteId) {
|
|
489
|
+
throw new Error("addReviewReply requires a noteId.");
|
|
490
|
+
}
|
|
491
|
+
if (!body) {
|
|
492
|
+
throw new Error("addReviewReply requires a reply body.");
|
|
493
|
+
}
|
|
494
|
+
const note = addReviewReply(this.session, noteId, body);
|
|
495
|
+
if (!note) {
|
|
496
|
+
throw new Error(`Review note ${noteId} was not found.`);
|
|
497
|
+
}
|
|
498
|
+
addDecisionRecord(this.session, {
|
|
499
|
+
kind: "review",
|
|
500
|
+
agent: note.agent,
|
|
501
|
+
taskId: note.taskId,
|
|
502
|
+
summary: `Replied to review note ${note.id}`,
|
|
503
|
+
detail: body,
|
|
504
|
+
metadata: {
|
|
505
|
+
reviewNoteId: note.id,
|
|
506
|
+
filePath: note.filePath,
|
|
507
|
+
hunkIndex: note.hunkIndex,
|
|
508
|
+
replyCount: note.comments.length
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
await saveSessionRecord(this.paths, this.session);
|
|
512
|
+
if (note.taskId) {
|
|
513
|
+
await this.refreshTaskArtifactReviewNotes(note.taskId);
|
|
514
|
+
}
|
|
515
|
+
await recordEvent(this.paths, this.session.id, "review.reply_added", {
|
|
516
|
+
reviewNoteId: note.id,
|
|
517
|
+
taskId: note.taskId,
|
|
518
|
+
agent: note.agent,
|
|
519
|
+
filePath: note.filePath,
|
|
520
|
+
replyCount: note.comments.length
|
|
521
|
+
});
|
|
522
|
+
await this.publishSnapshot("review.reply_added");
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async setReviewNoteStatusFromRpc(params) {
|
|
526
|
+
await this.runMutation(async ()=>{
|
|
527
|
+
const noteId = typeof params.noteId === "string" ? params.noteId : "";
|
|
528
|
+
const status = params.status === "resolved" ? "resolved" : "open";
|
|
529
|
+
if (!noteId) {
|
|
530
|
+
throw new Error("setReviewNoteStatus requires a noteId.");
|
|
531
|
+
}
|
|
532
|
+
const note = setReviewNoteStatus(this.session, noteId, status);
|
|
533
|
+
if (!note) {
|
|
534
|
+
throw new Error(`Review note ${noteId} was not found.`);
|
|
535
|
+
}
|
|
536
|
+
addDecisionRecord(this.session, {
|
|
537
|
+
kind: "review",
|
|
538
|
+
agent: note.agent,
|
|
539
|
+
taskId: note.taskId,
|
|
540
|
+
summary: `${status === "resolved" ? "Resolved" : "Reopened"} review note ${note.id}`,
|
|
541
|
+
detail: note.summary,
|
|
542
|
+
metadata: {
|
|
543
|
+
reviewNoteId: note.id,
|
|
544
|
+
status,
|
|
545
|
+
filePath: note.filePath,
|
|
546
|
+
hunkIndex: note.hunkIndex
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
await saveSessionRecord(this.paths, this.session);
|
|
550
|
+
if (note.taskId) {
|
|
551
|
+
await this.refreshTaskArtifactReviewNotes(note.taskId);
|
|
552
|
+
}
|
|
553
|
+
await recordEvent(this.paths, this.session.id, "review.note_status_changed", {
|
|
554
|
+
reviewNoteId: note.id,
|
|
555
|
+
taskId: note.taskId,
|
|
556
|
+
agent: note.agent,
|
|
557
|
+
filePath: note.filePath,
|
|
558
|
+
status: note.status
|
|
559
|
+
});
|
|
560
|
+
await this.publishSnapshot("review.note_status_changed");
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
async enqueueReviewFollowUpFromRpc(params) {
|
|
564
|
+
await this.runMutation(async ()=>{
|
|
565
|
+
const noteId = typeof params.noteId === "string" ? params.noteId : "";
|
|
566
|
+
const owner = params.owner === "claude" ? "claude" : "codex";
|
|
567
|
+
const mode = params.mode === "handoff" ? "handoff" : "fix";
|
|
568
|
+
if (!noteId) {
|
|
569
|
+
throw new Error("enqueueReviewFollowUp requires a noteId.");
|
|
570
|
+
}
|
|
571
|
+
const note = this.session.reviewNotes.find((item)=>item.id === noteId) ?? null;
|
|
572
|
+
if (!note) {
|
|
573
|
+
throw new Error(`Review note ${noteId} was not found.`);
|
|
574
|
+
}
|
|
575
|
+
const taskId = `task-review-${Date.now()}`;
|
|
576
|
+
const scope = note.hunkHeader ? `${note.filePath} ${note.hunkHeader}` : note.filePath;
|
|
577
|
+
const promptLines = [
|
|
578
|
+
`${mode === "handoff" ? "Handle a review handoff" : "Address a review note"} for ${scope}.`,
|
|
579
|
+
`Disposition: ${note.disposition}.`,
|
|
580
|
+
`Review note: ${note.body}`
|
|
581
|
+
];
|
|
582
|
+
if (note.taskId) {
|
|
583
|
+
promptLines.push(`Originating task: ${note.taskId}.`);
|
|
584
|
+
}
|
|
585
|
+
if (mode === "handoff") {
|
|
586
|
+
promptLines.push(`This was handed off from ${note.agent} work to ${owner}.`);
|
|
587
|
+
}
|
|
588
|
+
promptLines.push(`Focus the change in ${note.filePath} and update the managed worktree accordingly.`);
|
|
589
|
+
const task = buildAdHocTask(owner, promptLines.join(" "), taskId, {
|
|
590
|
+
routeReason: mode === "handoff" ? `Operator handed off review note ${note.id} to ${owner}.` : `Operator created a follow-up task from review note ${note.id}.`,
|
|
591
|
+
claimedPaths: [
|
|
592
|
+
note.filePath
|
|
593
|
+
]
|
|
594
|
+
});
|
|
595
|
+
this.session.tasks.push(task);
|
|
596
|
+
linkReviewFollowUpTask(this.session, note.id, taskId);
|
|
597
|
+
upsertPathClaim(this.session, {
|
|
598
|
+
taskId,
|
|
599
|
+
agent: owner,
|
|
600
|
+
source: "route",
|
|
601
|
+
paths: task.claimedPaths,
|
|
602
|
+
note: task.routeReason
|
|
603
|
+
});
|
|
604
|
+
addDecisionRecord(this.session, {
|
|
605
|
+
kind: "review",
|
|
606
|
+
agent: owner,
|
|
607
|
+
taskId,
|
|
608
|
+
summary: `Queued ${mode} follow-up from review note ${note.id}`,
|
|
609
|
+
detail: note.body,
|
|
610
|
+
metadata: {
|
|
611
|
+
reviewNoteId: note.id,
|
|
612
|
+
owner,
|
|
613
|
+
mode,
|
|
614
|
+
filePath: note.filePath,
|
|
615
|
+
sourceAgent: note.agent
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
await saveSessionRecord(this.paths, this.session);
|
|
619
|
+
if (note.taskId) {
|
|
620
|
+
await this.refreshTaskArtifactReviewNotes(note.taskId);
|
|
621
|
+
}
|
|
622
|
+
await recordEvent(this.paths, this.session.id, "review.followup_queued", {
|
|
623
|
+
reviewNoteId: note.id,
|
|
624
|
+
followUpTaskId: taskId,
|
|
625
|
+
owner,
|
|
626
|
+
mode,
|
|
627
|
+
filePath: note.filePath
|
|
628
|
+
});
|
|
629
|
+
await this.publishSnapshot("review.followup_queued");
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
async refreshTaskArtifactReviewNotes(taskId) {
|
|
633
|
+
const artifact = await loadTaskArtifact(this.paths, taskId);
|
|
634
|
+
if (!artifact) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
artifact.reviewNotes = reviewNotesForTask(this.session, taskId);
|
|
638
|
+
await saveTaskArtifact(this.paths, artifact);
|
|
639
|
+
}
|
|
640
|
+
async refreshReviewArtifactsForNotes(notes) {
|
|
641
|
+
const taskIds = [
|
|
642
|
+
...new Set(notes.map((note)=>note.taskId).filter((value)=>Boolean(value)))
|
|
643
|
+
];
|
|
644
|
+
for (const taskId of taskIds){
|
|
645
|
+
await this.refreshTaskArtifactReviewNotes(taskId);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async publishSnapshot(reason) {
|
|
649
|
+
if (this.subscribers.size === 0) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const snapshot = await this.buildSnapshot();
|
|
653
|
+
const notification = {
|
|
654
|
+
method: "snapshot.updated",
|
|
655
|
+
params: {
|
|
656
|
+
reason,
|
|
657
|
+
snapshot
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
for (const subscriber of this.subscribers){
|
|
661
|
+
this.writeNotification(subscriber, notification);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async closeRpcServer() {
|
|
665
|
+
const server = this.rpcServer;
|
|
666
|
+
this.rpcServer = null;
|
|
667
|
+
for (const client of this.clients){
|
|
668
|
+
client.end();
|
|
669
|
+
}
|
|
670
|
+
this.clients.clear();
|
|
671
|
+
this.subscribers.clear();
|
|
672
|
+
if (server) {
|
|
673
|
+
await new Promise((resolve)=>{
|
|
674
|
+
server.close(()=>resolve());
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
await fs.rm(this.paths.socketPath, {
|
|
678
|
+
force: true
|
|
679
|
+
}).catch(()=>{});
|
|
680
|
+
}
|
|
40
681
|
async tick() {
|
|
41
682
|
if (!this.running || this.processing) {
|
|
42
683
|
return;
|
|
@@ -50,6 +691,7 @@ export class KaviDaemon {
|
|
|
50
691
|
clearInterval(this.interval);
|
|
51
692
|
this.interval = null;
|
|
52
693
|
}
|
|
694
|
+
await this.closeRpcServer();
|
|
53
695
|
this.stopResolver?.();
|
|
54
696
|
return;
|
|
55
697
|
}
|
|
@@ -64,6 +706,7 @@ export class KaviDaemon {
|
|
|
64
706
|
await recordEvent(this.paths, this.session.id, "tasks.kickoff_created", {
|
|
65
707
|
count: this.session.tasks.length
|
|
66
708
|
});
|
|
709
|
+
await this.publishSnapshot("tasks.kickoff_created");
|
|
67
710
|
}
|
|
68
711
|
const pending = this.session.tasks.filter((task)=>task.status === "pending");
|
|
69
712
|
for (const task of pending){
|
|
@@ -89,6 +732,7 @@ export class KaviDaemon {
|
|
|
89
732
|
clearInterval(this.interval);
|
|
90
733
|
this.interval = null;
|
|
91
734
|
}
|
|
735
|
+
await this.closeRpcServer();
|
|
92
736
|
this.stopResolver?.();
|
|
93
737
|
return;
|
|
94
738
|
}
|
|
@@ -99,6 +743,7 @@ export class KaviDaemon {
|
|
|
99
743
|
await recordEvent(this.paths, this.session.id, "tasks.kickoff_enqueued", {
|
|
100
744
|
count: 2
|
|
101
745
|
});
|
|
746
|
+
await this.publishSnapshot("tasks.kickoff_enqueued");
|
|
102
747
|
continue;
|
|
103
748
|
}
|
|
104
749
|
if (command.type === "enqueue" && typeof command.payload.prompt === "string") {
|
|
@@ -132,6 +777,7 @@ export class KaviDaemon {
|
|
|
132
777
|
await recordEvent(this.paths, this.session.id, "task.enqueued", {
|
|
133
778
|
owner
|
|
134
779
|
});
|
|
780
|
+
await this.publishSnapshot("task.enqueued");
|
|
135
781
|
}
|
|
136
782
|
}
|
|
137
783
|
}
|
|
@@ -144,6 +790,7 @@ export class KaviDaemon {
|
|
|
144
790
|
taskId: task.id,
|
|
145
791
|
owner: task.owner
|
|
146
792
|
});
|
|
793
|
+
await this.publishSnapshot("task.started");
|
|
147
794
|
try {
|
|
148
795
|
let envelope;
|
|
149
796
|
let peerMessages;
|
|
@@ -159,7 +806,7 @@ export class KaviDaemon {
|
|
|
159
806
|
envelope = result.envelope;
|
|
160
807
|
rawOutput = result.raw;
|
|
161
808
|
peerMessages = buildClaudePeerMessages(result.envelope, "claude", task.id);
|
|
162
|
-
await this.markAgent("claude", result.envelope.summary, 0,
|
|
809
|
+
await this.markAgent("claude", result.envelope.summary, 0, result.sessionId);
|
|
163
810
|
} else {
|
|
164
811
|
throw new Error(`Unsupported task owner ${task.owner}.`);
|
|
165
812
|
}
|
|
@@ -192,8 +839,25 @@ export class KaviDaemon {
|
|
|
192
839
|
claimedPaths: task.claimedPaths
|
|
193
840
|
}
|
|
194
841
|
});
|
|
842
|
+
const autoResolvedNotes = autoResolveReviewNotesForCompletedTask(this.session, task.id);
|
|
843
|
+
for (const note of autoResolvedNotes){
|
|
844
|
+
addDecisionRecord(this.session, {
|
|
845
|
+
kind: "review",
|
|
846
|
+
agent: note.agent,
|
|
847
|
+
taskId: note.taskId,
|
|
848
|
+
summary: `Auto-resolved review note ${note.id}`,
|
|
849
|
+
detail: `Closed because linked follow-up task ${task.id} completed successfully.`,
|
|
850
|
+
metadata: {
|
|
851
|
+
reviewNoteId: note.id,
|
|
852
|
+
filePath: note.filePath,
|
|
853
|
+
followUpTaskId: task.id,
|
|
854
|
+
reason: "follow-up-task-completed"
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
}
|
|
195
858
|
this.session.peerMessages.push(...peerMessages);
|
|
196
859
|
await saveSessionRecord(this.paths, this.session);
|
|
860
|
+
const decisionReplay = buildDecisionReplay(this.session, task, task.owner === "claude" ? "claude" : "codex");
|
|
197
861
|
await saveTaskArtifact(this.paths, {
|
|
198
862
|
taskId: task.id,
|
|
199
863
|
sessionId: this.session.id,
|
|
@@ -201,18 +865,33 @@ export class KaviDaemon {
|
|
|
201
865
|
owner: task.owner,
|
|
202
866
|
status: task.status,
|
|
203
867
|
summary: task.summary,
|
|
868
|
+
routeReason: task.routeReason,
|
|
869
|
+
claimedPaths: task.claimedPaths,
|
|
870
|
+
decisionReplay,
|
|
204
871
|
rawOutput,
|
|
205
872
|
error: null,
|
|
206
873
|
envelope,
|
|
874
|
+
reviewNotes: reviewNotesForTask(this.session, task.id),
|
|
207
875
|
startedAt,
|
|
208
876
|
finishedAt: task.updatedAt
|
|
209
877
|
});
|
|
878
|
+
await this.refreshReviewArtifactsForNotes(autoResolvedNotes);
|
|
210
879
|
await recordEvent(this.paths, this.session.id, "task.completed", {
|
|
211
880
|
taskId: task.id,
|
|
212
881
|
owner: task.owner,
|
|
213
882
|
status: task.status,
|
|
214
883
|
peerMessages: peerMessages.length
|
|
215
884
|
});
|
|
885
|
+
for (const note of autoResolvedNotes){
|
|
886
|
+
await recordEvent(this.paths, this.session.id, "review.note_auto_resolved", {
|
|
887
|
+
reviewNoteId: note.id,
|
|
888
|
+
taskId: note.taskId,
|
|
889
|
+
followUpTaskId: task.id,
|
|
890
|
+
agent: note.agent,
|
|
891
|
+
filePath: note.filePath
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
await this.publishSnapshot("task.completed");
|
|
216
895
|
} catch (error) {
|
|
217
896
|
task.status = "failed";
|
|
218
897
|
task.summary = error instanceof Error ? error.message : String(error);
|
|
@@ -245,6 +924,7 @@ export class KaviDaemon {
|
|
|
245
924
|
});
|
|
246
925
|
await this.markAgent(task.owner, task.summary, 1, task.owner === "claude" ? `${this.session.id}-claude` : null);
|
|
247
926
|
await saveSessionRecord(this.paths, this.session);
|
|
927
|
+
const decisionReplay = buildDecisionReplay(this.session, task, task.owner === "claude" ? "claude" : "codex");
|
|
248
928
|
await saveTaskArtifact(this.paths, {
|
|
249
929
|
taskId: task.id,
|
|
250
930
|
sessionId: this.session.id,
|
|
@@ -252,9 +932,13 @@ export class KaviDaemon {
|
|
|
252
932
|
owner: task.owner,
|
|
253
933
|
status: task.status,
|
|
254
934
|
summary: task.summary,
|
|
935
|
+
routeReason: task.routeReason,
|
|
936
|
+
claimedPaths: task.claimedPaths,
|
|
937
|
+
decisionReplay,
|
|
255
938
|
rawOutput: null,
|
|
256
939
|
error: task.summary,
|
|
257
940
|
envelope: null,
|
|
941
|
+
reviewNotes: reviewNotesForTask(this.session, task.id),
|
|
258
942
|
startedAt,
|
|
259
943
|
finishedAt: task.updatedAt
|
|
260
944
|
});
|
|
@@ -263,6 +947,7 @@ export class KaviDaemon {
|
|
|
263
947
|
owner: task.owner,
|
|
264
948
|
error: task.summary
|
|
265
949
|
});
|
|
950
|
+
await this.publishSnapshot("task.failed");
|
|
266
951
|
}
|
|
267
952
|
}
|
|
268
953
|
async markAgent(agent, summary, exitCode, sessionId) {
|