@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/main.js
CHANGED
|
@@ -9,13 +9,15 @@ import { KaviDaemon } from "./daemon.js";
|
|
|
9
9
|
import { addDecisionRecord, upsertPathClaim } from "./decision-ledger.js";
|
|
10
10
|
import { runDoctor } from "./doctor.js";
|
|
11
11
|
import { writeJson } from "./fs.js";
|
|
12
|
-
import { createGitignoreEntries, detectRepoRoot, ensureWorktrees,
|
|
12
|
+
import { createGitignoreEntries, detectRepoRoot, ensureBootstrapCommit, ensureGitRepository, ensureWorktrees, findRepoRoot, findOverlappingWorktreePaths, landBranches, resolveTargetBranch } from "./git.js";
|
|
13
13
|
import { buildSessionId, resolveAppPaths } from "./paths.js";
|
|
14
14
|
import { isProcessAlive, spawnDetachedNode } from "./process.js";
|
|
15
|
+
import { pingRpc, readSnapshot, rpcEnqueueTask, rpcNotifyExternalUpdate, rpcKickoff, rpcRecentEvents, rpcResolveApproval, rpcShutdown, rpcTaskArtifact } from "./rpc.js";
|
|
16
|
+
import { markReviewNotesLandedForTasks } from "./reviews.js";
|
|
15
17
|
import { resolveSessionRuntime } from "./runtime.js";
|
|
16
18
|
import { buildAdHocTask, extractPromptPathHints, routeTask } from "./router.js";
|
|
17
|
-
import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
|
|
18
|
-
import { listTaskArtifacts, loadTaskArtifact } from "./task-artifacts.js";
|
|
19
|
+
import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
|
|
20
|
+
import { listTaskArtifacts, loadTaskArtifact, saveTaskArtifact } from "./task-artifacts.js";
|
|
19
21
|
import { attachTui } from "./tui.js";
|
|
20
22
|
const HEARTBEAT_STALE_MS = 10_000;
|
|
21
23
|
const CLAUDE_AUTO_ALLOW_TOOLS = new Set([
|
|
@@ -58,7 +60,7 @@ async function readStdinText() {
|
|
|
58
60
|
function renderUsage() {
|
|
59
61
|
return [
|
|
60
62
|
"Usage:",
|
|
61
|
-
" kavi init [--home]",
|
|
63
|
+
" kavi init [--home] [--no-commit]",
|
|
62
64
|
" kavi doctor [--json]",
|
|
63
65
|
" kavi start [--goal \"...\"]",
|
|
64
66
|
" kavi open [--goal \"...\"]",
|
|
@@ -68,6 +70,9 @@ function renderUsage() {
|
|
|
68
70
|
" kavi task [--agent codex|claude|auto] <prompt>",
|
|
69
71
|
" kavi tasks [--json]",
|
|
70
72
|
" kavi task-output <task-id|latest> [--json]",
|
|
73
|
+
" kavi decisions [--json] [--limit N]",
|
|
74
|
+
" kavi claims [--json] [--all]",
|
|
75
|
+
" kavi reviews [--json] [--all]",
|
|
71
76
|
" kavi approvals [--json] [--all]",
|
|
72
77
|
" kavi approve <request-id|latest> [--remember]",
|
|
73
78
|
" kavi deny <request-id|latest> [--remember]",
|
|
@@ -96,7 +101,7 @@ async function waitForSession(paths, expectedState = "running") {
|
|
|
96
101
|
try {
|
|
97
102
|
if (await sessionExists(paths)) {
|
|
98
103
|
const session = await loadSessionRecord(paths);
|
|
99
|
-
if (expectedState === "running" && isSessionLive(session)) {
|
|
104
|
+
if (expectedState === "running" && isSessionLive(session) && await pingRpc(paths)) {
|
|
100
105
|
return;
|
|
101
106
|
}
|
|
102
107
|
if (expectedState === "stopped" && session.status === "stopped") {
|
|
@@ -108,22 +113,65 @@ async function waitForSession(paths, expectedState = "running") {
|
|
|
108
113
|
}
|
|
109
114
|
throw new Error(`Timed out waiting for session state ${expectedState} in ${paths.stateFile}.`);
|
|
110
115
|
}
|
|
111
|
-
async function
|
|
112
|
-
|
|
116
|
+
async function prepareProjectContext(cwd, options) {
|
|
117
|
+
let repoRoot;
|
|
118
|
+
let createdRepository = false;
|
|
119
|
+
let hasGitRepository = false;
|
|
120
|
+
if (options.createRepository) {
|
|
121
|
+
const repository = await ensureGitRepository(cwd);
|
|
122
|
+
repoRoot = repository.repoRoot;
|
|
123
|
+
createdRepository = repository.createdRepository;
|
|
124
|
+
hasGitRepository = true;
|
|
125
|
+
} else {
|
|
126
|
+
const existingRepoRoot = await findRepoRoot(cwd);
|
|
127
|
+
repoRoot = existingRepoRoot ?? cwd;
|
|
128
|
+
hasGitRepository = existingRepoRoot !== null;
|
|
129
|
+
}
|
|
113
130
|
const paths = resolveAppPaths(repoRoot);
|
|
114
131
|
await ensureProjectScaffold(paths);
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
if (hasGitRepository) {
|
|
133
|
+
await createGitignoreEntries(repoRoot);
|
|
134
|
+
}
|
|
135
|
+
if (options.ensureHomeConfig) {
|
|
117
136
|
await ensureHomeConfig(paths);
|
|
118
|
-
console.log(`Initialized user-local Kavi config in ${paths.homeConfigFile}`);
|
|
119
137
|
}
|
|
120
|
-
|
|
138
|
+
return {
|
|
139
|
+
repoRoot,
|
|
140
|
+
paths,
|
|
141
|
+
createdRepository,
|
|
142
|
+
bootstrapCommit: options.ensureHeadCommit ? await ensureBootstrapCommit(repoRoot) : null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function commandInit(cwd, args) {
|
|
146
|
+
const skipCommit = args.includes("--no-commit");
|
|
147
|
+
const prepared = await prepareProjectContext(cwd, {
|
|
148
|
+
createRepository: true,
|
|
149
|
+
ensureHeadCommit: !skipCommit,
|
|
150
|
+
ensureHomeConfig: args.includes("--home")
|
|
151
|
+
});
|
|
152
|
+
if (args.includes("--home")) {
|
|
153
|
+
console.log(`Initialized user-local Kavi config in ${prepared.paths.homeConfigFile}`);
|
|
154
|
+
}
|
|
155
|
+
if (prepared.createdRepository) {
|
|
156
|
+
console.log(`Initialized git repository in ${prepared.repoRoot}`);
|
|
157
|
+
}
|
|
158
|
+
console.log(`Initialized Kavi project scaffold in ${prepared.paths.kaviDir}`);
|
|
159
|
+
if (skipCommit) {
|
|
160
|
+
console.log("Skipped bootstrap commit creation (--no-commit).");
|
|
161
|
+
console.log('Kavi will create the first base commit automatically on "kavi open" or "kavi start".');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (prepared.bootstrapCommit?.createdCommit) {
|
|
165
|
+
console.log(`Created bootstrap commit ${prepared.bootstrapCommit.commit.slice(0, 12)} with ${prepared.bootstrapCommit.stagedPaths.length} tracked path${prepared.bootstrapCommit.stagedPaths.length === 1 ? "" : "s"}.`);
|
|
166
|
+
}
|
|
121
167
|
}
|
|
122
168
|
async function commandDoctor(cwd, args) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
169
|
+
const prepared = await prepareProjectContext(cwd, {
|
|
170
|
+
createRepository: false,
|
|
171
|
+
ensureHeadCommit: false,
|
|
172
|
+
ensureHomeConfig: false
|
|
173
|
+
});
|
|
174
|
+
const checks = await runDoctor(prepared.repoRoot, prepared.paths);
|
|
127
175
|
if (args.includes("--json")) {
|
|
128
176
|
console.log(JSON.stringify(checks, null, 2));
|
|
129
177
|
process.exitCode = checks.some((check)=>!check.ok) ? 1 : 0;
|
|
@@ -139,20 +187,18 @@ async function commandDoctor(cwd, args) {
|
|
|
139
187
|
process.exitCode = failed ? 1 : 0;
|
|
140
188
|
}
|
|
141
189
|
async function startOrAttachSession(cwd, goal) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
190
|
+
const prepared = await prepareProjectContext(cwd, {
|
|
191
|
+
createRepository: true,
|
|
192
|
+
ensureHeadCommit: false,
|
|
193
|
+
ensureHomeConfig: true
|
|
194
|
+
});
|
|
195
|
+
const { repoRoot, paths } = prepared;
|
|
148
196
|
if (await sessionExists(paths)) {
|
|
149
197
|
try {
|
|
150
198
|
const session = await loadSessionRecord(paths);
|
|
151
|
-
if (isSessionLive(session)) {
|
|
199
|
+
if (isSessionLive(session) && await pingRpc(paths)) {
|
|
152
200
|
if (goal) {
|
|
153
|
-
await
|
|
154
|
-
prompt: goal
|
|
155
|
-
});
|
|
201
|
+
await rpcKickoff(paths, goal);
|
|
156
202
|
}
|
|
157
203
|
return session.socketPath;
|
|
158
204
|
}
|
|
@@ -162,14 +208,27 @@ async function startOrAttachSession(cwd, goal) {
|
|
|
162
208
|
});
|
|
163
209
|
} catch {}
|
|
164
210
|
}
|
|
211
|
+
await ensureStartupReady(repoRoot, paths);
|
|
165
212
|
const config = await loadConfig(paths);
|
|
166
213
|
const runtime = await resolveSessionRuntime(paths);
|
|
167
|
-
const
|
|
214
|
+
const bootstrapCommit = await ensureBootstrapCommit(repoRoot);
|
|
215
|
+
const baseCommit = bootstrapCommit.commit;
|
|
168
216
|
const sessionId = buildSessionId();
|
|
169
|
-
const rpcEndpoint =
|
|
217
|
+
const rpcEndpoint = paths.socketPath;
|
|
170
218
|
await fs.writeFile(paths.commandsFile, "", "utf8");
|
|
171
219
|
const worktrees = await ensureWorktrees(repoRoot, paths, sessionId, config, baseCommit);
|
|
172
220
|
await createSessionRecord(paths, config, runtime, sessionId, baseCommit, worktrees, goal, rpcEndpoint);
|
|
221
|
+
if (prepared.createdRepository) {
|
|
222
|
+
await recordEvent(paths, sessionId, "repo.initialized", {
|
|
223
|
+
repoRoot
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (bootstrapCommit.createdCommit) {
|
|
227
|
+
await recordEvent(paths, sessionId, "repo.bootstrap_committed", {
|
|
228
|
+
commit: bootstrapCommit.commit,
|
|
229
|
+
stagedPaths: bootstrapCommit.stagedPaths
|
|
230
|
+
});
|
|
231
|
+
}
|
|
173
232
|
const pid = spawnDetachedNode(runtime.nodeExecutable, [
|
|
174
233
|
fileURLToPath(import.meta.url),
|
|
175
234
|
"__daemon",
|
|
@@ -210,6 +269,20 @@ async function requireSession(cwd) {
|
|
|
210
269
|
paths
|
|
211
270
|
};
|
|
212
271
|
}
|
|
272
|
+
async function tryRpcSnapshot(paths) {
|
|
273
|
+
if (!await pingRpc(paths)) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
return await readSnapshot(paths);
|
|
277
|
+
}
|
|
278
|
+
async function notifyOperatorSurface(paths, reason) {
|
|
279
|
+
if (!await pingRpc(paths)) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
await rpcNotifyExternalUpdate(paths, reason);
|
|
284
|
+
} catch {}
|
|
285
|
+
}
|
|
213
286
|
async function commandOpen(cwd, args) {
|
|
214
287
|
const goal = getGoal(args);
|
|
215
288
|
await startOrAttachSession(cwd, goal);
|
|
@@ -224,8 +297,8 @@ async function commandResume(cwd) {
|
|
|
224
297
|
}
|
|
225
298
|
async function commandStart(cwd, args) {
|
|
226
299
|
const goal = getGoal(args);
|
|
227
|
-
const repoRoot = await detectRepoRoot(cwd);
|
|
228
300
|
const socketPath = await startOrAttachSession(cwd, goal);
|
|
301
|
+
const repoRoot = await detectRepoRoot(cwd);
|
|
229
302
|
const paths = resolveAppPaths(repoRoot);
|
|
230
303
|
const session = await loadSessionRecord(paths);
|
|
231
304
|
console.log(`Started Kavi session ${session.id}`);
|
|
@@ -238,17 +311,20 @@ async function commandStart(cwd, args) {
|
|
|
238
311
|
}
|
|
239
312
|
async function commandStatus(cwd, args) {
|
|
240
313
|
const { paths } = await requireSession(cwd);
|
|
241
|
-
const
|
|
242
|
-
const
|
|
314
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
315
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
316
|
+
const pendingApprovals = rpcSnapshot?.approvals.filter((item)=>item.status === "pending") ?? await listApprovalRequests(paths);
|
|
243
317
|
const heartbeatAgeMs = sessionHeartbeatAgeMs(session);
|
|
244
318
|
const payload = {
|
|
245
319
|
id: session.id,
|
|
246
320
|
status: session.status,
|
|
247
321
|
repoRoot: session.repoRoot,
|
|
322
|
+
socketPath: session.socketPath,
|
|
248
323
|
goal: session.goal,
|
|
249
324
|
daemonPid: session.daemonPid,
|
|
250
325
|
daemonHeartbeatAt: session.daemonHeartbeatAt,
|
|
251
326
|
daemonHealthy: isSessionLive(session),
|
|
327
|
+
rpcConnected: rpcSnapshot !== null,
|
|
252
328
|
heartbeatAgeMs,
|
|
253
329
|
runtime: session.runtime,
|
|
254
330
|
taskCounts: {
|
|
@@ -265,6 +341,10 @@ async function commandStatus(cwd, args) {
|
|
|
265
341
|
decisionCounts: {
|
|
266
342
|
total: session.decisions.length
|
|
267
343
|
},
|
|
344
|
+
reviewCounts: {
|
|
345
|
+
open: session.reviewNotes.filter((note)=>note.status === "open").length,
|
|
346
|
+
total: session.reviewNotes.length
|
|
347
|
+
},
|
|
268
348
|
pathClaimCounts: {
|
|
269
349
|
active: session.pathClaims.filter((claim)=>claim.status === "active").length
|
|
270
350
|
},
|
|
@@ -277,12 +357,14 @@ async function commandStatus(cwd, args) {
|
|
|
277
357
|
console.log(`Session: ${payload.id}`);
|
|
278
358
|
console.log(`Status: ${payload.status}${payload.daemonHealthy ? " (healthy)" : " (stale or stopped)"}`);
|
|
279
359
|
console.log(`Repo: ${payload.repoRoot}`);
|
|
360
|
+
console.log(`Control: ${payload.socketPath}${payload.rpcConnected ? " (connected)" : " (disconnected)"}`);
|
|
280
361
|
console.log(`Goal: ${payload.goal ?? "-"}`);
|
|
281
362
|
console.log(`Daemon PID: ${payload.daemonPid ?? "-"}`);
|
|
282
363
|
console.log(`Heartbeat: ${payload.daemonHeartbeatAt ?? "-"}${heartbeatAgeMs === null ? "" : ` (${heartbeatAgeMs} ms ago)`}`);
|
|
283
364
|
console.log(`Runtime: node=${payload.runtime.nodeExecutable} codex=${payload.runtime.codexExecutable} claude=${payload.runtime.claudeExecutable}`);
|
|
284
365
|
console.log(`Tasks: total=${payload.taskCounts.total} pending=${payload.taskCounts.pending} running=${payload.taskCounts.running} blocked=${payload.taskCounts.blocked} completed=${payload.taskCounts.completed} failed=${payload.taskCounts.failed}`);
|
|
285
366
|
console.log(`Approvals: pending=${payload.approvalCounts.pending}`);
|
|
367
|
+
console.log(`Reviews: open=${payload.reviewCounts.open} total=${payload.reviewCounts.total}`);
|
|
286
368
|
console.log(`Decisions: total=${payload.decisionCounts.total}`);
|
|
287
369
|
console.log(`Path claims: active=${payload.pathClaimCounts.active}`);
|
|
288
370
|
for (const worktree of payload.worktrees){
|
|
@@ -290,7 +372,7 @@ async function commandStatus(cwd, args) {
|
|
|
290
372
|
}
|
|
291
373
|
}
|
|
292
374
|
async function commandPaths(cwd, args) {
|
|
293
|
-
const repoRoot = await
|
|
375
|
+
const repoRoot = await findRepoRoot(cwd) ?? cwd;
|
|
294
376
|
const paths = resolveAppPaths(repoRoot);
|
|
295
377
|
const runtime = await resolveSessionRuntime(paths);
|
|
296
378
|
const payload = {
|
|
@@ -305,6 +387,7 @@ async function commandPaths(cwd, args) {
|
|
|
305
387
|
eventsFile: paths.eventsFile,
|
|
306
388
|
approvalsFile: paths.approvalsFile,
|
|
307
389
|
commandsFile: paths.commandsFile,
|
|
390
|
+
socketPath: paths.socketPath,
|
|
308
391
|
runsDir: paths.runsDir,
|
|
309
392
|
claudeSettingsFile: paths.claudeSettingsFile,
|
|
310
393
|
homeApprovalRulesFile: paths.homeApprovalRulesFile,
|
|
@@ -325,6 +408,7 @@ async function commandPaths(cwd, args) {
|
|
|
325
408
|
console.log(`Events file: ${payload.eventsFile}`);
|
|
326
409
|
console.log(`Approvals file: ${payload.approvalsFile}`);
|
|
327
410
|
console.log(`Command queue: ${payload.commandsFile}`);
|
|
411
|
+
console.log(`Control socket: ${payload.socketPath}`);
|
|
328
412
|
console.log(`Task artifacts: ${payload.runsDir}`);
|
|
329
413
|
console.log(`Claude settings: ${payload.claudeSettingsFile}`);
|
|
330
414
|
console.log(`Approval rules: ${payload.homeApprovalRulesFile}`);
|
|
@@ -332,7 +416,8 @@ async function commandPaths(cwd, args) {
|
|
|
332
416
|
}
|
|
333
417
|
async function commandTask(cwd, args) {
|
|
334
418
|
const { paths } = await requireSession(cwd);
|
|
335
|
-
const
|
|
419
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
420
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
336
421
|
const requestedAgent = getFlag(args, "--agent");
|
|
337
422
|
const prompt = getGoal(args.filter((arg, index)=>arg !== "--agent" && args[index - 1] !== "--agent"));
|
|
338
423
|
if (!prompt) {
|
|
@@ -345,14 +430,25 @@ async function commandTask(cwd, args) {
|
|
|
345
430
|
reason: `User explicitly assigned the task to ${requestedAgent}.`,
|
|
346
431
|
claimedPaths: extractPromptPathHints(prompt)
|
|
347
432
|
} : await routeTask(prompt, session, paths);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
433
|
+
if (rpcSnapshot) {
|
|
434
|
+
await rpcEnqueueTask(paths, {
|
|
435
|
+
owner: routeDecision.owner,
|
|
436
|
+
prompt,
|
|
437
|
+
routeReason: routeDecision.reason,
|
|
438
|
+
claimedPaths: routeDecision.claimedPaths,
|
|
439
|
+
routeStrategy: routeDecision.strategy,
|
|
440
|
+
routeConfidence: routeDecision.confidence
|
|
441
|
+
});
|
|
442
|
+
} else {
|
|
443
|
+
await appendCommand(paths, "enqueue", {
|
|
444
|
+
owner: routeDecision.owner,
|
|
445
|
+
prompt,
|
|
446
|
+
routeReason: routeDecision.reason,
|
|
447
|
+
claimedPaths: routeDecision.claimedPaths,
|
|
448
|
+
routeStrategy: routeDecision.strategy,
|
|
449
|
+
routeConfidence: routeDecision.confidence
|
|
450
|
+
});
|
|
451
|
+
}
|
|
356
452
|
await recordEvent(paths, session.id, "task.cli_enqueued", {
|
|
357
453
|
owner: routeDecision.owner,
|
|
358
454
|
prompt,
|
|
@@ -364,7 +460,8 @@ async function commandTask(cwd, args) {
|
|
|
364
460
|
}
|
|
365
461
|
async function commandTasks(cwd, args) {
|
|
366
462
|
const { paths } = await requireSession(cwd);
|
|
367
|
-
const
|
|
463
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
464
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
368
465
|
const artifacts = await listTaskArtifacts(paths);
|
|
369
466
|
const artifactMap = new Map(artifacts.map((artifact)=>[
|
|
370
467
|
artifact.taskId,
|
|
@@ -411,12 +508,13 @@ function resolveRequestedTaskId(args, knownTaskIds) {
|
|
|
411
508
|
}
|
|
412
509
|
async function commandTaskOutput(cwd, args) {
|
|
413
510
|
const { paths } = await requireSession(cwd);
|
|
414
|
-
const
|
|
511
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
512
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
415
513
|
const sortedTasks = [
|
|
416
514
|
...session.tasks
|
|
417
515
|
].sort((left, right)=>left.updatedAt.localeCompare(right.updatedAt));
|
|
418
516
|
const taskId = resolveRequestedTaskId(args, sortedTasks.map((task)=>task.id));
|
|
419
|
-
const artifact = await loadTaskArtifact(paths, taskId);
|
|
517
|
+
const artifact = rpcSnapshot ? await rpcTaskArtifact(paths, taskId) : await loadTaskArtifact(paths, taskId);
|
|
420
518
|
if (!artifact) {
|
|
421
519
|
throw new Error(`No task artifact found for ${taskId}.`);
|
|
422
520
|
}
|
|
@@ -430,15 +528,104 @@ async function commandTaskOutput(cwd, args) {
|
|
|
430
528
|
console.log(`Started: ${artifact.startedAt}`);
|
|
431
529
|
console.log(`Finished: ${artifact.finishedAt}`);
|
|
432
530
|
console.log(`Summary: ${artifact.summary ?? "-"}`);
|
|
531
|
+
console.log(`Route: ${artifact.routeReason ?? "-"}`);
|
|
532
|
+
console.log(`Claimed paths: ${artifact.claimedPaths.join(", ") || "-"}`);
|
|
433
533
|
console.log(`Error: ${artifact.error ?? "-"}`);
|
|
534
|
+
console.log("Decision Replay:");
|
|
535
|
+
for (const line of artifact.decisionReplay){
|
|
536
|
+
console.log(line);
|
|
537
|
+
}
|
|
434
538
|
console.log("Envelope:");
|
|
435
539
|
console.log(JSON.stringify(artifact.envelope, null, 2));
|
|
540
|
+
console.log("Review Notes:");
|
|
541
|
+
if (artifact.reviewNotes.length === 0) {
|
|
542
|
+
console.log("-");
|
|
543
|
+
} else {
|
|
544
|
+
for (const note of artifact.reviewNotes){
|
|
545
|
+
console.log(`${note.createdAt} | ${note.disposition} | ${note.status} | ${note.filePath}${note.hunkIndex === null ? "" : ` | hunk ${note.hunkIndex + 1}`}`);
|
|
546
|
+
console.log(` comments: ${note.comments.length}`);
|
|
547
|
+
for (const [index, comment] of note.comments.entries()){
|
|
548
|
+
console.log(` ${index === 0 ? "root" : `reply-${index}`}: ${comment.body}`);
|
|
549
|
+
}
|
|
550
|
+
console.log(` landed: ${note.landedAt ?? "-"}`);
|
|
551
|
+
console.log(` follow-ups: ${note.followUpTaskIds.join(", ") || "-"}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
436
554
|
console.log("Raw Output:");
|
|
437
555
|
console.log(artifact.rawOutput ?? "");
|
|
438
556
|
}
|
|
557
|
+
async function commandDecisions(cwd, args) {
|
|
558
|
+
const { paths } = await requireSession(cwd);
|
|
559
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
560
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
561
|
+
const limitArg = getFlag(args, "--limit");
|
|
562
|
+
const limit = limitArg ? Number(limitArg) : 20;
|
|
563
|
+
const decisions = [
|
|
564
|
+
...session.decisions
|
|
565
|
+
].sort((left, right)=>left.createdAt.localeCompare(right.createdAt)).slice(-Math.max(1, Number.isFinite(limit) ? limit : 20));
|
|
566
|
+
if (args.includes("--json")) {
|
|
567
|
+
console.log(JSON.stringify(decisions, null, 2));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (decisions.length === 0) {
|
|
571
|
+
console.log("No decisions recorded.");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
for (const decision of decisions){
|
|
575
|
+
console.log(`${decision.createdAt} | ${decision.kind} | ${decision.agent ?? "-"} | ${decision.summary}`);
|
|
576
|
+
console.log(` task: ${decision.taskId ?? "-"}`);
|
|
577
|
+
console.log(` detail: ${decision.detail}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function commandClaims(cwd, args) {
|
|
581
|
+
const { paths } = await requireSession(cwd);
|
|
582
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
583
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
584
|
+
const claims = args.includes("--all") ? session.pathClaims : session.pathClaims.filter((claim)=>claim.status === "active");
|
|
585
|
+
if (args.includes("--json")) {
|
|
586
|
+
console.log(JSON.stringify(claims, null, 2));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (claims.length === 0) {
|
|
590
|
+
console.log("No path claims recorded.");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
for (const claim of claims){
|
|
594
|
+
console.log(`${claim.id} | ${claim.agent} | ${claim.status} | ${claim.source} | ${claim.paths.join(", ") || "-"}`);
|
|
595
|
+
console.log(` task: ${claim.taskId}`);
|
|
596
|
+
console.log(` updated: ${claim.updatedAt}`);
|
|
597
|
+
console.log(` note: ${claim.note ?? "-"}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async function commandReviews(cwd, args) {
|
|
601
|
+
const { paths } = await requireSession(cwd);
|
|
602
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
603
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
604
|
+
const notes = args.includes("--all") ? [
|
|
605
|
+
...session.reviewNotes
|
|
606
|
+
] : session.reviewNotes.filter((note)=>note.status === "open");
|
|
607
|
+
if (args.includes("--json")) {
|
|
608
|
+
console.log(JSON.stringify(notes, null, 2));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (notes.length === 0) {
|
|
612
|
+
console.log("No review notes recorded.");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
for (const note of notes){
|
|
616
|
+
console.log(`${note.id} | ${note.agent} | ${note.status} | ${note.disposition} | ${note.filePath}${note.hunkIndex === null ? "" : ` | hunk ${note.hunkIndex + 1}`}`);
|
|
617
|
+
console.log(` task: ${note.taskId ?? "-"}`);
|
|
618
|
+
console.log(` updated: ${note.updatedAt}`);
|
|
619
|
+
console.log(` comments: ${note.comments.length}`);
|
|
620
|
+
console.log(` landed: ${note.landedAt ?? "-"}`);
|
|
621
|
+
console.log(` follow-ups: ${note.followUpTaskIds.join(", ") || "-"}`);
|
|
622
|
+
console.log(` body: ${note.body}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
439
625
|
async function commandApprovals(cwd, args) {
|
|
440
626
|
const { paths } = await requireSession(cwd);
|
|
441
|
-
const
|
|
627
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
628
|
+
const requests = rpcSnapshot ? rpcSnapshot.approvals.filter((request)=>args.includes("--all") || request.status === "pending") : await listApprovalRequests(paths, {
|
|
442
629
|
includeResolved: args.includes("--all")
|
|
443
630
|
});
|
|
444
631
|
if (args.includes("--json")) {
|
|
@@ -469,40 +656,54 @@ function resolveApprovalRequestId(requests, requested) {
|
|
|
469
656
|
}
|
|
470
657
|
async function commandResolveApproval(cwd, args, decision) {
|
|
471
658
|
const { paths } = await requireSession(cwd);
|
|
472
|
-
const
|
|
659
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
660
|
+
const requests = rpcSnapshot?.approvals ?? await listApprovalRequests(paths, {
|
|
473
661
|
includeResolved: true
|
|
474
662
|
});
|
|
475
663
|
const requestedId = args.find((arg)=>!arg.startsWith("--")) ?? "latest";
|
|
476
664
|
const requestId = resolveApprovalRequestId(requests, requestedId);
|
|
477
665
|
const remember = args.includes("--remember");
|
|
478
|
-
const request =
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
666
|
+
const request = requests.find((item)=>item.id === requestId);
|
|
667
|
+
if (!request) {
|
|
668
|
+
throw new Error(`Approval request ${requestId} not found.`);
|
|
669
|
+
}
|
|
670
|
+
if (rpcSnapshot) {
|
|
671
|
+
await rpcResolveApproval(paths, {
|
|
672
|
+
requestId,
|
|
673
|
+
decision,
|
|
674
|
+
remember
|
|
675
|
+
});
|
|
676
|
+
} else {
|
|
677
|
+
const resolved = await resolveApprovalRequest(paths, requestId, decision, remember);
|
|
678
|
+
const session = await loadSessionRecord(paths);
|
|
679
|
+
addDecisionRecord(session, {
|
|
680
|
+
kind: "approval",
|
|
681
|
+
agent: resolved.agent,
|
|
682
|
+
summary: `${decision === "allow" ? "Approved" : "Denied"} ${resolved.toolName}`,
|
|
683
|
+
detail: resolved.summary,
|
|
684
|
+
metadata: {
|
|
685
|
+
requestId: resolved.id,
|
|
686
|
+
remember,
|
|
687
|
+
toolName: resolved.toolName
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
await saveSessionRecord(paths, session);
|
|
691
|
+
await recordEvent(paths, session.id, "approval.resolved", {
|
|
692
|
+
requestId: resolved.id,
|
|
693
|
+
decision,
|
|
487
694
|
remember,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
await recordEvent(paths, session.id, "approval.resolved", {
|
|
493
|
-
requestId: request.id,
|
|
494
|
-
decision,
|
|
495
|
-
remember,
|
|
496
|
-
agent: request.agent,
|
|
497
|
-
toolName: request.toolName
|
|
498
|
-
});
|
|
695
|
+
agent: resolved.agent,
|
|
696
|
+
toolName: resolved.toolName
|
|
697
|
+
});
|
|
698
|
+
}
|
|
499
699
|
console.log(`${decision === "allow" ? "Approved" : "Denied"} ${request.id}: ${request.summary}${remember ? " (remembered)" : ""}`);
|
|
500
700
|
}
|
|
501
701
|
async function commandEvents(cwd, args) {
|
|
502
702
|
const { paths } = await requireSession(cwd);
|
|
503
703
|
const limitArg = getFlag(args, "--limit");
|
|
504
704
|
const limit = limitArg ? Number(limitArg) : 20;
|
|
505
|
-
const
|
|
705
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
706
|
+
const events = rpcSnapshot ? await rpcRecentEvents(paths, Number.isFinite(limit) ? limit : 20) : await readRecentEvents(paths, Number.isFinite(limit) ? limit : 20);
|
|
506
707
|
for (const event of events){
|
|
507
708
|
console.log(`${event.timestamp} ${event.type} ${JSON.stringify(event.payload)}`);
|
|
508
709
|
}
|
|
@@ -517,8 +718,12 @@ async function commandStop(cwd) {
|
|
|
517
718
|
console.log(`Marked stale Kavi session ${session.id} as stopped`);
|
|
518
719
|
return;
|
|
519
720
|
}
|
|
520
|
-
await
|
|
521
|
-
|
|
721
|
+
if (await pingRpc(paths)) {
|
|
722
|
+
await rpcShutdown(paths);
|
|
723
|
+
} else {
|
|
724
|
+
await appendCommand(paths, "shutdown", {});
|
|
725
|
+
await recordEvent(paths, session.id, "daemon.stop_requested", {});
|
|
726
|
+
}
|
|
522
727
|
await waitForSession(paths, "stopped");
|
|
523
728
|
console.log(`Stopped Kavi session ${session.id}`);
|
|
524
729
|
}
|
|
@@ -580,6 +785,46 @@ async function commandLand(cwd) {
|
|
|
580
785
|
snapshotCommits: result.snapshotCommits,
|
|
581
786
|
commands: result.commandsRun
|
|
582
787
|
});
|
|
788
|
+
const landedReviewNotes = markReviewNotesLandedForTasks(session, session.tasks.filter((task)=>task.status === "completed").map((task)=>task.id));
|
|
789
|
+
for (const note of landedReviewNotes){
|
|
790
|
+
addDecisionRecord(session, {
|
|
791
|
+
kind: "review",
|
|
792
|
+
agent: note.agent,
|
|
793
|
+
taskId: note.taskId,
|
|
794
|
+
summary: `Marked review note ${note.id} as landed`,
|
|
795
|
+
detail: `Follow-up work for ${note.filePath} is now part of ${targetBranch}.`,
|
|
796
|
+
metadata: {
|
|
797
|
+
reviewNoteId: note.id,
|
|
798
|
+
filePath: note.filePath,
|
|
799
|
+
landedAt: note.landedAt,
|
|
800
|
+
targetBranch
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
await recordEvent(paths, session.id, "review.note_landed", {
|
|
804
|
+
reviewNoteId: note.id,
|
|
805
|
+
taskId: note.taskId,
|
|
806
|
+
followUpTaskIds: note.followUpTaskIds,
|
|
807
|
+
agent: note.agent,
|
|
808
|
+
filePath: note.filePath,
|
|
809
|
+
landedAt: note.landedAt,
|
|
810
|
+
targetBranch
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
await saveSessionRecord(paths, session);
|
|
814
|
+
const artifactTaskIds = [
|
|
815
|
+
...new Set(landedReviewNotes.flatMap((note)=>note.taskId ? [
|
|
816
|
+
note.taskId
|
|
817
|
+
] : []))
|
|
818
|
+
];
|
|
819
|
+
for (const taskId of artifactTaskIds){
|
|
820
|
+
const artifact = await loadTaskArtifact(paths, taskId);
|
|
821
|
+
if (!artifact) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
artifact.reviewNotes = session.reviewNotes.filter((note)=>note.taskId === taskId);
|
|
825
|
+
await saveTaskArtifact(paths, artifact);
|
|
826
|
+
}
|
|
827
|
+
await notifyOperatorSurface(paths, "land.completed");
|
|
583
828
|
console.log(`Landed branches into ${targetBranch}`);
|
|
584
829
|
console.log(`Integration branch: ${result.integrationBranch}`);
|
|
585
830
|
console.log(`Integration worktree: ${result.integrationPath}`);
|
|
@@ -618,6 +863,7 @@ async function commandHook(args) {
|
|
|
618
863
|
toolName: descriptor.toolName,
|
|
619
864
|
summary: descriptor.summary
|
|
620
865
|
});
|
|
866
|
+
await notifyOperatorSurface(paths, "approval.auto_allowed");
|
|
621
867
|
console.log(JSON.stringify({
|
|
622
868
|
continue: true,
|
|
623
869
|
suppressOutput: true,
|
|
@@ -642,6 +888,7 @@ async function commandHook(args) {
|
|
|
642
888
|
decision: rule.decision,
|
|
643
889
|
summary: descriptor.summary
|
|
644
890
|
});
|
|
891
|
+
await notifyOperatorSurface(paths, "approval.auto_decided");
|
|
645
892
|
console.log(JSON.stringify({
|
|
646
893
|
continue: true,
|
|
647
894
|
suppressOutput: true,
|
|
@@ -666,6 +913,7 @@ async function commandHook(args) {
|
|
|
666
913
|
toolName: request.toolName,
|
|
667
914
|
summary: request.summary
|
|
668
915
|
});
|
|
916
|
+
await notifyOperatorSurface(paths, "approval.requested");
|
|
669
917
|
const resolved = await waitForApprovalDecision(paths, request.id);
|
|
670
918
|
const approved = resolved?.status === "approved";
|
|
671
919
|
const denied = resolved?.status === "denied";
|
|
@@ -674,6 +922,7 @@ async function commandHook(args) {
|
|
|
674
922
|
requestId: request.id,
|
|
675
923
|
outcome: approved ? "approved" : denied ? "denied" : "expired"
|
|
676
924
|
});
|
|
925
|
+
await notifyOperatorSurface(paths, "approval.completed");
|
|
677
926
|
console.log(JSON.stringify({
|
|
678
927
|
continue: true,
|
|
679
928
|
suppressOutput: true,
|
|
@@ -687,6 +936,7 @@ async function commandHook(args) {
|
|
|
687
936
|
}
|
|
688
937
|
if (session) {
|
|
689
938
|
await recordEvent(paths, session.id, "claude.hook", hookPayload);
|
|
939
|
+
await notifyOperatorSurface(paths, "claude.hook");
|
|
690
940
|
}
|
|
691
941
|
console.log(JSON.stringify({
|
|
692
942
|
continue: true
|
|
@@ -731,6 +981,15 @@ async function main() {
|
|
|
731
981
|
case "task-output":
|
|
732
982
|
await commandTaskOutput(cwd, args);
|
|
733
983
|
break;
|
|
984
|
+
case "decisions":
|
|
985
|
+
await commandDecisions(cwd, args);
|
|
986
|
+
break;
|
|
987
|
+
case "claims":
|
|
988
|
+
await commandClaims(cwd, args);
|
|
989
|
+
break;
|
|
990
|
+
case "reviews":
|
|
991
|
+
await commandReviews(cwd, args);
|
|
992
|
+
break;
|
|
734
993
|
case "approvals":
|
|
735
994
|
await commandApprovals(cwd, args);
|
|
736
995
|
break;
|
package/dist/paths.js
CHANGED
|
@@ -23,7 +23,7 @@ export function resolveAppPaths(repoRoot) {
|
|
|
23
23
|
approvalsFile: path.join(stateDir, "approvals.json"),
|
|
24
24
|
commandsFile: path.join(runtimeDir, "commands.jsonl"),
|
|
25
25
|
claudeSettingsFile: path.join(runtimeDir, "claude.settings.json"),
|
|
26
|
-
socketPath: path.join(
|
|
26
|
+
socketPath: path.join(homeStateDir, "sockets", `${safeRepoId}.sock`),
|
|
27
27
|
homeConfigDir,
|
|
28
28
|
homeConfigFile: path.join(homeConfigDir, "config.toml"),
|
|
29
29
|
homeApprovalRulesFile: path.join(homeConfigDir, "approval-rules.json"),
|