@mandipadk7/kavi 0.1.7 → 1.0.0
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 +18 -5
- package/dist/adapters/claude.js +4 -1
- package/dist/config.js +4 -3
- package/dist/daemon.js +77 -3
- package/dist/git.js +61 -6
- package/dist/main.js +339 -65
- package/dist/paths.js +2 -0
- package/dist/recommendations.js +205 -19
- package/dist/reports.js +118 -0
- package/dist/rpc.js +20 -1
- package/dist/session.js +11 -0
- package/dist/tui.js +354 -37
- package/dist/workflow.js +450 -0
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -10,12 +10,13 @@ import { KaviDaemon } from "./daemon.js";
|
|
|
10
10
|
import { addDecisionRecord, buildClaimHotspots, releasePathClaims, upsertPathClaim } from "./decision-ledger.js";
|
|
11
11
|
import { runDoctor } from "./doctor.js";
|
|
12
12
|
import { writeJson } from "./fs.js";
|
|
13
|
-
import { createGitignoreEntries, detectRepoRoot, ensureBootstrapCommit, ensureGitRepository, ensureWorktrees, findRepoRoot, findOverlappingWorktreePaths, landBranches, resolveTargetBranch } from "./git.js";
|
|
13
|
+
import { createGitignoreEntries, detectRepoRoot, ensureBootstrapCommit, ensureGitRepository, ensureWorktrees, findRepoRoot, findOverlappingWorktreePaths, getBranchCommit, landBranches, listWorktreeChangedPaths, resolveTargetBranch } from "./git.js";
|
|
14
14
|
import { loadPackageInfo } from "./package-info.js";
|
|
15
15
|
import { buildSessionId, resolveAppPaths } from "./paths.js";
|
|
16
16
|
import { isProcessAlive, runCommand, runInteractiveCommand, spawnDetachedNode } from "./process.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { buildLandReport, loadLatestLandReport, saveLandReport } from "./reports.js";
|
|
18
|
+
import { pingRpc, readSnapshot, rpcDismissRecommendation, rpcEnqueueTask, rpcNotifyExternalUpdate, rpcKickoff, rpcRecentEvents, rpcResolveApproval, rpcRestoreRecommendation, rpcShutdown, rpcTaskArtifact } from "./rpc.js";
|
|
19
|
+
import { buildOperatorRecommendations, buildRecommendationActionPlan, dismissOperatorRecommendation, restoreOperatorRecommendation } from "./recommendations.js";
|
|
19
20
|
import { findOwnershipRuleConflicts } from "./ownership.js";
|
|
20
21
|
import { filterReviewNotes, markReviewNotesLandedForTasks } from "./reviews.js";
|
|
21
22
|
import { resolveSessionRuntime } from "./runtime.js";
|
|
@@ -24,6 +25,7 @@ import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent,
|
|
|
24
25
|
import { listTaskArtifacts, loadTaskArtifact, saveTaskArtifact } from "./task-artifacts.js";
|
|
25
26
|
import { attachTui } from "./tui.js";
|
|
26
27
|
import { buildUpdatePlan, parseRegistryVersion } from "./update.js";
|
|
28
|
+
import { buildWorkflowActivity, buildWorkflowResult, buildWorkflowSummary } from "./workflow.js";
|
|
27
29
|
const HEARTBEAT_STALE_MS = 10_000;
|
|
28
30
|
const CLAUDE_AUTO_ALLOW_TOOLS = new Set([
|
|
29
31
|
"Read",
|
|
@@ -58,6 +60,24 @@ function getOptionalFilter(args, name) {
|
|
|
58
60
|
}
|
|
59
61
|
return value;
|
|
60
62
|
}
|
|
63
|
+
function parseRecommendationKind(value) {
|
|
64
|
+
if (!value) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (value === "all" || value === "handoff" || value === "integration" || value === "ownership-config") {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Unsupported recommendation kind "${value}".`);
|
|
71
|
+
}
|
|
72
|
+
function parseRecommendationStatus(value) {
|
|
73
|
+
if (!value) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (value === "all" || value === "active" || value === "dismissed") {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Unsupported recommendation status "${value}".`);
|
|
80
|
+
}
|
|
61
81
|
async function readStdinText() {
|
|
62
82
|
if (process.stdin.isTTY) {
|
|
63
83
|
return "";
|
|
@@ -95,13 +115,18 @@ function renderUsage() {
|
|
|
95
115
|
" kavi start [--goal \"...\"]",
|
|
96
116
|
" kavi open [--goal \"...\"]",
|
|
97
117
|
" kavi resume",
|
|
118
|
+
" kavi summary [--json]",
|
|
119
|
+
" kavi result [--json]",
|
|
98
120
|
" kavi status [--json]",
|
|
121
|
+
" kavi activity [--json] [--limit N]",
|
|
99
122
|
" kavi route [--json] [--no-ai] <prompt>",
|
|
100
123
|
" kavi routes [--json] [--limit N]",
|
|
101
124
|
" kavi paths [--json]",
|
|
102
125
|
" kavi task [--agent codex|claude|auto] <prompt>",
|
|
103
|
-
" kavi recommend [--json]",
|
|
104
|
-
" kavi recommend-apply <recommendation-id>",
|
|
126
|
+
" kavi recommend [--json] [--all] [--kind handoff|integration|ownership-config] [--status active|dismissed] [--agent codex|claude|operator]",
|
|
127
|
+
" kavi recommend-apply <recommendation-id> [--force]",
|
|
128
|
+
" kavi recommend-dismiss <recommendation-id> [--reason \"...\"]",
|
|
129
|
+
" kavi recommend-restore <recommendation-id>",
|
|
105
130
|
" kavi tasks [--json]",
|
|
106
131
|
" kavi task-output <task-id|latest> [--json]",
|
|
107
132
|
" kavi decisions [--json] [--limit N]",
|
|
@@ -425,6 +450,29 @@ async function tryRpcSnapshot(paths) {
|
|
|
425
450
|
}
|
|
426
451
|
return await readSnapshot(paths);
|
|
427
452
|
}
|
|
453
|
+
async function loadSnapshot(paths, eventLimit = 80) {
|
|
454
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
455
|
+
if (rpcSnapshot) {
|
|
456
|
+
return rpcSnapshot;
|
|
457
|
+
}
|
|
458
|
+
const session = await loadSessionRecord(paths);
|
|
459
|
+
const approvals = await listApprovalRequests(paths, {
|
|
460
|
+
includeResolved: true
|
|
461
|
+
});
|
|
462
|
+
const events = await readRecentEvents(paths, eventLimit);
|
|
463
|
+
const worktreeDiffs = await Promise.all(session.worktrees.map(async (worktree)=>({
|
|
464
|
+
agent: worktree.agent,
|
|
465
|
+
paths: await listWorktreeChangedPaths(worktree.path, session.baseCommit)
|
|
466
|
+
})));
|
|
467
|
+
const latestLandReport = await loadLatestLandReport(paths);
|
|
468
|
+
return {
|
|
469
|
+
session,
|
|
470
|
+
approvals,
|
|
471
|
+
events,
|
|
472
|
+
worktreeDiffs,
|
|
473
|
+
latestLandReport
|
|
474
|
+
};
|
|
475
|
+
}
|
|
428
476
|
async function notifyOperatorSurface(paths, reason) {
|
|
429
477
|
if (!await pingRpc(paths)) {
|
|
430
478
|
return;
|
|
@@ -480,14 +528,17 @@ async function commandStart(cwd, args) {
|
|
|
480
528
|
}
|
|
481
529
|
async function commandStatus(cwd, args) {
|
|
482
530
|
const { paths } = await requireSession(cwd);
|
|
483
|
-
const
|
|
484
|
-
const session =
|
|
485
|
-
const pendingApprovals =
|
|
531
|
+
const snapshot = await loadSnapshot(paths, 60);
|
|
532
|
+
const session = snapshot.session;
|
|
533
|
+
const pendingApprovals = snapshot.approvals.filter((item)=>item.status === "pending");
|
|
486
534
|
const heartbeatAgeMs = sessionHeartbeatAgeMs(session);
|
|
487
535
|
const routeAnalytics = buildRouteAnalytics(session.tasks);
|
|
488
536
|
const ownershipConflicts = findOwnershipRuleConflicts(session.config);
|
|
489
537
|
const claimHotspots = buildClaimHotspots(session);
|
|
490
|
-
const recommendations = buildOperatorRecommendations(session
|
|
538
|
+
const recommendations = buildOperatorRecommendations(session, {
|
|
539
|
+
includeDismissed: true
|
|
540
|
+
});
|
|
541
|
+
const workflowSummary = buildWorkflowSummary(snapshot);
|
|
491
542
|
const payload = {
|
|
492
543
|
id: session.id,
|
|
493
544
|
status: session.status,
|
|
@@ -497,8 +548,14 @@ async function commandStatus(cwd, args) {
|
|
|
497
548
|
daemonPid: session.daemonPid,
|
|
498
549
|
daemonHeartbeatAt: session.daemonHeartbeatAt,
|
|
499
550
|
daemonHealthy: isSessionLive(session),
|
|
500
|
-
rpcConnected:
|
|
551
|
+
rpcConnected: await pingRpc(paths),
|
|
501
552
|
heartbeatAgeMs,
|
|
553
|
+
workflowStage: workflowSummary.stage,
|
|
554
|
+
latestLandReport: workflowSummary.latestLandReport ? {
|
|
555
|
+
id: workflowSummary.latestLandReport.id,
|
|
556
|
+
createdAt: workflowSummary.latestLandReport.createdAt,
|
|
557
|
+
targetBranch: workflowSummary.latestLandReport.targetBranch
|
|
558
|
+
} : null,
|
|
502
559
|
runtime: session.runtime,
|
|
503
560
|
taskCounts: {
|
|
504
561
|
total: session.tasks.length,
|
|
@@ -523,6 +580,9 @@ async function commandStatus(cwd, args) {
|
|
|
523
580
|
},
|
|
524
581
|
recommendationCounts: {
|
|
525
582
|
total: recommendations.length,
|
|
583
|
+
active: recommendations.filter((item)=>item.status === "active").length,
|
|
584
|
+
dismissed: recommendations.filter((item)=>item.status === "dismissed").length,
|
|
585
|
+
withOpenFollowUps: recommendations.filter((item)=>item.openFollowUpTaskIds.length > 0).length,
|
|
526
586
|
integration: recommendations.filter((item)=>item.kind === "integration").length,
|
|
527
587
|
handoff: recommendations.filter((item)=>item.kind === "handoff").length,
|
|
528
588
|
ownershipConfig: recommendations.filter((item)=>item.kind === "ownership-config").length
|
|
@@ -545,6 +605,10 @@ async function commandStatus(cwd, args) {
|
|
|
545
605
|
console.log(`Repo: ${payload.repoRoot}`);
|
|
546
606
|
console.log(`Control: ${payload.socketPath}${payload.rpcConnected ? " (connected)" : " (disconnected)"}`);
|
|
547
607
|
console.log(`Goal: ${payload.goal ?? "-"}`);
|
|
608
|
+
console.log(`Workflow stage: ${payload.workflowStage.label} | ${payload.workflowStage.detail}`);
|
|
609
|
+
if (payload.latestLandReport) {
|
|
610
|
+
console.log(`Latest land: ${payload.latestLandReport.createdAt} -> ${payload.latestLandReport.targetBranch}`);
|
|
611
|
+
}
|
|
548
612
|
console.log(`Daemon PID: ${payload.daemonPid ?? "-"}`);
|
|
549
613
|
console.log(`Heartbeat: ${payload.daemonHeartbeatAt ?? "-"}${heartbeatAgeMs === null ? "" : ` (${heartbeatAgeMs} ms ago)`}`);
|
|
550
614
|
console.log(`Runtime: node=${payload.runtime.nodeExecutable} codex=${payload.runtime.codexExecutable} claude=${payload.runtime.claudeExecutable}`);
|
|
@@ -553,7 +617,7 @@ async function commandStatus(cwd, args) {
|
|
|
553
617
|
console.log(`Reviews: open=${payload.reviewCounts.open} total=${payload.reviewCounts.total}`);
|
|
554
618
|
console.log(`Decisions: total=${payload.decisionCounts.total}`);
|
|
555
619
|
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}`);
|
|
620
|
+
console.log(`Recommendations: total=${payload.recommendationCounts.total} active=${payload.recommendationCounts.active} dismissed=${payload.recommendationCounts.dismissed} open-followups=${payload.recommendationCounts.withOpenFollowUps} integration=${payload.recommendationCounts.integration} handoff=${payload.recommendationCounts.handoff} ownership-config=${payload.recommendationCounts.ownershipConfig}`);
|
|
557
621
|
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(", ") || "-"}`);
|
|
558
622
|
console.log(`Ownership conflicts: ${payload.ownershipConflicts} | Claim hotspots: ${payload.claimHotspots}`);
|
|
559
623
|
console.log(`Routing ownership: codex=${payload.routingOwnership.codexPaths.join(", ") || "-"} | claude=${payload.routingOwnership.claudePaths.join(", ") || "-"}`);
|
|
@@ -561,6 +625,128 @@ async function commandStatus(cwd, args) {
|
|
|
561
625
|
console.log(`- ${worktree.agent}: ${worktree.path}`);
|
|
562
626
|
}
|
|
563
627
|
}
|
|
628
|
+
async function commandSummary(cwd, args) {
|
|
629
|
+
const { paths } = await requireSession(cwd);
|
|
630
|
+
const snapshot = await loadSnapshot(paths, 120);
|
|
631
|
+
const artifacts = await listTaskArtifacts(paths);
|
|
632
|
+
const summary = buildWorkflowSummary(snapshot, artifacts);
|
|
633
|
+
const result = buildWorkflowResult(snapshot, artifacts);
|
|
634
|
+
if (args.includes("--json")) {
|
|
635
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
console.log(`Goal: ${summary.goal ?? "-"}`);
|
|
639
|
+
console.log(`Stage: ${summary.stage.label} | ${summary.stage.detail}`);
|
|
640
|
+
console.log(`Headline: ${result.headline}`);
|
|
641
|
+
console.log(`Tasks: pending=${summary.taskCounts.pending} running=${summary.taskCounts.running} blocked=${summary.taskCounts.blocked} completed=${summary.taskCounts.completed} failed=${summary.taskCounts.failed}`);
|
|
642
|
+
console.log(`Approvals: pending=${summary.approvalCounts.pending} | Reviews: open=${summary.reviewCounts.open} | Recommendations: active=${summary.recommendationCounts.active} dismissed=${summary.recommendationCounts.dismissed}`);
|
|
643
|
+
console.log(`Land readiness: ${summary.landReadiness.state}`);
|
|
644
|
+
if (summary.latestLandReport) {
|
|
645
|
+
console.log(`Latest landed result: ${summary.latestLandReport.createdAt} -> ${summary.latestLandReport.targetBranch}`);
|
|
646
|
+
}
|
|
647
|
+
if (summary.landReadiness.blockers.length > 0) {
|
|
648
|
+
console.log("Blockers:");
|
|
649
|
+
for (const blocker of summary.landReadiness.blockers){
|
|
650
|
+
console.log(`- ${blocker}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (summary.landReadiness.warnings.length > 0) {
|
|
654
|
+
console.log("Warnings:");
|
|
655
|
+
for (const warning of summary.landReadiness.warnings){
|
|
656
|
+
console.log(`- ${warning}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
console.log("Current changes:");
|
|
660
|
+
for (const changeSet of summary.changedByAgent){
|
|
661
|
+
console.log(`- ${changeSet.agent}: ${changeSet.paths.length} path(s)${changeSet.paths.length > 0 ? ` | ${changeSet.paths.join(", ")}` : ""}`);
|
|
662
|
+
}
|
|
663
|
+
if (summary.completedTasks.length > 0) {
|
|
664
|
+
console.log("Completed results:");
|
|
665
|
+
for (const task of summary.completedTasks.slice(0, 8)){
|
|
666
|
+
console.log(`- ${task.taskId} | ${task.owner} | ${task.title} | ${task.summary}${task.claimedPaths.length > 0 ? ` | paths=${task.claimedPaths.join(", ")}` : ""}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (summary.recentActivity.length > 0) {
|
|
670
|
+
console.log("Recent activity:");
|
|
671
|
+
for (const entry of summary.recentActivity.slice(0, 8)){
|
|
672
|
+
console.log(`- ${entry.timestamp} | ${entry.title} | ${entry.detail}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (summary.landReadiness.nextActions.length > 0) {
|
|
676
|
+
console.log("Next actions:");
|
|
677
|
+
for (const action of summary.landReadiness.nextActions){
|
|
678
|
+
console.log(`- ${action}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function commandResult(cwd, args) {
|
|
683
|
+
const { paths } = await requireSession(cwd);
|
|
684
|
+
const snapshot = await loadSnapshot(paths, 120);
|
|
685
|
+
const artifacts = await listTaskArtifacts(paths);
|
|
686
|
+
const result = buildWorkflowResult(snapshot, artifacts);
|
|
687
|
+
if (args.includes("--json")) {
|
|
688
|
+
console.log(JSON.stringify(result, null, 2));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
console.log(`Goal: ${result.goal ?? "-"}`);
|
|
692
|
+
console.log(`Stage: ${result.stage.label} | ${result.stage.detail}`);
|
|
693
|
+
console.log(`Headline: ${result.headline}`);
|
|
694
|
+
if (result.latestLandReport) {
|
|
695
|
+
console.log(`Latest land: ${result.latestLandReport.createdAt} | ${result.latestLandReport.targetBranch}`);
|
|
696
|
+
console.log(`Validation: ${result.latestLandReport.validationCommand.trim() || "(none configured)"} | ${result.latestLandReport.validationStatus} | ${result.latestLandReport.validationDetail}`);
|
|
697
|
+
console.log(`Review threads landed: ${result.latestLandReport.reviewThreadsLanded}`);
|
|
698
|
+
} else {
|
|
699
|
+
console.log("Latest land: none yet");
|
|
700
|
+
}
|
|
701
|
+
console.log("Agent results:");
|
|
702
|
+
for (const agent of result.agentResults){
|
|
703
|
+
console.log(`- ${agent.agent}: completed=${agent.completedTaskCount} | latest=${agent.latestTaskTitle ?? "-"} | ${agent.latestSummary ?? "No completed result yet."}`);
|
|
704
|
+
if (agent.changedPaths.length > 0) {
|
|
705
|
+
console.log(` unlanded: ${agent.changedPaths.join(", ")}`);
|
|
706
|
+
} else if (agent.landedPaths.length > 0) {
|
|
707
|
+
console.log(` landed: ${agent.landedPaths.join(", ")}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (result.completedTasks.length > 0) {
|
|
711
|
+
console.log("Completed outputs:");
|
|
712
|
+
for (const task of result.completedTasks.slice(0, 8)){
|
|
713
|
+
console.log(`- ${task.owner} | ${task.title} | ${task.summary}${task.claimedPaths.length > 0 ? ` | paths=${task.claimedPaths.join(", ")}` : ""}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (result.latestLandReport?.summary.length) {
|
|
717
|
+
console.log("Merged result summary:");
|
|
718
|
+
for (const line of result.latestLandReport.summary){
|
|
719
|
+
console.log(`- ${line}`);
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
console.log("Result summary:");
|
|
723
|
+
for (const line of result.summaryLines.slice(0, 6)){
|
|
724
|
+
console.log(`- ${line}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (result.nextActions.length > 0) {
|
|
728
|
+
console.log("Next actions:");
|
|
729
|
+
for (const action of result.nextActions){
|
|
730
|
+
console.log(`- ${action}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function commandActivity(cwd, args) {
|
|
735
|
+
const { paths } = await requireSession(cwd);
|
|
736
|
+
const limitArg = getFlag(args, "--limit");
|
|
737
|
+
const limit = limitArg ? Number(limitArg) : 30;
|
|
738
|
+
const snapshot = await loadSnapshot(paths, Math.max(50, Number.isFinite(limit) ? limit : 30));
|
|
739
|
+
const artifacts = await listTaskArtifacts(paths);
|
|
740
|
+
const activity = buildWorkflowActivity(snapshot, artifacts, Math.max(1, Number.isFinite(limit) ? limit : 30));
|
|
741
|
+
if (args.includes("--json")) {
|
|
742
|
+
console.log(JSON.stringify(activity, null, 2));
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
for (const entry of activity){
|
|
746
|
+
console.log(`${entry.timestamp} ${entry.title}`);
|
|
747
|
+
console.log(` ${entry.detail}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
564
750
|
async function commandRoute(cwd, args) {
|
|
565
751
|
const prompt = getGoal(args);
|
|
566
752
|
if (!prompt) {
|
|
@@ -645,7 +831,18 @@ async function commandRecommend(cwd, args) {
|
|
|
645
831
|
const { paths } = await requireSession(cwd);
|
|
646
832
|
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
647
833
|
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
648
|
-
const
|
|
834
|
+
const kind = parseRecommendationKind(getOptionalFilter(args, "--kind"));
|
|
835
|
+
const status = parseRecommendationStatus(getOptionalFilter(args, "--status"));
|
|
836
|
+
const targetAgent = getOptionalFilter(args, "--agent");
|
|
837
|
+
if (targetAgent && targetAgent !== "codex" && targetAgent !== "claude" && targetAgent !== "operator" && targetAgent !== "all") {
|
|
838
|
+
throw new Error(`Unsupported recommendation agent "${targetAgent}".`);
|
|
839
|
+
}
|
|
840
|
+
const recommendations = buildOperatorRecommendations(session, {
|
|
841
|
+
includeDismissed: args.includes("--all") || status === "dismissed" || status === "all",
|
|
842
|
+
kind: kind ?? undefined,
|
|
843
|
+
status: status ?? undefined,
|
|
844
|
+
targetAgent: targetAgent ?? undefined
|
|
845
|
+
});
|
|
649
846
|
if (args.includes("--json")) {
|
|
650
847
|
console.log(JSON.stringify(recommendations, null, 2));
|
|
651
848
|
return;
|
|
@@ -655,9 +852,13 @@ async function commandRecommend(cwd, args) {
|
|
|
655
852
|
return;
|
|
656
853
|
}
|
|
657
854
|
for (const recommendation of recommendations){
|
|
658
|
-
console.log(`${recommendation.id} | ${recommendation.kind} | ${recommendation.targetAgent ?? "-"}`);
|
|
855
|
+
console.log(`${recommendation.id} | ${recommendation.status} | ${recommendation.kind} | ${recommendation.targetAgent ?? "-"}`);
|
|
659
856
|
console.log(` title: ${recommendation.title}`);
|
|
660
857
|
console.log(` detail: ${recommendation.detail}`);
|
|
858
|
+
console.log(` open follow-ups: ${recommendation.openFollowUpTaskIds.join(", ") || "-"}`);
|
|
859
|
+
if (recommendation.dismissedReason) {
|
|
860
|
+
console.log(` dismissed reason: ${recommendation.dismissedReason}`);
|
|
861
|
+
}
|
|
661
862
|
console.log(` command: ${recommendation.commandHint}`);
|
|
662
863
|
}
|
|
663
864
|
}
|
|
@@ -669,65 +870,82 @@ async function commandRecommendApply(cwd, args) {
|
|
|
669
870
|
if (!recommendationId) {
|
|
670
871
|
throw new Error("A recommendation id is required. Example: kavi recommend-apply integration:src/ui/App.tsx");
|
|
671
872
|
}
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
};
|
|
873
|
+
const plan = buildRecommendationActionPlan(session, recommendationId, {
|
|
874
|
+
force: args.includes("--force")
|
|
875
|
+
});
|
|
702
876
|
if (rpcSnapshot) {
|
|
703
877
|
await rpcEnqueueTask(paths, {
|
|
704
|
-
owner:
|
|
705
|
-
prompt,
|
|
706
|
-
routeReason:
|
|
707
|
-
routeMetadata:
|
|
708
|
-
claimedPaths:
|
|
709
|
-
routeStrategy:
|
|
710
|
-
routeConfidence:
|
|
878
|
+
owner: plan.owner,
|
|
879
|
+
prompt: plan.prompt,
|
|
880
|
+
routeReason: plan.routeReason,
|
|
881
|
+
routeMetadata: plan.routeMetadata,
|
|
882
|
+
claimedPaths: plan.claimedPaths,
|
|
883
|
+
routeStrategy: plan.routeStrategy,
|
|
884
|
+
routeConfidence: plan.routeConfidence,
|
|
885
|
+
recommendationId: plan.recommendation.id,
|
|
886
|
+
recommendationKind: plan.recommendation.kind
|
|
711
887
|
});
|
|
712
888
|
} else {
|
|
713
889
|
await appendCommand(paths, "enqueue", {
|
|
714
|
-
owner:
|
|
715
|
-
prompt,
|
|
716
|
-
routeReason:
|
|
717
|
-
routeMetadata:
|
|
718
|
-
claimedPaths:
|
|
719
|
-
routeStrategy:
|
|
720
|
-
routeConfidence:
|
|
890
|
+
owner: plan.owner,
|
|
891
|
+
prompt: plan.prompt,
|
|
892
|
+
routeReason: plan.routeReason,
|
|
893
|
+
routeMetadata: plan.routeMetadata,
|
|
894
|
+
claimedPaths: plan.claimedPaths,
|
|
895
|
+
routeStrategy: plan.routeStrategy,
|
|
896
|
+
routeConfidence: plan.routeConfidence,
|
|
897
|
+
recommendationId: plan.recommendation.id,
|
|
898
|
+
recommendationKind: plan.recommendation.kind
|
|
721
899
|
});
|
|
722
900
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
901
|
+
console.log(`Queued ${plan.owner} task from recommendation ${plan.recommendation.id}.`);
|
|
902
|
+
}
|
|
903
|
+
async function commandRecommendDismiss(cwd, args) {
|
|
904
|
+
const { paths } = await requireSession(cwd);
|
|
905
|
+
const recommendationId = args.find((arg)=>!arg.startsWith("--"));
|
|
906
|
+
if (!recommendationId) {
|
|
907
|
+
throw new Error("A recommendation id is required. Example: kavi recommend-dismiss integration:src/ui");
|
|
908
|
+
}
|
|
909
|
+
const reason = getOptionalFilter(args, "--reason");
|
|
910
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
911
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
912
|
+
const recommendation = dismissOperatorRecommendation(session, recommendationId, reason);
|
|
913
|
+
if (rpcSnapshot) {
|
|
914
|
+
await rpcDismissRecommendation(paths, {
|
|
915
|
+
recommendationId,
|
|
916
|
+
reason
|
|
917
|
+
});
|
|
918
|
+
} else {
|
|
919
|
+
await saveSessionRecord(paths, session);
|
|
920
|
+
await recordEvent(paths, session.id, "recommendation.dismissed", {
|
|
921
|
+
recommendationId,
|
|
922
|
+
reason
|
|
923
|
+
});
|
|
924
|
+
await notifyOperatorSurface(paths, "recommendation.dismissed");
|
|
925
|
+
}
|
|
926
|
+
console.log(`Dismissed recommendation ${recommendation.id}.`);
|
|
927
|
+
}
|
|
928
|
+
async function commandRecommendRestore(cwd, args) {
|
|
929
|
+
const { paths } = await requireSession(cwd);
|
|
930
|
+
const recommendationId = args.find((arg)=>!arg.startsWith("--"));
|
|
931
|
+
if (!recommendationId) {
|
|
932
|
+
throw new Error("A recommendation id is required. Example: kavi recommend-restore integration:src/ui");
|
|
933
|
+
}
|
|
934
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
935
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
936
|
+
const recommendation = restoreOperatorRecommendation(session, recommendationId);
|
|
937
|
+
if (rpcSnapshot) {
|
|
938
|
+
await rpcRestoreRecommendation(paths, {
|
|
939
|
+
recommendationId
|
|
940
|
+
});
|
|
941
|
+
} else {
|
|
942
|
+
await saveSessionRecord(paths, session);
|
|
943
|
+
await recordEvent(paths, session.id, "recommendation.restored", {
|
|
944
|
+
recommendationId
|
|
945
|
+
});
|
|
946
|
+
await notifyOperatorSurface(paths, "recommendation.restored");
|
|
947
|
+
}
|
|
948
|
+
console.log(`Restored recommendation ${recommendation.id}.`);
|
|
731
949
|
}
|
|
732
950
|
async function commandPaths(cwd, args) {
|
|
733
951
|
const repoRoot = await findRepoRoot(cwd) ?? cwd;
|
|
@@ -743,6 +961,7 @@ async function commandPaths(cwd, args) {
|
|
|
743
961
|
integrationRoot: paths.integrationRoot,
|
|
744
962
|
stateFile: paths.stateFile,
|
|
745
963
|
eventsFile: paths.eventsFile,
|
|
964
|
+
reportsDir: paths.reportsDir,
|
|
746
965
|
approvalsFile: paths.approvalsFile,
|
|
747
966
|
commandsFile: paths.commandsFile,
|
|
748
967
|
socketPath: paths.socketPath,
|
|
@@ -764,6 +983,7 @@ async function commandPaths(cwd, args) {
|
|
|
764
983
|
console.log(`Integration: ${payload.integrationRoot}`);
|
|
765
984
|
console.log(`State file: ${payload.stateFile}`);
|
|
766
985
|
console.log(`Events file: ${payload.eventsFile}`);
|
|
986
|
+
console.log(`Reports: ${payload.reportsDir}`);
|
|
767
987
|
console.log(`Approvals file: ${payload.approvalsFile}`);
|
|
768
988
|
console.log(`Command queue: ${payload.commandsFile}`);
|
|
769
989
|
console.log(`Control socket: ${payload.socketPath}`);
|
|
@@ -1113,6 +1333,10 @@ async function commandLand(cwd) {
|
|
|
1113
1333
|
const paths = resolveAppPaths(repoRoot);
|
|
1114
1334
|
const session = await loadSessionRecord(paths);
|
|
1115
1335
|
const targetBranch = await resolveTargetBranch(repoRoot, session.config.baseBranch);
|
|
1336
|
+
const preLandChanges = await Promise.all(session.worktrees.map(async (worktree)=>({
|
|
1337
|
+
agent: worktree.agent,
|
|
1338
|
+
paths: await listWorktreeChangedPaths(worktree.path, session.baseCommit)
|
|
1339
|
+
})));
|
|
1116
1340
|
const overlappingPaths = await findOverlappingWorktreePaths(session.worktrees, session.baseCommit);
|
|
1117
1341
|
if (overlappingPaths.length > 0) {
|
|
1118
1342
|
const existing = session.tasks.find((task)=>task.status === "pending" && task.title === "Resolve integration overlap");
|
|
@@ -1158,6 +1382,10 @@ async function commandLand(cwd) {
|
|
|
1158
1382
|
overlappingPaths
|
|
1159
1383
|
});
|
|
1160
1384
|
console.log("Landing blocked because both agent worktrees changed overlapping paths.");
|
|
1385
|
+
console.log("Current change surface:");
|
|
1386
|
+
for (const changeSet of preLandChanges){
|
|
1387
|
+
console.log(`- ${changeSet.agent}: ${changeSet.paths.length} path(s)${changeSet.paths.length > 0 ? ` | ${changeSet.paths.join(", ")}` : ""}`);
|
|
1388
|
+
}
|
|
1161
1389
|
console.log("Queued integration task for codex:");
|
|
1162
1390
|
for (const filePath of overlappingPaths){
|
|
1163
1391
|
console.log(`- ${filePath}`);
|
|
@@ -1175,6 +1403,7 @@ async function commandLand(cwd) {
|
|
|
1175
1403
|
const releasedClaims = releasePathClaims(session, {
|
|
1176
1404
|
note: `Released after landing into ${targetBranch}.`
|
|
1177
1405
|
});
|
|
1406
|
+
session.baseCommit = await getBranchCommit(repoRoot, targetBranch);
|
|
1178
1407
|
for (const claim of releasedClaims){
|
|
1179
1408
|
addDecisionRecord(session, {
|
|
1180
1409
|
kind: "integration",
|
|
@@ -1236,10 +1465,40 @@ async function commandLand(cwd) {
|
|
|
1236
1465
|
artifact.reviewNotes = session.reviewNotes.filter((note)=>note.taskId === taskId);
|
|
1237
1466
|
await saveTaskArtifact(paths, artifact);
|
|
1238
1467
|
}
|
|
1468
|
+
const artifacts = await listTaskArtifacts(paths);
|
|
1469
|
+
const postLandSnapshot = await loadSnapshot(paths, 60);
|
|
1470
|
+
const postLandSummary = buildWorkflowSummary(postLandSnapshot, artifacts);
|
|
1471
|
+
const landReport = buildLandReport({
|
|
1472
|
+
id: buildSessionId(),
|
|
1473
|
+
sessionId: session.id,
|
|
1474
|
+
goal: session.goal,
|
|
1475
|
+
createdAt: new Date().toISOString(),
|
|
1476
|
+
targetBranch,
|
|
1477
|
+
integrationBranch: result.integrationBranch,
|
|
1478
|
+
integrationPath: result.integrationPath,
|
|
1479
|
+
validationCommand: session.config.validationCommand,
|
|
1480
|
+
validationStatus: result.validation.status,
|
|
1481
|
+
validationDetail: result.validation.detail,
|
|
1482
|
+
changedByAgent: preLandChanges,
|
|
1483
|
+
completedTasks: postLandSummary.completedTasks,
|
|
1484
|
+
snapshotCommits: result.snapshotCommits,
|
|
1485
|
+
commandsRun: result.commandsRun,
|
|
1486
|
+
reviewThreadsLanded: landedReviewNotes.length,
|
|
1487
|
+
openReviewThreadsRemaining: session.reviewNotes.filter((note)=>note.status === "open").length
|
|
1488
|
+
});
|
|
1489
|
+
await saveLandReport(paths, landReport);
|
|
1239
1490
|
await notifyOperatorSurface(paths, "land.completed");
|
|
1240
1491
|
console.log(`Landed branches into ${targetBranch}`);
|
|
1241
1492
|
console.log(`Integration branch: ${result.integrationBranch}`);
|
|
1242
1493
|
console.log(`Integration worktree: ${result.integrationPath}`);
|
|
1494
|
+
console.log("Merged change surface:");
|
|
1495
|
+
for (const changeSet of preLandChanges){
|
|
1496
|
+
console.log(`- ${changeSet.agent}: ${changeSet.paths.length} path(s)${changeSet.paths.length > 0 ? ` | ${changeSet.paths.join(", ")}` : ""}`);
|
|
1497
|
+
}
|
|
1498
|
+
console.log(`Validation: ${result.validation.command || "(none configured)"} | ${result.validation.status} | ${result.validation.detail}`);
|
|
1499
|
+
console.log(`Review threads landed: ${landedReviewNotes.length}`);
|
|
1500
|
+
console.log(`Result report: ${landReport.id}`);
|
|
1501
|
+
console.log("Inspect result: kavi result");
|
|
1243
1502
|
for (const snapshot of result.snapshotCommits){
|
|
1244
1503
|
console.log(`Snapshot ${snapshot.agent}: ${snapshot.commit}${snapshot.createdCommit ? " (created)" : " (unchanged)"}`);
|
|
1245
1504
|
}
|
|
@@ -1386,9 +1645,18 @@ async function main() {
|
|
|
1386
1645
|
case "resume":
|
|
1387
1646
|
await commandResume(cwd);
|
|
1388
1647
|
break;
|
|
1648
|
+
case "summary":
|
|
1649
|
+
await commandSummary(cwd, args);
|
|
1650
|
+
break;
|
|
1651
|
+
case "result":
|
|
1652
|
+
await commandResult(cwd, args);
|
|
1653
|
+
break;
|
|
1389
1654
|
case "status":
|
|
1390
1655
|
await commandStatus(cwd, args);
|
|
1391
1656
|
break;
|
|
1657
|
+
case "activity":
|
|
1658
|
+
await commandActivity(cwd, args);
|
|
1659
|
+
break;
|
|
1392
1660
|
case "route":
|
|
1393
1661
|
await commandRoute(cwd, args);
|
|
1394
1662
|
break;
|
|
@@ -1407,6 +1675,12 @@ async function main() {
|
|
|
1407
1675
|
case "recommend-apply":
|
|
1408
1676
|
await commandRecommendApply(cwd, args);
|
|
1409
1677
|
break;
|
|
1678
|
+
case "recommend-dismiss":
|
|
1679
|
+
await commandRecommendDismiss(cwd, args);
|
|
1680
|
+
break;
|
|
1681
|
+
case "recommend-restore":
|
|
1682
|
+
await commandRecommendRestore(cwd, args);
|
|
1683
|
+
break;
|
|
1410
1684
|
case "tasks":
|
|
1411
1685
|
await commandTasks(cwd, args);
|
|
1412
1686
|
break;
|
package/dist/paths.js
CHANGED
|
@@ -9,6 +9,7 @@ export function resolveAppPaths(repoRoot) {
|
|
|
9
9
|
const homeStateDir = process.env.KAVI_HOME_STATE_DIR ?? path.join(home, ".local", "state", "kavi");
|
|
10
10
|
const runtimeDir = path.join(kaviDir, "runtime");
|
|
11
11
|
const stateDir = path.join(kaviDir, "state");
|
|
12
|
+
const reportsDir = path.join(stateDir, "reports");
|
|
12
13
|
const runsDir = path.join(runtimeDir, "runs");
|
|
13
14
|
return {
|
|
14
15
|
repoRoot,
|
|
@@ -16,6 +17,7 @@ export function resolveAppPaths(repoRoot) {
|
|
|
16
17
|
configFile: path.join(kaviDir, "config.toml"),
|
|
17
18
|
promptsDir: path.join(kaviDir, "prompts"),
|
|
18
19
|
stateDir,
|
|
20
|
+
reportsDir,
|
|
19
21
|
runtimeDir,
|
|
20
22
|
runsDir,
|
|
21
23
|
stateFile: path.join(stateDir, "session.json"),
|