@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/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, findOverlappingWorktreePaths, getHeadCommit, landBranches, resolveTargetBranch } from "./git.js";
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 commandInit(cwd, args) {
112
- const repoRoot = await detectRepoRoot(cwd);
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
- await createGitignoreEntries(repoRoot);
116
- if (args.includes("--home")) {
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
- console.log(`Initialized Kavi project scaffold in ${paths.kaviDir}`);
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 repoRoot = await detectRepoRoot(cwd);
124
- const paths = resolveAppPaths(repoRoot);
125
- await ensureProjectScaffold(paths);
126
- const checks = await runDoctor(repoRoot, paths);
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 repoRoot = await detectRepoRoot(cwd);
143
- const paths = resolveAppPaths(repoRoot);
144
- await ensureProjectScaffold(paths);
145
- await createGitignoreEntries(repoRoot);
146
- await ensureHomeConfig(paths);
147
- await ensureStartupReady(repoRoot, paths);
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 appendCommand(paths, "kickoff", {
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 baseCommit = await getHeadCommit(repoRoot);
214
+ const bootstrapCommit = await ensureBootstrapCommit(repoRoot);
215
+ const baseCommit = bootstrapCommit.commit;
168
216
  const sessionId = buildSessionId();
169
- const rpcEndpoint = "file://session-state";
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 session = await loadSessionRecord(paths);
242
- const pendingApprovals = await listApprovalRequests(paths);
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 detectRepoRoot(cwd);
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 session = await loadSessionRecord(paths);
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
- await appendCommand(paths, "enqueue", {
349
- owner: routeDecision.owner,
350
- prompt,
351
- routeReason: routeDecision.reason,
352
- claimedPaths: routeDecision.claimedPaths,
353
- routeStrategy: routeDecision.strategy,
354
- routeConfidence: routeDecision.confidence
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 session = await loadSessionRecord(paths);
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 session = await loadSessionRecord(paths);
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 requests = await listApprovalRequests(paths, {
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 requests = await listApprovalRequests(paths, {
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 = await resolveApprovalRequest(paths, requestId, decision, remember);
479
- const session = await loadSessionRecord(paths);
480
- addDecisionRecord(session, {
481
- kind: "approval",
482
- agent: request.agent,
483
- summary: `${decision === "allow" ? "Approved" : "Denied"} ${request.toolName}`,
484
- detail: request.summary,
485
- metadata: {
486
- requestId: request.id,
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
- toolName: request.toolName
489
- }
490
- });
491
- await saveSessionRecord(paths, session);
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 events = await readRecentEvents(paths, Number.isFinite(limit) ? limit : 20);
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 appendCommand(paths, "shutdown", {});
521
- await recordEvent(paths, session.id, "daemon.stop_requested", {});
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(runtimeDir, "kavid.sock"),
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"),