@mandipadk7/kavi 0.1.6 → 0.1.7

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 CHANGED
@@ -3,10 +3,12 @@
3
3
  Kavi is a local terminal control plane for managed Codex and Claude collaboration.
4
4
 
5
5
  Current capabilities:
6
+ - `kavi version` and `kavi --version`: print the installed package version.
6
7
  - `kavi init`: create repo-local `.kavi` config, prompt files, ignore rules, and bootstrap git if the folder is not already a repository.
7
8
  - `kavi init --home`: also scaffold the user-local config file used for binary overrides.
8
9
  - `kavi init --no-commit`: skip the bootstrap commit and let `kavi open` or `kavi start` create the first base commit later.
9
10
  - `kavi doctor`: verify Node, Codex, Claude, git worktree support, and local readiness.
11
+ - `kavi update`: check for and install a newer published Kavi package from npm.
10
12
  - `kavi start`: start a managed session without attaching the TUI.
11
13
  - `kavi open`: create a managed session with separate Codex and Claude worktrees and open the full-screen operator console, even from an empty folder or a repo with no `HEAD` yet.
12
14
  - `kavi resume`: reopen the operator console for the current repo session.
@@ -15,6 +17,8 @@ Current capabilities:
15
17
  - `kavi routes`: inspect recent task routing decisions with strategy, confidence, and metadata.
16
18
  - `kavi paths`: inspect resolved repo-local, user-local, worktree, and runtime paths.
17
19
  - `kavi task`: enqueue a task for `codex`, `claude`, or `auto` routing.
20
+ - `kavi recommend`: inspect integration, handoff, and ownership-configuration recommendations derived from live claims, reviews, and routing state.
21
+ - `kavi recommend-apply`: turn an actionable handoff or integration recommendation into a queued managed task.
18
22
  - `kavi tasks`: inspect the session task list with summaries and artifact availability.
19
23
  - `kavi task-output`: inspect the normalized envelope and raw output for a completed task.
20
24
  - `kavi decisions`: inspect the persisted routing, approval, task, and integration decisions.
@@ -50,8 +54,10 @@ Notes:
50
54
  - The console is keyboard-driven: `1-7` switch views, `j/k` move selection, `[` and `]` cycle task detail sections, `,` and `.` cycle changed files, `{` and `}` cycle patch hunks, `A/C/Q/M` add review notes, `o/O` cycle existing threads, `T` reply, `E` edit, `R` resolve or reopen, `a` cycle thread assignee, `w` mark a thread as won't-fix, `x` mark it as accepted-risk, `F` queue a fix task, `H` queue a handoff task, `y/n` resolve approvals, and `c` opens the inline task composer with live route preview diagnostics.
51
55
  - Review filters are available both in the CLI and the TUI: `kavi reviews --assignee operator --status open`, and inside the console use `u`, `v`, and `d` to cycle assignee, status, and disposition filters for the active diff context.
52
56
  - The claims inspector now shows active overlap hotspots and ownership-rule conflicts so routing pressure points are visible directly in the operator surface.
57
+ - The claims and decision inspectors now also show recommendation-driven next actions, so hotspots, cross-agent review pressure, and ownership-config problems can be turned into follow-up tasks directly from the operator surface.
53
58
  - Review threads now carry explicit assignees and richer dispositions, including `accepted risk` and `won't fix`, instead of relying only on free-form note text.
54
59
  - Successful follow-up tasks now auto-resolve linked open review threads, landed follow-up work marks those resolved threads as landed, and replying to a resolved thread reopens it.
60
+ - `kavi update --check` reports newer published builds without installing them, and `kavi update` can apply a chosen `latest`, `beta`, or exact version after confirmation.
55
61
 
56
62
  Install commands for testers:
57
63
 
package/dist/main.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs/promises";
3
+ import readline from "node:readline/promises";
3
4
  import process from "node:process";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { createApprovalRequest, describeToolUse, findApprovalRule, listApprovalRequests, resolveApprovalRequest, waitForApprovalDecision } from "./approvals.js";
