@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 +6 -0
- package/dist/main.js +260 -1
- package/dist/package-info.js +15 -0
- package/dist/process.js +16 -0
- package/dist/recommendations.js +139 -0
- package/dist/tui.js +24 -3
- package/dist/update.js +36 -0
- package/package.json +1 -1
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
|