@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/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 { pingRpc, readSnapshot, rpcEnqueueTask, rpcNotifyExternalUpdate, rpcKickoff, rpcRecentEvents, rpcResolveApproval, rpcShutdown, rpcTaskArtifact } from "./rpc.js";
18
- import { buildOperatorRecommendations } from "./recommendations.js";
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 rpcSnapshot = await tryRpcSnapshot(paths);
484
- const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
485
- const pendingApprovals = rpcSnapshot?.approvals.filter((item)=>item.status === "pending") ?? await listApprovalRequests(paths);
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: rpcSnapshot !== null,
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 recommendations = buildOperatorRecommendations(session);
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 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
- };
873
+ const plan = buildRecommendationActionPlan(session, recommendationId, {
874
+ force: args.includes("--force")
875
+ });
702
876
  if (rpcSnapshot) {
703
877
  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
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: routeDecision.owner,
715
- prompt,
716
- routeReason: routeDecision.reason,
717
- routeMetadata: routeDecision.metadata,
718
- claimedPaths: routeDecision.claimedPaths,
719
- routeStrategy: routeDecision.strategy,
720
- routeConfidence: routeDecision.confidence
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
- 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}.`);
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"),