@@ -10,9 +11,11 @@ import { addDecisionRecord, buildClaimHotspots, releasePathClaims, upsertPathCla
10
11
  import { runDoctor } from "./doctor.js";
11
12
  import { writeJson } from "./fs.js";
12
13
  import { createGitignoreEntries, detectRepoRoot, ensureBootstrapCommit, ensureGitRepository, ensureWorktrees, findRepoRoot, findOverlappingWorktreePaths, landBranches, resolveTargetBranch } from "./git.js";
14
+ import { loadPackageInfo } from "./package-info.js";
13
15
  import { buildSessionId, resolveAppPaths } from "./paths.js";
14
- import { isProcessAlive, spawnDetachedNode } from "./process.js";
16
+ import { isProcessAlive, runCommand, runInteractiveCommand, spawnDetachedNode } from "./process.js";
15
17
  import { pingRpc, readSnapshot, rpcEnqueueTask, rpcNotifyExternalUpdate, rpcKickoff, rpcRecentEvents, rpcResolveApproval, rpcShutdown, rpcTaskArtifact } from "./rpc.js";
18
+ import { buildOperatorRecommendations } from "./recommendations.js";
16
19
  import { findOwnershipRuleConflicts } from "./ownership.js";
17
20
  import { filterReviewNotes, markReviewNotesLandedForTasks } from "./reviews.js";
18
21
  import { resolveSessionRuntime } from "./runtime.js";
@@ -20,6 +23,7 @@ import { buildAdHocTask, extractPromptPathHints, previewRouteDecision, routeTask
20
23
  import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
21
24
  import { listTaskArtifacts, loadTaskArtifact, saveTaskArtifact } from "./task-artifacts.js";
22
25
  import { attachTui } from "./tui.js";
26
+ import { buildUpdatePlan, parseRegistryVersion } from "./update.js";
23
27
  const HEARTBEAT_STALE_MS = 10_000;
24
28
  const CLAUDE_AUTO_ALLOW_TOOLS = new Set([
25
29
  "Read",
@@ -65,11 +69,29 @@ async function readStdinText() {
65
69
  }
66
70
  return content;
67
71
  }
72
+ async function confirmAction(prompt) {
73
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
74
+ return false;
75
+ }
76
+ const rl = readline.createInterface({
77
+ input: process.stdin,
78
+ output: process.stdout
79
+ });
80
+ try {
81
+ const answer = await rl.question(`${prompt} [y/N]: `);
82
+ const normalized = answer.trim().toLowerCase();
83
+ return normalized === "y" || normalized === "yes";
84
+ } finally{
85
+ rl.close();
86
+ }
87
+ }
68
88
  function renderUsage() {
69
89
  return [
70
90
  "Usage:",
91
+ " kavi version",
71
92
  " kavi init [--home] [--no-commit]",
72
93
  " kavi doctor [--json]",
94
+ " kavi update [--check] [--dry-run] [--yes] [--tag latest|beta] [--version X.Y.Z]",
73
95
  " kavi start [--goal \"...\"]",
74
96
  " kavi open [--goal \"...\"]",
75
97
  " kavi resume",
@@ -78,6 +100,8 @@ function renderUsage() {
78
100
  " kavi routes [--json] [--limit N]",
79
101
  " kavi paths [--json]",
80
102
  " kavi task [--agent codex|claude|auto] <prompt>",
103
+ " kavi recommend [--json]",
104
+ " kavi recommend-apply <recommendation-id>",
81
105
  " kavi tasks [--json]",
82
106
  " kavi task-output <task-id|latest> [--json]",
83
107
  " kavi decisions [--json] [--limit N]",
@@ -92,6 +116,19 @@ function renderUsage() {
92
116
  " kavi help"
93
117
  ].join("\n");
94
118
  }
119
+ async function commandVersion(args) {
120
+ const packageInfo = await loadPackageInfo();
121
+ const payload = {
122
+ name: packageInfo.name,
123
+ version: packageInfo.version,
124
+ node: process.version
125
+ };
126
+ if (args.includes("--json")) {
127
+ console.log(JSON.stringify(payload, null, 2));
128
+ return;
129
+ }
130
+ console.log(`${packageInfo.name} ${packageInfo.version}`);
131
+ }
95
132
  function isSessionLive(session) {
96
133
  if (session.status !== "running") {
97
134
  return false;
@@ -196,6 +233,108 @@ async function commandDoctor(cwd, args) {
196
233
  }
197
234
  process.exitCode = failed ? 1 : 0;
198
235
  }
236
+ async function commandUpdate(cwd, args) {
237
+ const packageInfo = await loadPackageInfo();
238
+ const tag = getOptionalFilter(args, "--tag");
239
+ const version = getOptionalFilter(args, "--version");
240
+ const plan = buildUpdatePlan(packageInfo.name, {
241
+ tag,
242
+ version
243
+ });
244
+ const repoRoot = await findRepoRoot(cwd) ?? cwd;
245
+ const paths = resolveAppPaths(repoRoot);
246
+ const hasSession = await sessionExists(paths);
247
+ const session = hasSession ? await loadSessionRecord(paths) : null;
248
+ const registry = await runCommand("npm", plan.viewArgs, {
249
+ cwd: repoRoot
250
+ });
251
+ if (registry.code !== 0) {
252
+ const detail = registry.stderr.trim() || registry.stdout.trim() || "npm view failed";
253
+ if (session) {
254
+ await recordEvent(paths, session.id, "update.failed", {
255
+ packageName: packageInfo.name,
256
+ targetSpecifier: plan.targetSpecifier,
257
+ detail
258
+ });
259
+ }
260
+ throw new Error(`Unable to resolve update target for ${packageInfo.name}@${plan.targetSpecifier}: ${detail}`);
261
+ }
262
+ const targetVersion = parseRegistryVersion(registry.stdout);
263
+ if (!targetVersion) {
264
+ throw new Error(`Unable to parse npm registry version for ${packageInfo.name}@${plan.targetSpecifier}.`);
265
+ }
266
+ if (session) {
267
+ await recordEvent(paths, session.id, "update.checked", {
268
+ packageName: packageInfo.name,
269
+ currentVersion: packageInfo.version,
270
+ targetVersion,
271
+ targetSpecifier: plan.targetSpecifier
272
+ });
273
+ }
274
+ const payload = {
275
+ packageName: packageInfo.name,
276
+ currentVersion: packageInfo.version,
277
+ targetVersion,
278
+ targetSpecifier: plan.targetSpecifier,
279
+ command: [
280
+ "npm",
281
+ ...plan.installArgs
282
+ ]
283
+ };
284
+ if (args.includes("--check") || args.includes("--dry-run")) {
285
+ if (args.includes("--json")) {
286
+ console.log(JSON.stringify(payload, null, 2));
287
+ return;
288
+ }
289
+ console.log(`Package: ${payload.packageName}`);
290
+ console.log(`Current version: ${payload.currentVersion}`);
291
+ console.log(`Target version: ${payload.targetVersion}`);
292
+ console.log(`Target specifier: ${payload.targetSpecifier}`);
293
+ console.log(`Command: ${payload.command.join(" ")}`);
294
+ return;
295
+ }
296
+ if (targetVersion === packageInfo.version) {
297
+ console.log(`${packageInfo.name} is already at ${packageInfo.version}.`);
298
+ return;
299
+ }
300
+ const confirmed = args.includes("--yes") || await confirmAction(`Update ${packageInfo.name} from ${packageInfo.version} to ${targetVersion} using npm?`);
301
+ if (!confirmed) {
302
+ console.log("Update cancelled.");
303
+ return;
304
+ }
305
+ if (session) {
306
+ await recordEvent(paths, session.id, "update.started", {
307
+ packageName: packageInfo.name,
308
+ currentVersion: packageInfo.version,
309
+ targetVersion,
310
+ targetSpecifier: plan.targetSpecifier
311
+ });
312
+ }
313
+ const exitCode = await runInteractiveCommand("npm", plan.installArgs, {
314
+ cwd: repoRoot
315
+ });
316
+ if (exitCode !== 0) {
317
+ if (session) {
318
+ await recordEvent(paths, session.id, "update.failed", {
319
+ packageName: packageInfo.name,
320
+ currentVersion: packageInfo.version,
321
+ targetVersion,
322
+ targetSpecifier: plan.targetSpecifier,
323
+ exitCode
324
+ });
325
+ }
326
+ throw new Error(`npm install returned exit code ${exitCode}.`);
327
+ }
328
+ if (session) {
329
+ await recordEvent(paths, session.id, "update.completed", {
330
+ packageName: packageInfo.name,
331
+ previousVersion: packageInfo.version,
332
+ targetVersion,
333
+ targetSpecifier: plan.targetSpecifier
334
+ });
335
+ }
336
+ console.log(`Updated ${packageInfo.name} from ${packageInfo.version} to ${targetVersion}.`);
337
+ }
199
338
  async function startOrAttachSession(cwd, goal) {
200
339
  const prepared = await prepareProjectContext(cwd, {
201
340
  createRepository: true,
@@ -348,6 +487,7 @@ async function commandStatus(cwd, args) {
348
487
  const routeAnalytics = buildRouteAnalytics(session.tasks);
349
488
  const ownershipConflicts = findOwnershipRuleConflicts(session.config);
350
489
  const claimHotspots = buildClaimHotspots(session);
490
+ const recommendations = buildOperatorRecommendations(session);
351
491
  const payload = {
352
492
  id: session.id,
353
493
  status: session.status,
@@ -381,6 +521,12 @@ async function commandStatus(cwd, args) {
381
521
  pathClaimCounts: {
382
522
  active: session.pathClaims.filter((claim)=>claim.status === "active").length
383
523
  },
524
+ recommendationCounts: {
525
+ total: recommendations.length,
526
+ integration: recommendations.filter((item)=>item.kind === "integration").length,
527
+ handoff: recommendations.filter((item)=>item.kind === "handoff").length,
528
+ ownershipConfig: recommendations.filter((item)=>item.kind === "ownership-config").length
529
+ },
384
530
  routeCounts: routeAnalytics,
385
531
  ownershipConflicts: ownershipConflicts.length,
386
532
  claimHotspots: claimHotspots.length,
@@ -407,6 +553,7 @@ async function commandStatus(cwd, args) {
407
553
  console.log(`Reviews: open=${payload.reviewCounts.open} total=${payload.reviewCounts.total}`);
408
554
  console.log(`Decisions: total=${payload.decisionCounts.total}`);
409
555
  console.log(`Path claims: active=${payload.pathClaimCounts.active}`);
556
+ console.log(`Recommendations: total=${payload.recommendationCounts.total} integration=${payload.recommendationCounts.integration} handoff=${payload.recommendationCounts.handoff} ownership-config=${payload.recommendationCounts.ownershipConfig}`);
410
557
  console.log(`Routes: codex=${payload.routeCounts.byOwner.codex} claude=${payload.routeCounts.byOwner.claude} | strategies=${Object.entries(payload.routeCounts.byStrategy).map(([strategy, count])=>`${strategy}:${count}`).join(", ") || "-"}`);
411
558
  console.log(`Ownership conflicts: ${payload.ownershipConflicts} | Claim hotspots: ${payload.claimHotspots}`);
412
559
  console.log(`Routing ownership: codex=${payload.routingOwnership.codexPaths.join(", ") || "-"} | claude=${payload.routingOwnership.claudePaths.join(", ") || "-"}`);
@@ -431,6 +578,16 @@ async function commandRoute(cwd, args) {
431
578
  mode: allowAi && session ? "live" : "preview",
432
579
  route: routeDecision
433
580
  };
581
+ if (session) {
582
+ await recordEvent(paths, session.id, "route.previewed", {
583
+ prompt,
584
+ mode: payload.mode,
585
+ owner: routeDecision.owner,
586
+ strategy: routeDecision.strategy,
587
+ confidence: routeDecision.confidence,
588
+ claimedPaths: routeDecision.claimedPaths
589
+ });
590
+ }
434
591
  if (args.includes("--json")) {
435
592
  console.log(JSON.stringify(payload, null, 2));
436
593
  return;
@@ -484,6 +641,94 @@ async function commandRoutes(cwd, args) {
484
641
  }
485
642
  }
486
643
  }
644
+ async function commandRecommend(cwd, args) {
645
+ const { paths } = await requireSession(cwd);
646
+ const rpcSnapshot = await tryRpcSnapshot(paths);
647
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
648
+ const recommendations = buildOperatorRecommendations(session);
649
+ if (args.includes("--json")) {
650
+ console.log(JSON.stringify(recommendations, null, 2));
651
+ return;
652
+ }
653
+ if (recommendations.length === 0) {
654
+ console.log("No operator recommendations right now.");
655
+ return;
656
+ }
657
+ for (const recommendation of recommendations){
658
+ console.log(`${recommendation.id} | ${recommendation.kind} | ${recommendation.targetAgent ?? "-"}`);
659
+ console.log(` title: ${recommendation.title}`);
660
+ console.log(` detail: ${recommendation.detail}`);
661
+ console.log(` command: ${recommendation.commandHint}`);
662
+ }
663
+ }
664
+ async function commandRecommendApply(cwd, args) {
665
+ const { paths } = await requireSession(cwd);
666
+ const rpcSnapshot = await tryRpcSnapshot(paths);
667
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
668
+ const recommendationId = args.find((arg)=>!arg.startsWith("--"));
669
+ if (!recommendationId) {
670
+ throw new Error("A recommendation id is required. Example: kavi recommend-apply integration:src/ui/App.tsx");
671
+ }
672
+ const recommendation = buildOperatorRecommendations(session).find((item)=>item.id === recommendationId);
673
+ if (!recommendation) {
674
+ throw new Error(`Recommendation ${recommendationId} was not found.`);
675
+ }
676
+ if (recommendation.kind === "ownership-config") {
677
+ throw new Error(`Recommendation ${recommendation.id} is advisory only. Run "${recommendation.commandHint}" and update the config manually.`);
678
+ }
679
+ const owner = recommendation.targetAgent === "operator" || recommendation.targetAgent === null ? "codex" : recommendation.targetAgent;
680
+ const prompt = recommendation.kind === "integration" ? [
681
+ "Coordinate and resolve overlapping agent work before landing.",
682
+ recommendation.detail,
683
+ recommendation.filePath ? `Primary hotspot: ${recommendation.filePath}` : null
684
+ ].filter(Boolean).join("\n") : [
685
+ "Pick up ownership-aware handoff work from Kavi.",
686
+ recommendation.detail,
687
+ recommendation.filePath ? `Focus path: ${recommendation.filePath}` : null
688
+ ].filter(Boolean).join("\n");
689
+ const routeDecision = {
690
+ owner,
691
+ strategy: "manual",
692
+ confidence: 1,
693
+ reason: `Queued from Kavi recommendation ${recommendation.id}.`,
694
+ claimedPaths: recommendation.filePath ? [
695
+ recommendation.filePath
696
+ ] : [],
697
+ metadata: {
698
+ recommendationId: recommendation.id,
699
+ recommendationKind: recommendation.kind
700
+ }
701
+ };
702
+ if (rpcSnapshot) {
703
+ await rpcEnqueueTask(paths, {
704
+ owner: routeDecision.owner,
705
+ prompt,
706
+ routeReason: routeDecision.reason,
707
+ routeMetadata: routeDecision.metadata,
708
+ claimedPaths: routeDecision.claimedPaths,
709
+ routeStrategy: routeDecision.strategy,
710
+ routeConfidence: routeDecision.confidence
711
+ });
712
+ } else {
713
+ await appendCommand(paths, "enqueue", {
714
+ owner: routeDecision.owner,
715
+ prompt,
716
+ routeReason: routeDecision.reason,
717
+ routeMetadata: routeDecision.metadata,
718
+ claimedPaths: routeDecision.claimedPaths,
719
+ routeStrategy: routeDecision.strategy,
720
+ routeConfidence: routeDecision.confidence
721
+ });
722
+ }
723
+ await recordEvent(paths, session.id, "recommendation.applied", {
724
+ recommendationId: recommendation.id,
725
+ recommendationKind: recommendation.kind,
726
+ owner,
727
+ filePath: recommendation.filePath
728
+ });
729
+ await notifyOperatorSurface(paths, "recommendation.applied");
730
+ console.log(`Queued ${owner} task from recommendation ${recommendation.id}.`);
731
+ }
487
732
  async function commandPaths(cwd, args) {
488
733
  const repoRoot = await findRepoRoot(cwd) ?? cwd;
489
734
  const paths = resolveAppPaths(repoRoot);
@@ -1113,6 +1358,11 @@ async function main() {
1113
1358
  const [command = "open", ...args] = process.argv.slice(2);
1114
1359
  const cwd = process.cwd();
1115
1360
  switch(command){
1361
+ case "version":
1362
+ case "--version":
1363
+ case "-v":
1364
+ await commandVersion(args);
1365
+ break;
1116
1366
  case "help":
1117
1367
  case "--help":
1118
1368
  case "-h":
@@ -1124,6 +1374,9 @@ async function main() {
1124
1374
  case "doctor":
1125
1375
  await commandDoctor(cwd, args);
1126
1376
  break;
1377
+ case "update":
1378
+ await commandUpdate(cwd, args);
1379
+ break;
1127
1380
  case "start":
1128
1381
  await commandStart(cwd, args);
1129
1382
  break;
@@ -1148,6 +1401,12 @@ async function main() {
1148
1401
  case "task":
1149
1402
  await commandTask(cwd, args);
1150
1403
  break;
1404
+ case "recommend":
1405
+ await commandRecommend(cwd, args);
1406
+ break;
1407
+ case "recommend-apply":
1408
+ await commandRecommendApply(cwd, args);
1409
+ break;
1151
1410
  case "tasks":
1152
1411
  await commandTasks(cwd, args);
1153
1412
  break;
@@ -0,0 +1,15 @@
1
+ import fs from "node:fs/promises";
2
+ const packageJsonUrl = new URL("../package.json", import.meta.url);
3
+ export async function loadPackageInfo() {
4
+ const raw = await fs.readFile(packageJsonUrl, "utf8");
5
+ const parsed = JSON.parse(raw);
6
+ return {
7
+ name: typeof parsed.name === "string" ? parsed.name : "kavi",
8
+ version: typeof parsed.version === "string" ? parsed.version : "0.0.0",
9
+ description: typeof parsed.description === "string" ? parsed.description : "",
10
+ homepage: typeof parsed.homepage === "string" ? parsed.homepage : null
11
+ };
12
+ }
13
+
14
+
15
+ //# sourceURL=package-info.ts
package/dist/process.js CHANGED
@@ -44,6 +44,22 @@ export async function runCommand(command, args, options = {}) {
44
44
  });
45
45
  });
46
46
  }
47
+ export async function runInteractiveCommand(command, args, options = {}) {
48
+ return await new Promise((resolve, reject)=>{
49
+ const child = spawn(command, args, {
50
+ ...options,
51
+ stdio: "inherit"
52
+ });
53
+ child.on("error", reject);
54
+ child.on("close", (code, signal)=>{
55
+ if (signal) {
56
+ reject(new Error(`${command} ${args.join(" ")} exited on signal ${signal}`));
57
+ return;
58
+ }
59
+ resolve(code ?? 1);
60
+ });
61
+ });
62
+ }
47
63
  export function spawnDetachedNode(nodeExecutable, args, cwd) {
48
64
  const child = spawn(nodeExecutable, [
49
65
  "--experimental-strip-types",
@@ -0,0 +1,139 @@
1
+ import { buildClaimHotspots } from "./decision-ledger.js";
2
+ import { findOwnershipRuleConflicts } from "./ownership.js";
3
+ function normalizePath(value) {
4
+ return value.trim().replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
5
+ }
6
+ function pathMatchesScope(scope, filePath) {
7
+ const normalizedScope = normalizePath(scope);
8
+ const normalizedFile = normalizePath(filePath);
9
+ return normalizedScope === normalizedFile || normalizedScope.startsWith(`${normalizedFile}/`) || normalizedFile.startsWith(`${normalizedScope}/`);
10
+ }
11
+ function otherAgent(agent) {
12
+ return agent === "codex" ? "claude" : "codex";
13
+ }
14
+ export function buildOperatorRecommendations(session) {
15
+ const recommendations = [];
16
+ const hotspots = buildClaimHotspots(session);
17
+ const ownershipConflicts = findOwnershipRuleConflicts(session.config);
18
+ const openReviewNotes = session.reviewNotes.filter((note)=>note.status === "open");
19
+ for (const hotspot of hotspots){
20
+ const relatedNotes = openReviewNotes.filter((note)=>pathMatchesScope(hotspot.path, note.filePath));
21
+ recommendations.push({
22
+ id: `integration:${hotspot.path}`,
23
+ kind: "integration",
24
+ title: `Coordinate overlapping work on ${hotspot.path}`,
25
+ detail: relatedNotes.length > 0 ? `Multiple agents are touching ${hotspot.path}, and ${relatedNotes.length} open review note(s) are still active there.` : `Multiple agents still claim overlapping work on ${hotspot.path}.`,
26
+ targetAgent: "codex",
27
+ filePath: hotspot.path,
28
+ taskIds: hotspot.taskIds,
29
+ reviewNoteIds: relatedNotes.map((note)=>note.id),
30
+ commandHint: `kavi recommend-apply integration:${hotspot.path}`,
31
+ metadata: {
32
+ hotspot
33
+ }
34
+ });
35
+ }
36
+ for (const note of openReviewNotes){
37
+ if (!note.assignee || note.assignee === "operator" || note.assignee === note.agent) {
38
+ continue;
39
+ }
40
+ recommendations.push({
41
+ id: `handoff:${note.id}:${note.assignee}`,
42
+ kind: "handoff",
43
+ title: `Hand off ${note.filePath} review work to ${note.assignee}`,
44
+ detail: `Review note ${note.id} is assigned to ${note.assignee} even though it originated from ${note.agent}.`,
45
+ targetAgent: note.assignee,
46
+ filePath: note.filePath,
47
+ taskIds: note.taskId ? [
48
+ note.taskId
49
+ ] : [],
50
+ reviewNoteIds: [
51
+ note.id
52
+ ],
53
+ commandHint: `kavi recommend-apply handoff:${note.id}:${note.assignee}`,
54
+ metadata: {
55
+ reviewNoteId: note.id,
56
+ sourceAgent: note.agent,
57
+ targetAgent: note.assignee
58
+ }
59
+ });
60
+ }
61
+ for (const hotspot of hotspots){
62
+ const hotspotAgents = new Set(hotspot.agents);
63
+ for (const note of openReviewNotes){
64
+ if (!pathMatchesScope(hotspot.path, note.filePath)) {
65
+ continue;
66
+ }
67
+ const targetAgent = note.assignee && note.assignee !== "operator" ? note.assignee : otherAgent(note.agent);
68
+ if (hotspotAgents.has(targetAgent)) {
69
+ recommendations.push({
70
+ id: `handoff:${note.id}:${targetAgent}`,
71
+ kind: "handoff",
72
+ title: `Ask ${targetAgent} to address ${note.filePath}`,
73
+ detail: `Active hotspot pressure on ${hotspot.path} overlaps review note ${note.id}.`,
74
+ targetAgent,
75
+ filePath: note.filePath,
76
+ taskIds: note.taskId ? [
77
+ note.taskId
78
+ ] : hotspot.taskIds,
79
+ reviewNoteIds: [
80
+ note.id
81
+ ],
82
+ commandHint: `kavi recommend-apply handoff:${note.id}:${targetAgent}`,
83
+ metadata: {
84
+ hotspotPath: hotspot.path,
85
+ reviewNoteId: note.id,
86
+ sourceAgent: note.agent,
87
+ targetAgent
88
+ }
89
+ });
90
+ }
91
+ }
92
+ }
93
+ for (const conflict of ownershipConflicts){
94
+ recommendations.push({
95
+ id: `ownership:${conflict.leftPattern}:${conflict.rightPattern}`,
96
+ kind: "ownership-config",
97
+ title: "Resolve overlapping ownership rules",
98
+ detail: conflict.detail,
99
+ targetAgent: "operator",
100
+ filePath: null,
101
+ taskIds: [],
102
+ reviewNoteIds: [],
103
+ commandHint: "kavi doctor",
104
+ metadata: {
105
+ conflict
106
+ }
107
+ });
108
+ }
109
+ const deduped = new Map();
110
+ for (const recommendation of recommendations){
111
+ if (!deduped.has(recommendation.id)) {
112
+ deduped.set(recommendation.id, recommendation);
113
+ }
114
+ }
115
+ const priority = (kind)=>{
116
+ switch(kind){
117
+ case "integration":
118
+ return 0;
119
+ case "handoff":
120
+ return 1;
121
+ case "ownership-config":
122
+ return 2;
123
+ default:
124
+ return 3;
125
+ }
126
+ };
127
+ return [
128
+ ...deduped.values()
129
+ ].sort((left, right)=>{
130
+ const kindDelta = priority(left.kind) - priority(right.kind);
131
+ if (kindDelta !== 0) {
132
+ return kindDelta;
133
+ }
134
+ return left.title.localeCompare(right.title);
135
+ });
136
+ }
137
+
138
+
139
+ //# sourceURL=recommendations.ts
package/dist/tui.js CHANGED
@@ -3,6 +3,7 @@ import readline from "node:readline";
3
3
  import process from "node:process";
4
4
  import { buildClaimHotspots } from "./decision-ledger.js";
5
5
  import { findOwnershipRuleConflicts } from "./ownership.js";
6
+ import { buildOperatorRecommendations } from "./recommendations.js";
6
7
  import { cycleReviewAssignee, reviewNoteMatchesFilters } from "./reviews.js";
7
8
  import { extractPromptPathHints, previewRouteDecision, routeTask } from "./router.js";
8
9
  import { pingRpc, rpcAddReviewNote, rpcAddReviewReply, rpcEnqueueReviewFollowUp, readSnapshot, rpcEnqueueTask, rpcResolveApproval, rpcSetReviewNoteStatus, rpcShutdown, rpcTaskArtifact, rpcUpdateReviewNote, rpcWorktreeDiff, subscribeSnapshotRpc } from "./rpc.js";
@@ -670,6 +671,18 @@ function composerRoutePreview(snapshot, composer) {
670
671
  }
671
672
  };
672
673
  }
674
+ function recommendationsForClaim(snapshot, claim) {
675
+ if (!snapshot || !claim) {
676
+ return [];
677
+ }
678
+ return buildOperatorRecommendations(snapshot.session).filter((recommendation)=>recommendation.taskIds.includes(claim.taskId) || claim.paths.length > 0 && recommendation.filePath !== null && claim.paths.some((filePath)=>recommendation.filePath === filePath || recommendation.filePath.startsWith(`${filePath}/`) || filePath.startsWith(`${recommendation.filePath}/`)));
679
+ }
680
+ function recommendationsForDecision(snapshot, decision) {
681
+ if (!snapshot || !decision) {
682
+ return [];
683
+ }
684
+ return buildOperatorRecommendations(snapshot.session).filter((recommendation)=>decision.taskId !== null && recommendation.taskIds.includes(decision.taskId) || typeof decision.metadata?.hotspot === "string" && recommendation.filePath === decision.metadata.hotspot);
685
+ }
673
686
  function syncSelectedReviewNote(snapshot, ui) {
674
687
  const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui), ui.reviewFilters);
675
688
  ui.selectedReviewNoteId = notes.some((note)=>note.id === ui.selectedReviewNoteId) ? ui.selectedReviewNoteId : notes[0]?.id ?? null;
@@ -969,6 +982,7 @@ function renderClaimInspector(snapshot, claim, width, height) {
969
982
  const innerWidth = Math.max(16, width - 2);
970
983
  const hotspots = snapshot ? buildClaimHotspots(snapshot.session) : [];
971
984
  const ownershipConflicts = snapshot ? findOwnershipRuleConflicts(snapshot.session.config) : [];
985
+ const recommendations = recommendationsForClaim(snapshot, claim);
972
986
  const lines = [];
973
987
  if (claim) {
974
988
  lines.push(...section("Claim", [
@@ -993,17 +1007,21 @@ function renderClaimInspector(snapshot, claim, width, height) {
993
1007
  ]), ...section("Ownership Conflicts", ownershipConflicts.length > 0 ? ownershipConflicts.slice(0, 6).flatMap((conflict)=>wrapText(`- [${conflict.kind}] ${conflict.leftOwner}:${conflict.leftPattern} <> ${conflict.rightOwner}:${conflict.rightPattern}`, innerWidth)) : [
994
1008
  "- none"
995
1009
  ]));
1010
+ lines.push(...section("Recommended Actions", recommendations.length > 0 ? recommendations.slice(0, 4).flatMap((recommendation)=>wrapText(`- ${recommendation.title} | ${recommendation.commandHint}`, innerWidth)) : [
1011
+ "- none"
1012
+ ]));
996
1013
  return renderPanel("Inspector | Claim", width, height, lines, {
997
1014
  focused: true
998
1015
  });
999
1016
  }
1000
- function renderDecisionInspector(decision, width, height) {
1017
+ function renderDecisionInspector(snapshot, decision, width, height) {
1001
1018
  if (!decision) {
1002
1019
  return renderPanel("Inspector | Decision", width, height, [
1003
1020
  styleLine("No decision selected.", "muted")
1004
1021
  ]);
1005
1022
  }
1006
1023
  const innerWidth = Math.max(16, width - 2);
1024
+ const recommendations = recommendationsForDecision(snapshot, decision);
1007
1025
  const lines = [
1008
1026
  ...section("Decision", [
1009
1027
  `Id: ${decision.id}`,
@@ -1014,7 +1032,10 @@ function renderDecisionInspector(decision, width, height) {
1014
1032
  `Summary: ${decision.summary}`,
1015
1033
  `Detail: ${decision.detail}`
1016
1034
  ].flatMap((line)=>wrapText(line, innerWidth))),
1017
- ...section("Metadata", formatJson(decision.metadata, innerWidth))
1035
+ ...section("Metadata", formatJson(decision.metadata, innerWidth)),
1036
+ ...section("Recommended Actions", recommendations.length > 0 ? recommendations.slice(0, 4).flatMap((recommendation)=>wrapText(`- ${recommendation.title} | ${recommendation.commandHint}`, innerWidth)) : [
1037
+ "- none"
1038
+ ])
1018
1039
  ];
1019
1040
  return renderPanel("Inspector | Decision", width, height, lines, {
1020
1041
  focused: true
@@ -1138,7 +1159,7 @@ function renderInspector(snapshot, ui, width, height) {
1138
1159
  case "claims":
1139
1160
  return renderClaimInspector(snapshot, selectedClaim(snapshot, ui), width, height);
1140
1161
  case "decisions":
1141
- return renderDecisionInspector(selectedDecision(snapshot, ui), width, height);
1162
+ return renderDecisionInspector(snapshot, selectedDecision(snapshot, ui), width, height);
1142
1163
  case "events":
1143
1164
  return renderEventInspector(selectedEvent(snapshot, ui), width, height);
1144
1165
  case "messages":
package/dist/update.js ADDED
@@ -0,0 +1,36 @@
1
+ export function buildUpdatePlan(packageName, options = {}) {
2
+ const targetSpecifier = options.version ?? options.tag ?? "latest";
3
+ return {
4
+ targetSpecifier,
5
+ installArgs: [
6
+ "install",
7
+ "-g",
8
+ `${packageName}@${targetSpecifier}`
9
+ ],
10
+ viewArgs: [
11
+ "view",
12
+ `${packageName}@${targetSpecifier}`,
13
+ "version",
14
+ "--json"
15
+ ]
16
+ };
17
+ }
18
+ export function parseRegistryVersion(stdout) {
19
+ const trimmed = stdout.trim();
20
+ if (!trimmed) {
21
+ return null;
22
+ }
23
+ try {
24
+ const parsed = JSON.parse(trimmed);
25
+ if (typeof parsed === "string") {
26
+ return parsed;
27
+ }
28
+ if (Array.isArray(parsed) && typeof parsed[0] === "string") {
29
+ return parsed[0];
30
+ }
31
+ } catch {}
32
+ return trimmed.replaceAll(/^"+|"+$/g, "");
33
+ }
34
+
35
+
36
+ //# sourceURL=update.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandipadk7/kavi",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Managed Codex + Claude collaboration TUI",
5
5
  "type": "module",
6
6
  "preferGlobal": true,