@mandipadk7/kavi 0.1.1 → 0.1.2
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 -4
- package/dist/adapters/claude.js +131 -8
- package/dist/adapters/shared.js +19 -3
- package/dist/daemon.js +367 -4
- package/dist/git.js +97 -0
- package/dist/main.js +159 -45
- package/dist/paths.js +1 -1
- package/dist/rpc.js +226 -0
- package/dist/task-artifacts.js +10 -2
- package/dist/tui.js +1653 -85
- package/package.json +4 -10
package/dist/daemon.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
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";
|
|
7
12
|
import { buildAdHocTask, buildKickoffTasks } from "./router.js";
|
|
8
|
-
import { loadSessionRecord, recordEvent, saveSessionRecord } from "./session.js";
|
|
9
|
-
import { saveTaskArtifact } from "./task-artifacts.js";
|
|
13
|
+
import { loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord } from "./session.js";
|
|
14
|
+
import { loadTaskArtifact, saveTaskArtifact } from "./task-artifacts.js";
|
|
10
15
|
export class KaviDaemon {
|
|
11
16
|
paths;
|
|
12
17
|
session;
|
|
@@ -15,6 +20,10 @@ export class KaviDaemon {
|
|
|
15
20
|
processing = false;
|
|
16
21
|
interval = null;
|
|
17
22
|
stopResolver = null;
|
|
23
|
+
rpcServer = null;
|
|
24
|
+
clients = new Set();
|
|
25
|
+
subscribers = new Set();
|
|
26
|
+
mutationQueue = Promise.resolve();
|
|
18
27
|
constructor(paths){
|
|
19
28
|
this.paths = paths;
|
|
20
29
|
}
|
|
@@ -28,6 +37,7 @@ export class KaviDaemon {
|
|
|
28
37
|
await recordEvent(this.paths, this.session.id, "daemon.started", {
|
|
29
38
|
daemonPid: process.pid
|
|
30
39
|
});
|
|
40
|
+
await this.startRpcServer();
|
|
31
41
|
this.running = true;
|
|
32
42
|
void this.tick();
|
|
33
43
|
this.interval = setInterval(()=>{
|
|
@@ -37,6 +47,343 @@ export class KaviDaemon {
|
|
|
37
47
|
this.stopResolver = resolve;
|
|
38
48
|
});
|
|
39
49
|
}
|
|
50
|
+
async startRpcServer() {
|
|
51
|
+
await fs.mkdir(path.dirname(this.paths.socketPath), {
|
|
52
|
+
recursive: true
|
|
53
|
+
});
|
|
54
|
+
await fs.rm(this.paths.socketPath, {
|
|
55
|
+
force: true
|
|
56
|
+
}).catch(()=>{});
|
|
57
|
+
this.rpcServer = net.createServer((socket)=>{
|
|
58
|
+
this.clients.add(socket);
|
|
59
|
+
socket.setEncoding("utf8");
|
|
60
|
+
let buffer = "";
|
|
61
|
+
socket.on("data", (chunk)=>{
|
|
62
|
+
buffer += chunk;
|
|
63
|
+
while(true){
|
|
64
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
65
|
+
if (newlineIndex === -1) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
69
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
70
|
+
if (!line) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
let request;
|
|
74
|
+
try {
|
|
75
|
+
request = JSON.parse(line);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
this.writeRpc(socket, {
|
|
78
|
+
id: "parse-error",
|
|
79
|
+
error: {
|
|
80
|
+
message: error instanceof Error ? error.message : "Unable to parse RPC payload."
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
void this.handleRpcRequest(socket, request);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
const cleanup = ()=>{
|
|
89
|
+
this.clients.delete(socket);
|
|
90
|
+
this.subscribers.delete(socket);
|
|
91
|
+
};
|
|
92
|
+
socket.on("error", cleanup);
|
|
93
|
+
socket.on("close", cleanup);
|
|
94
|
+
});
|
|
95
|
+
await new Promise((resolve, reject)=>{
|
|
96
|
+
this.rpcServer?.once("error", reject);
|
|
97
|
+
this.rpcServer?.listen(this.paths.socketPath, ()=>{
|
|
98
|
+
this.rpcServer?.off("error", reject);
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
writeRpc(socket, response, onWritten) {
|
|
104
|
+
socket.write(`${JSON.stringify(response)}\n`, onWritten);
|
|
105
|
+
}
|
|
106
|
+
writeNotification(socket, notification) {
|
|
107
|
+
socket.write(`${JSON.stringify(notification)}\n`);
|
|
108
|
+
}
|
|
109
|
+
async handleRpcRequest(socket, request) {
|
|
110
|
+
try {
|
|
111
|
+
const dispatch = await this.dispatchRpc(socket, request.method, request.params ?? {});
|
|
112
|
+
this.writeRpc(socket, {
|
|
113
|
+
id: request.id,
|
|
114
|
+
result: dispatch.result
|
|
115
|
+
}, ()=>{
|
|
116
|
+
if (dispatch.shutdownAfterResponse) {
|
|
117
|
+
void this.stopFromRpc();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
this.writeRpc(socket, {
|
|
122
|
+
id: request.id,
|
|
123
|
+
error: {
|
|
124
|
+
message: error instanceof Error ? error.message : String(error)
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async dispatchRpc(socket, method, params) {
|
|
130
|
+
switch(method){
|
|
131
|
+
case "ping":
|
|
132
|
+
return {
|
|
133
|
+
result: {
|
|
134
|
+
ok: true,
|
|
135
|
+
sessionId: this.session.id
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
case "snapshot":
|
|
139
|
+
return {
|
|
140
|
+
result: await this.buildSnapshot()
|
|
141
|
+
};
|
|
142
|
+
case "subscribe":
|
|
143
|
+
this.subscribers.add(socket);
|
|
144
|
+
return {
|
|
145
|
+
result: await this.buildSnapshot()
|
|
146
|
+
};
|
|
147
|
+
case "kickoff":
|
|
148
|
+
await this.kickoffFromRpc(params);
|
|
149
|
+
return {
|
|
150
|
+
result: {
|
|
151
|
+
ok: true
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
case "enqueueTask":
|
|
155
|
+
await this.enqueueRpcTask(params);
|
|
156
|
+
return {
|
|
157
|
+
result: {
|
|
158
|
+
ok: true
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
case "shutdown":
|
|
162
|
+
return {
|
|
163
|
+
result: {
|
|
164
|
+
ok: true
|
|
165
|
+
},
|
|
166
|
+
shutdownAfterResponse: true
|
|
167
|
+
};
|
|
168
|
+
case "resolveApproval":
|
|
169
|
+
await this.resolveApprovalFromRpc(params);
|
|
170
|
+
return {
|
|
171
|
+
result: {
|
|
172
|
+
ok: true
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
case "taskArtifact":
|
|
176
|
+
return {
|
|
177
|
+
result: await this.getTaskArtifactFromRpc(params)
|
|
178
|
+
};
|
|
179
|
+
case "events":
|
|
180
|
+
return {
|
|
181
|
+
result: await this.getEventsFromRpc(params)
|
|
182
|
+
};
|
|
183
|
+
case "worktreeDiff":
|
|
184
|
+
return {
|
|
185
|
+
result: await this.getWorktreeDiffFromRpc(params)
|
|
186
|
+
};
|
|
187
|
+
case "notifyExternalUpdate":
|
|
188
|
+
await this.publishSnapshot(typeof params.reason === "string" && params.reason.trim() ? params.reason.trim() : "external.update");
|
|
189
|
+
return {
|
|
190
|
+
result: {
|
|
191
|
+
ok: true
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
default:
|
|
195
|
+
throw new Error(`Unknown RPC method: ${method}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async buildSnapshot() {
|
|
199
|
+
const session = await loadSessionRecord(this.paths);
|
|
200
|
+
const events = await readRecentEvents(this.paths, 30);
|
|
201
|
+
const approvals = await listApprovalRequests(this.paths, {
|
|
202
|
+
includeResolved: true
|
|
203
|
+
});
|
|
204
|
+
const worktreeDiffs = await Promise.all(session.worktrees.map(async (worktree)=>({
|
|
205
|
+
agent: worktree.agent,
|
|
206
|
+
paths: await listWorktreeChangedPaths(worktree.path, session.baseCommit).catch(()=>[])
|
|
207
|
+
})));
|
|
208
|
+
return {
|
|
209
|
+
session,
|
|
210
|
+
events,
|
|
211
|
+
approvals,
|
|
212
|
+
worktreeDiffs
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async runMutation(fn) {
|
|
216
|
+
const previous = this.mutationQueue;
|
|
217
|
+
let release = ()=>{};
|
|
218
|
+
this.mutationQueue = new Promise((resolve)=>{
|
|
219
|
+
release = resolve;
|
|
220
|
+
});
|
|
221
|
+
await previous;
|
|
222
|
+
try {
|
|
223
|
+
return await fn();
|
|
224
|
+
} finally{
|
|
225
|
+
release();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async enqueueRpcTask(params) {
|
|
229
|
+
await this.runMutation(async ()=>{
|
|
230
|
+
const prompt = typeof params.prompt === "string" ? params.prompt : "";
|
|
231
|
+
if (!prompt.trim()) {
|
|
232
|
+
throw new Error("enqueueTask requires a prompt.");
|
|
233
|
+
}
|
|
234
|
+
const owner = params.owner === "claude" ? "claude" : "codex";
|
|
235
|
+
const commandId = `rpc-${Date.now()}`;
|
|
236
|
+
const taskId = `task-${commandId}`;
|
|
237
|
+
const task = buildAdHocTask(owner, prompt, taskId, {
|
|
238
|
+
routeReason: typeof params.routeReason === "string" ? params.routeReason : null,
|
|
239
|
+
claimedPaths: Array.isArray(params.claimedPaths) ? params.claimedPaths.map((item)=>String(item)) : []
|
|
240
|
+
});
|
|
241
|
+
this.session.tasks.push(task);
|
|
242
|
+
addDecisionRecord(this.session, {
|
|
243
|
+
kind: "route",
|
|
244
|
+
agent: owner,
|
|
245
|
+
taskId,
|
|
246
|
+
summary: `Routed task to ${owner}`,
|
|
247
|
+
detail: typeof params.routeReason === "string" ? params.routeReason : `Task enqueued for ${owner}.`,
|
|
248
|
+
metadata: {
|
|
249
|
+
strategy: typeof params.routeStrategy === "string" ? params.routeStrategy : "unknown",
|
|
250
|
+
confidence: typeof params.routeConfidence === "number" ? params.routeConfidence : null,
|
|
251
|
+
claimedPaths: task.claimedPaths
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
upsertPathClaim(this.session, {
|
|
255
|
+
taskId,
|
|
256
|
+
agent: owner,
|
|
257
|
+
source: "route",
|
|
258
|
+
paths: task.claimedPaths,
|
|
259
|
+
note: task.routeReason
|
|
260
|
+
});
|
|
261
|
+
await saveSessionRecord(this.paths, this.session);
|
|
262
|
+
await recordEvent(this.paths, this.session.id, "task.enqueued", {
|
|
263
|
+
owner,
|
|
264
|
+
via: "rpc"
|
|
265
|
+
});
|
|
266
|
+
await this.publishSnapshot("task.enqueued");
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
async kickoffFromRpc(params) {
|
|
270
|
+
await this.runMutation(async ()=>{
|
|
271
|
+
const prompt = typeof params.prompt === "string" ? params.prompt : "";
|
|
272
|
+
if (!prompt.trim()) {
|
|
273
|
+
throw new Error("kickoff requires a prompt.");
|
|
274
|
+
}
|
|
275
|
+
this.session.goal = prompt;
|
|
276
|
+
this.session.tasks.push(...buildKickoffTasks(prompt));
|
|
277
|
+
await saveSessionRecord(this.paths, this.session);
|
|
278
|
+
await recordEvent(this.paths, this.session.id, "tasks.kickoff_enqueued", {
|
|
279
|
+
count: 2,
|
|
280
|
+
via: "rpc"
|
|
281
|
+
});
|
|
282
|
+
await this.publishSnapshot("tasks.kickoff_enqueued");
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
async stopFromRpc() {
|
|
286
|
+
await this.runMutation(async ()=>{
|
|
287
|
+
this.session.status = "stopped";
|
|
288
|
+
this.running = false;
|
|
289
|
+
this.session.daemonHeartbeatAt = new Date().toISOString();
|
|
290
|
+
await saveSessionRecord(this.paths, this.session);
|
|
291
|
+
await recordEvent(this.paths, this.session.id, "daemon.stopped", {
|
|
292
|
+
via: "rpc"
|
|
293
|
+
});
|
|
294
|
+
if (this.interval) {
|
|
295
|
+
clearInterval(this.interval);
|
|
296
|
+
this.interval = null;
|
|
297
|
+
}
|
|
298
|
+
await this.closeRpcServer();
|
|
299
|
+
this.stopResolver?.();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
async resolveApprovalFromRpc(params) {
|
|
303
|
+
await this.runMutation(async ()=>{
|
|
304
|
+
const requestId = typeof params.requestId === "string" ? params.requestId : "";
|
|
305
|
+
const decision = params.decision === "deny" ? "deny" : "allow";
|
|
306
|
+
const remember = params.remember === true;
|
|
307
|
+
if (!requestId) {
|
|
308
|
+
throw new Error("resolveApproval requires a requestId.");
|
|
309
|
+
}
|
|
310
|
+
const request = await resolveApprovalRequest(this.paths, requestId, decision, remember);
|
|
311
|
+
addDecisionRecord(this.session, {
|
|
312
|
+
kind: "approval",
|
|
313
|
+
agent: request.agent,
|
|
314
|
+
summary: `${decision === "allow" ? "Approved" : "Denied"} ${request.toolName}`,
|
|
315
|
+
detail: request.summary,
|
|
316
|
+
metadata: {
|
|
317
|
+
requestId: request.id,
|
|
318
|
+
remember,
|
|
319
|
+
toolName: request.toolName
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
await saveSessionRecord(this.paths, this.session);
|
|
323
|
+
await recordEvent(this.paths, this.session.id, "approval.resolved", {
|
|
324
|
+
requestId: request.id,
|
|
325
|
+
decision,
|
|
326
|
+
remember,
|
|
327
|
+
agent: request.agent,
|
|
328
|
+
toolName: request.toolName,
|
|
329
|
+
via: "rpc"
|
|
330
|
+
});
|
|
331
|
+
await this.publishSnapshot("approval.resolved");
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
async getTaskArtifactFromRpc(params) {
|
|
335
|
+
const taskId = typeof params.taskId === "string" ? params.taskId : "";
|
|
336
|
+
if (!taskId) {
|
|
337
|
+
throw new Error("taskArtifact requires a taskId.");
|
|
338
|
+
}
|
|
339
|
+
return await loadTaskArtifact(this.paths, taskId);
|
|
340
|
+
}
|
|
341
|
+
async getEventsFromRpc(params) {
|
|
342
|
+
const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : 20;
|
|
343
|
+
return await readRecentEvents(this.paths, limit);
|
|
344
|
+
}
|
|
345
|
+
async getWorktreeDiffFromRpc(params) {
|
|
346
|
+
const agent = params.agent === "claude" ? "claude" : "codex";
|
|
347
|
+
const filePath = typeof params.filePath === "string" ? params.filePath : null;
|
|
348
|
+
const worktree = this.session.worktrees.find((item)=>item.agent === agent);
|
|
349
|
+
if (!worktree) {
|
|
350
|
+
throw new Error(`No managed worktree found for ${agent}.`);
|
|
351
|
+
}
|
|
352
|
+
return await getWorktreeDiffReview(agent, worktree.path, this.session.baseCommit, filePath);
|
|
353
|
+
}
|
|
354
|
+
async publishSnapshot(reason) {
|
|
355
|
+
if (this.subscribers.size === 0) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const snapshot = await this.buildSnapshot();
|
|
359
|
+
const notification = {
|
|
360
|
+
method: "snapshot.updated",
|
|
361
|
+
params: {
|
|
362
|
+
reason,
|
|
363
|
+
snapshot
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
for (const subscriber of this.subscribers){
|
|
367
|
+
this.writeNotification(subscriber, notification);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async closeRpcServer() {
|
|
371
|
+
const server = this.rpcServer;
|
|
372
|
+
this.rpcServer = null;
|
|
373
|
+
for (const client of this.clients){
|
|
374
|
+
client.end();
|
|
375
|
+
}
|
|
376
|
+
this.clients.clear();
|
|
377
|
+
this.subscribers.clear();
|
|
378
|
+
if (server) {
|
|
379
|
+
await new Promise((resolve)=>{
|
|
380
|
+
server.close(()=>resolve());
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
await fs.rm(this.paths.socketPath, {
|
|
384
|
+
force: true
|
|
385
|
+
}).catch(()=>{});
|
|
386
|
+
}
|
|
40
387
|
async tick() {
|
|
41
388
|
if (!this.running || this.processing) {
|
|
42
389
|
return;
|
|
@@ -50,6 +397,7 @@ export class KaviDaemon {
|
|
|
50
397
|
clearInterval(this.interval);
|
|
51
398
|
this.interval = null;
|
|
52
399
|
}
|
|
400
|
+
await this.closeRpcServer();
|
|
53
401
|
this.stopResolver?.();
|
|
54
402
|
return;
|
|
55
403
|
}
|
|
@@ -64,6 +412,7 @@ export class KaviDaemon {
|
|
|
64
412
|
await recordEvent(this.paths, this.session.id, "tasks.kickoff_created", {
|
|
65
413
|
count: this.session.tasks.length
|
|
66
414
|
});
|
|
415
|
+
await this.publishSnapshot("tasks.kickoff_created");
|
|
67
416
|
}
|
|
68
417
|
const pending = this.session.tasks.filter((task)=>task.status === "pending");
|
|
69
418
|
for (const task of pending){
|
|
@@ -89,6 +438,7 @@ export class KaviDaemon {
|
|
|
89
438
|
clearInterval(this.interval);
|
|
90
439
|
this.interval = null;
|
|
91
440
|
}
|
|
441
|
+
await this.closeRpcServer();
|
|
92
442
|
this.stopResolver?.();
|
|
93
443
|
return;
|
|
94
444
|
}
|
|
@@ -99,6 +449,7 @@ export class KaviDaemon {
|
|
|
99
449
|
await recordEvent(this.paths, this.session.id, "tasks.kickoff_enqueued", {
|
|
100
450
|
count: 2
|
|
101
451
|
});
|
|
452
|
+
await this.publishSnapshot("tasks.kickoff_enqueued");
|
|
102
453
|
continue;
|
|
103
454
|
}
|
|
104
455
|
if (command.type === "enqueue" && typeof command.payload.prompt === "string") {
|
|
@@ -132,6 +483,7 @@ export class KaviDaemon {
|
|
|
132
483
|
await recordEvent(this.paths, this.session.id, "task.enqueued", {
|
|
133
484
|
owner
|
|
134
485
|
});
|
|
486
|
+
await this.publishSnapshot("task.enqueued");
|
|
135
487
|
}
|
|
136
488
|
}
|
|
137
489
|
}
|
|
@@ -144,6 +496,7 @@ export class KaviDaemon {
|
|
|
144
496
|
taskId: task.id,
|
|
145
497
|
owner: task.owner
|
|
146
498
|
});
|
|
499
|
+
await this.publishSnapshot("task.started");
|
|
147
500
|
try {
|
|
148
501
|
let envelope;
|
|
149
502
|
let peerMessages;
|
|
@@ -159,7 +512,7 @@ export class KaviDaemon {
|
|
|
159
512
|
envelope = result.envelope;
|
|
160
513
|
rawOutput = result.raw;
|
|
161
514
|
peerMessages = buildClaudePeerMessages(result.envelope, "claude", task.id);
|
|
162
|
-
await this.markAgent("claude", result.envelope.summary, 0,
|
|
515
|
+
await this.markAgent("claude", result.envelope.summary, 0, result.sessionId);
|
|
163
516
|
} else {
|
|
164
517
|
throw new Error(`Unsupported task owner ${task.owner}.`);
|
|
165
518
|
}
|
|
@@ -194,6 +547,7 @@ export class KaviDaemon {
|
|
|
194
547
|
});
|
|
195
548
|
this.session.peerMessages.push(...peerMessages);
|
|
196
549
|
await saveSessionRecord(this.paths, this.session);
|
|
550
|
+
const decisionReplay = buildDecisionReplay(this.session, task, task.owner === "claude" ? "claude" : "codex");
|
|
197
551
|
await saveTaskArtifact(this.paths, {
|
|
198
552
|
taskId: task.id,
|
|
199
553
|
sessionId: this.session.id,
|
|
@@ -201,6 +555,9 @@ export class KaviDaemon {
|
|
|
201
555
|
owner: task.owner,
|
|
202
556
|
status: task.status,
|
|
203
557
|
summary: task.summary,
|
|
558
|
+
routeReason: task.routeReason,
|
|
559
|
+
claimedPaths: task.claimedPaths,
|
|
560
|
+
decisionReplay,
|
|
204
561
|
rawOutput,
|
|
205
562
|
error: null,
|
|
206
563
|
envelope,
|
|
@@ -213,6 +570,7 @@ export class KaviDaemon {
|
|
|
213
570
|
status: task.status,
|
|
214
571
|
peerMessages: peerMessages.length
|
|
215
572
|
});
|
|
573
|
+
await this.publishSnapshot("task.completed");
|
|
216
574
|
} catch (error) {
|
|
217
575
|
task.status = "failed";
|
|
218
576
|
task.summary = error instanceof Error ? error.message : String(error);
|
|
@@ -245,6 +603,7 @@ export class KaviDaemon {
|
|
|
245
603
|
});
|
|
246
604
|
await this.markAgent(task.owner, task.summary, 1, task.owner === "claude" ? `${this.session.id}-claude` : null);
|
|
247
605
|
await saveSessionRecord(this.paths, this.session);
|
|
606
|
+
const decisionReplay = buildDecisionReplay(this.session, task, task.owner === "claude" ? "claude" : "codex");
|
|
248
607
|
await saveTaskArtifact(this.paths, {
|
|
249
608
|
taskId: task.id,
|
|
250
609
|
sessionId: this.session.id,
|
|
@@ -252,6 +611,9 @@ export class KaviDaemon {
|
|
|
252
611
|
owner: task.owner,
|
|
253
612
|
status: task.status,
|
|
254
613
|
summary: task.summary,
|
|
614
|
+
routeReason: task.routeReason,
|
|
615
|
+
claimedPaths: task.claimedPaths,
|
|
616
|
+
decisionReplay,
|
|
255
617
|
rawOutput: null,
|
|
256
618
|
error: task.summary,
|
|
257
619
|
envelope: null,
|
|
@@ -263,6 +625,7 @@ export class KaviDaemon {
|
|
|
263
625
|
owner: task.owner,
|
|
264
626
|
error: task.summary
|
|
265
627
|
});
|
|
628
|
+
await this.publishSnapshot("task.failed");
|
|
266
629
|
}
|
|
267
630
|
}
|
|
268
631
|
async markAgent(agent, summary, exitCode, sessionId) {
|
package/dist/git.js
CHANGED
|
@@ -59,6 +59,45 @@ function uniqueSorted(values) {
|
|
|
59
59
|
...new Set(values)
|
|
60
60
|
].sort();
|
|
61
61
|
}
|
|
62
|
+
async function isUntrackedPath(worktreePath, filePath) {
|
|
63
|
+
const result = await runCommand("git", [
|
|
64
|
+
"ls-files",
|
|
65
|
+
"--others",
|
|
66
|
+
"--exclude-standard",
|
|
67
|
+
"--",
|
|
68
|
+
filePath
|
|
69
|
+
], {
|
|
70
|
+
cwd: worktreePath
|
|
71
|
+
});
|
|
72
|
+
if (result.code !== 0) {
|
|
73
|
+
throw new Error(result.stderr.trim() || `Unable to inspect untracked status for ${filePath}.`);
|
|
74
|
+
}
|
|
75
|
+
return parsePathList(result.stdout).includes(filePath);
|
|
76
|
+
}
|
|
77
|
+
async function buildSyntheticAddedFilePatch(worktreePath, filePath) {
|
|
78
|
+
const absolutePath = path.join(worktreePath, filePath);
|
|
79
|
+
const content = await fs.readFile(absolutePath, "utf8");
|
|
80
|
+
const normalized = content.replaceAll("\r", "");
|
|
81
|
+
const endsWithNewline = normalized.endsWith("\n");
|
|
82
|
+
const lines = normalized.length === 0 ? [] : normalized.replace(/\n$/, "").split("\n");
|
|
83
|
+
const patchLines = [
|
|
84
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
85
|
+
"new file mode 100644",
|
|
86
|
+
"--- /dev/null",
|
|
87
|
+
`+++ b/${filePath}`
|
|
88
|
+
];
|
|
89
|
+
if (lines.length > 0) {
|
|
90
|
+
patchLines.push(`@@ -0,0 +1,${lines.length} @@`);
|
|
91
|
+
patchLines.push(...lines.map((line)=>`+${line}`));
|
|
92
|
+
if (!endsWithNewline) {
|
|
93
|
+
patchLines.push("\");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
stat: lines.length > 0 ? `new file | ${lines.length} insertion${lines.length === 1 ? "" : "s"}(+)` : "new file | empty",
|
|
98
|
+
patch: patchLines.join("\n")
|
|
99
|
+
};
|
|
100
|
+
}
|
|
62
101
|
export async function resolveTargetBranch(repoRoot, configuredBranch) {
|
|
63
102
|
const exists = await runCommand("git", [
|
|
64
103
|
"show-ref",
|
|
@@ -204,6 +243,64 @@ export async function listWorktreeChangedPaths(worktreePath, baseCommit) {
|
|
|
204
243
|
...parsePathList(untracked.stdout)
|
|
205
244
|
]);
|
|
206
245
|
}
|
|
246
|
+
export async function getWorktreeDiffReview(agent, worktreePath, baseCommit, filePath) {
|
|
247
|
+
const changedPaths = await listWorktreeChangedPaths(worktreePath, baseCommit);
|
|
248
|
+
const selectedPath = filePath && changedPaths.includes(filePath) ? filePath : changedPaths[0] ?? null;
|
|
249
|
+
if (!selectedPath) {
|
|
250
|
+
return {
|
|
251
|
+
agent,
|
|
252
|
+
changedPaths,
|
|
253
|
+
selectedPath: null,
|
|
254
|
+
stat: "No changed files in this worktree.",
|
|
255
|
+
patch: ""
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (await isUntrackedPath(worktreePath, selectedPath)) {
|
|
259
|
+
const synthetic = await buildSyntheticAddedFilePatch(worktreePath, selectedPath);
|
|
260
|
+
return {
|
|
261
|
+
agent,
|
|
262
|
+
changedPaths,
|
|
263
|
+
selectedPath,
|
|
264
|
+
stat: synthetic.stat,
|
|
265
|
+
patch: synthetic.patch
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const [statResult, patchResult] = await Promise.all([
|
|
269
|
+
runCommand("git", [
|
|
270
|
+
"diff",
|
|
271
|
+
"--stat",
|
|
272
|
+
"--find-renames",
|
|
273
|
+
baseCommit,
|
|
274
|
+
"--",
|
|
275
|
+
selectedPath
|
|
276
|
+
], {
|
|
277
|
+
cwd: worktreePath
|
|
278
|
+
}),
|
|
279
|
+
runCommand("git", [
|
|
280
|
+
"diff",
|
|
281
|
+
"--find-renames",
|
|
282
|
+
"--unified=3",
|
|
283
|
+
baseCommit,
|
|
284
|
+
"--",
|
|
285
|
+
selectedPath
|
|
286
|
+
], {
|
|
287
|
+
cwd: worktreePath
|
|
288
|
+
})
|
|
289
|
+
]);
|
|
290
|
+
if (statResult.code !== 0) {
|
|
291
|
+
throw new Error(statResult.stderr.trim() || `Unable to build diff stat for ${selectedPath}.`);
|
|
292
|
+
}
|
|
293
|
+
if (patchResult.code !== 0) {
|
|
294
|
+
throw new Error(patchResult.stderr.trim() || `Unable to build diff patch for ${selectedPath}.`);
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
agent,
|
|
298
|
+
changedPaths,
|
|
299
|
+
selectedPath,
|
|
300
|
+
stat: statResult.stdout.trim() || "No diff stat available.",
|
|
301
|
+
patch: patchResult.stdout.trim() || "No textual patch available."
|
|
302
|
+
};
|
|
303
|
+
}
|
|
207
304
|
export async function findOverlappingWorktreePaths(worktrees, baseCommit) {
|
|
208
305
|
const pathSets = await Promise.all(worktrees.map(async (worktree)=>({
|
|
209
306
|
agent: worktree.agent,
|