@mandipadk7/kavi 0.1.7 → 1.0.1

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, rpcSetFullAccessMode, 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",
@@ -31,6 +33,9 @@ const CLAUDE_AUTO_ALLOW_TOOLS = new Set([
31
33
  "Grep",
32
34
  "LS"
33
35
  ]);
36
+ function hasFlag(args, name) {
37
+ return args.includes(name);
38
+ }
34
39
  function getFlag(args, name) {
35
40
  const index = args.indexOf(name);
36
41
  if (index === -1) {
@@ -58,6 +63,24 @@ function getOptionalFilter(args, name) {
58
63
  }
59
64
  return value;
60
65
  }
66
+ function parseRecommendationKind(value) {
67
+ if (!value) {
68
+ return null;
69
+ }
70
+ if (value === "all" || value === "handoff" || value === "integration" || value === "ownership-config") {
71
+ return value;
72
+ }
73
+ throw new Error(`Unsupported recommendation kind "${value}".`);
74
+ }
75
+ function parseRecommendationStatus(value) {
76
+ if (!value) {
77
+ return null;
78
+ }
79
+ if (value === "all" || value === "active" || value === "dismissed") {
80
+ return value;
81
+ }
82
+ throw new Error(`Unsupported recommendation status "${value}".`);
83
+ }
61
84
  async function readStdinText() {
62
85
  if (process.stdin.isTTY) {
63
86
  return "";
@@ -92,16 +115,21 @@ function renderUsage() {
92
115
  " kavi init [--home] [--no-commit]",
93
116
  " kavi doctor [--json]",
94
117
  " kavi update [--check] [--dry-run] [--yes] [--tag latest|beta] [--version X.Y.Z]",
95
- " kavi start [--goal \"...\"]",
96
- " kavi open [--goal \"...\"]",
118
+ " kavi start [--goal \"...\"] [--approve-all]",
119
+ " kavi open [--goal \"...\"] [--approve-all]",
97
120
  " kavi resume",
121
+ " kavi summary [--json]",
122
+ " kavi result [--json]",
98
123
  " kavi status [--json]",
124
+ " kavi activity [--json] [--limit N]",
99
125
  " kavi route [--json] [--no-ai] <prompt>",
100
126
  " kavi routes [--json] [--limit N]",
101
127
  " kavi paths [--json]",
102
128
  " kavi task [--agent codex|claude|auto] <prompt>",
103
- " kavi recommend [--json]",
104
- " kavi recommend-apply <recommendation-id>",
129
+ " kavi recommend [--json] [--all] [--kind handoff|integration|ownership-config] [--status active|dismissed] [--agent codex|claude|operator]",
130
+ " kavi recommend-apply <recommendation-id> [--force]",
131
+ " kavi recommend-dismiss <recommendation-id> [--reason \"...\"]",
132
+ " kavi recommend-restore <recommendation-id>",
105
133
  " kavi tasks [--json]",
106
134
  " kavi task-output <task-id|latest> [--json]",
107
135
  " kavi decisions [--json] [--limit N]",
@@ -335,7 +363,10 @@ async function commandUpdate(cwd, args) {
335
363
  }
336
364
  console.log(`Updated ${packageInfo.name} from ${packageInfo.version} to ${targetVersion}.`);
337
365
  }
338
- async function startOrAttachSession(cwd, goal) {
366
+ function renderFullAccessWarning() {
367
+ return "WARNING: approve-all is enabled. Claude and Codex will run with full access, and Kavi approval prompts will be bypassed for future turns.";
368
+ }
369
+ async function startOrAttachSession(cwd, goal, enableFullAccessMode) {
339
370
  const prepared = await prepareProjectContext(cwd, {
340
371
  createRepository: true,
341
372
  ensureHeadCommit: false,
@@ -346,6 +377,11 @@ async function startOrAttachSession(cwd, goal) {
346
377
  try {
347
378
  const session = await loadSessionRecord(paths);
348
379
  if (isSessionLive(session) && await pingRpc(paths)) {
380
+ if (enableFullAccessMode && !session.fullAccessMode) {
381
+ await rpcSetFullAccessMode(paths, {
382
+ enabled: true
383
+ });
384
+ }
349
385
  if (goal) {
350
386
  await rpcKickoff(paths, goal);
351
387
  }
@@ -366,7 +402,7 @@ async function startOrAttachSession(cwd, goal) {
366
402
  const rpcEndpoint = paths.socketPath;
367
403
  await fs.writeFile(paths.commandsFile, "", "utf8");
368
404
  const worktrees = await ensureWorktrees(repoRoot, paths, sessionId, config, baseCommit);
369
- await createSessionRecord(paths, config, runtime, sessionId, baseCommit, worktrees, goal, rpcEndpoint);
405
+ await createSessionRecord(paths, config, runtime, sessionId, baseCommit, worktrees, goal, rpcEndpoint, enableFullAccessMode);
370
406
  if (prepared.createdRepository) {
371
407
  await recordEvent(paths, sessionId, "repo.initialized", {
372
408
  repoRoot
@@ -425,6 +461,29 @@ async function tryRpcSnapshot(paths) {
425
461
  }
426
462
  return await readSnapshot(paths);
427
463
  }
464
+ async function loadSnapshot(paths, eventLimit = 80) {
465
+ const rpcSnapshot = await tryRpcSnapshot(paths);
466
+ if (rpcSnapshot) {
467
+ return rpcSnapshot;
468
+ }
469
+ const session = await loadSessionRecord(paths);
470
+ const approvals = await listApprovalRequests(paths, {
471
+ includeResolved: true
472
+ });
473
+ const events = await readRecentEvents(paths, eventLimit);
474
+ const worktreeDiffs = await Promise.all(session.worktrees.map(async (worktree)=>({
475
+ agent: worktree.agent,
476
+ paths: await listWorktreeChangedPaths(worktree.path, session.baseCommit)
477
+ })));
478
+ const latestLandReport = await loadLatestLandReport(paths);
479
+ return {
480
+ session,
481
+ approvals,
482
+ events,
483
+ worktreeDiffs,
484
+ latestLandReport
485
+ };
486
+ }
428
487
  async function notifyOperatorSurface(paths, reason) {
429
488
  if (!await pingRpc(paths)) {
430
489
  return;
@@ -454,9 +513,13 @@ function buildRouteAnalytics(tasks) {
454
513
  }
455
514
  async function commandOpen(cwd, args) {
456
515
  const goal = getGoal(args);
457
- await startOrAttachSession(cwd, goal);
516
+ await startOrAttachSession(cwd, goal, hasFlag(args, "--approve-all"));
458
517
  const repoRoot = await detectRepoRoot(cwd);
459
518
  const paths = resolveAppPaths(repoRoot);
519
+ const session = await loadSessionRecord(paths);
520
+ if (session.fullAccessMode) {
521
+ console.log(renderFullAccessWarning());
522
+ }
460
523
  await attachTui(paths);
461
524
  }
462
525
  async function commandResume(cwd) {
@@ -466,28 +529,35 @@ async function commandResume(cwd) {
466
529
  }
467
530
  async function commandStart(cwd, args) {
468
531
  const goal = getGoal(args);
469
- const socketPath = await startOrAttachSession(cwd, goal);
532
+ const socketPath = await startOrAttachSession(cwd, goal, hasFlag(args, "--approve-all"));
470
533
  const repoRoot = await detectRepoRoot(cwd);
471
534
  const paths = resolveAppPaths(repoRoot);
472
535
  const session = await loadSessionRecord(paths);
473
536
  console.log(`Started Kavi session ${session.id}`);
474
537
  console.log(`Repo: ${repoRoot}`);
475
538
  console.log(`Control: ${socketPath}`);
539
+ console.log(`Access: ${session.fullAccessMode ? "approve-all" : "standard"}`);
476
540
  console.log(`Runtime: node=${session.runtime.nodeExecutable} codex=${session.runtime.codexExecutable} claude=${session.runtime.claudeExecutable}`);
541
+ if (session.fullAccessMode) {
542
+ console.log(renderFullAccessWarning());
543
+ }
477
544
  for (const worktree of session.worktrees){
478
545
  console.log(`- ${worktree.agent}: ${worktree.path}`);
479
546
  }
480
547
  }
481
548
  async function commandStatus(cwd, args) {
482
549
  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);
550
+ const snapshot = await loadSnapshot(paths, 60);
551
+ const session = snapshot.session;
552
+ const pendingApprovals = snapshot.approvals.filter((item)=>item.status === "pending");
486
553
  const heartbeatAgeMs = sessionHeartbeatAgeMs(session);
487
554
  const routeAnalytics = buildRouteAnalytics(session.tasks);
488
555
  const ownershipConflicts = findOwnershipRuleConflicts(session.config);
489
556
  const claimHotspots = buildClaimHotspots(session);
490
- const recommendations = buildOperatorRecommendations(session);
557
+ const recommendations = buildOperatorRecommendations(session, {
558
+ includeDismissed: true
559
+ });
560
+ const workflowSummary = buildWorkflowSummary(snapshot);
491
561
  const payload = {
492
562
  id: session.id,
493
563
  status: session.status,
@@ -496,9 +566,16 @@ async function commandStatus(cwd, args) {
496
566
  goal: session.goal,
497
567
  daemonPid: session.daemonPid,
498
568
  daemonHeartbeatAt: session.daemonHeartbeatAt,
569
+ fullAccessMode: session.fullAccessMode,
499
570
  daemonHealthy: isSessionLive(session),
500
- rpcConnected: rpcSnapshot !== null,
571
+ rpcConnected: await pingRpc(paths),
501
572
  heartbeatAgeMs,
573
+ workflowStage: workflowSummary.stage,
574
+ latestLandReport: workflowSummary.latestLandReport ? {
575
+ id: workflowSummary.latestLandReport.id,
576
+ createdAt: workflowSummary.latestLandReport.createdAt,
577
+ targetBranch: workflowSummary.latestLandReport.targetBranch
578
+ } : null,
502
579
  runtime: session.runtime,
503
580
  taskCounts: {
504
581
  total: session.tasks.length,
@@ -523,6 +600,9 @@ async function commandStatus(cwd, args) {
523
600
  },
524
601
  recommendationCounts: {
525
602
  total: recommendations.length,
603
+ active: recommendations.filter((item)=>item.status === "active").length,
604
+ dismissed: recommendations.filter((item)=>item.status === "dismissed").length,
605
+ withOpenFollowUps: recommendations.filter((item)=>item.openFollowUpTaskIds.length > 0).length,
526
606
  integration: recommendations.filter((item)=>item.kind === "integration").length,
527
607
  handoff: recommendations.filter((item)=>item.kind === "handoff").length,
528
608
  ownershipConfig: recommendations.filter((item)=>item.kind === "ownership-config").length
@@ -544,7 +624,12 @@ async function commandStatus(cwd, args) {
544
624
  console.log(`Status: ${payload.status}${payload.daemonHealthy ? " (healthy)" : " (stale or stopped)"}`);
545
625
  console.log(`Repo: ${payload.repoRoot}`);
546
626
  console.log(`Control: ${payload.socketPath}${payload.rpcConnected ? " (connected)" : " (disconnected)"}`);
627
+ console.log(`Access: ${payload.fullAccessMode ? "approve-all" : "standard"}`);
547
628
  console.log(`Goal: ${payload.goal ?? "-"}`);
629
+ console.log(`Workflow stage: ${payload.workflowStage.label} | ${payload.workflowStage.detail}`);
630
+ if (payload.latestLandReport) {
631
+ console.log(`Latest land: ${payload.latestLandReport.createdAt} -> ${payload.latestLandReport.targetBranch}`);
632
+ }
548
633
  console.log(`Daemon PID: ${payload.daemonPid ?? "-"}`);
549
634
  console.log(`Heartbeat: ${payload.daemonHeartbeatAt ?? "-"}${heartbeatAgeMs === null ? "" : ` (${heartbeatAgeMs} ms ago)`}`);
550
635
  console.log(`Runtime: node=${payload.runtime.nodeExecutable} codex=${payload.runtime.codexExecutable} claude=${payload.runtime.claudeExecutable}`);
@@ -553,7 +638,7 @@ async function commandStatus(cwd, args) {
553
638
  console.log(`Reviews: open=${payload.reviewCounts.open} total=${payload.reviewCounts.total}`);
554
639
  console.log(`Decisions: total=${payload.decisionCounts.total}`);
555
640
  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}`);
641
+ 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
642
  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
643
  console.log(`Ownership conflicts: ${payload.ownershipConflicts} | Claim hotspots: ${payload.claimHotspots}`);
559
644
  console.log(`Routing ownership: codex=${payload.routingOwnership.codexPaths.join(", ") || "-"} | claude=${payload.routingOwnership.claudePaths.join(", ") || "-"}`);
@@ -561,6 +646,128 @@ async function commandStatus(cwd, args) {
561
646
  console.log(`- ${worktree.agent}: ${worktree.path}`);
562
647
  }
563
648
  }
649
+ async function commandSummary(cwd, args) {
650
+ const { paths } = await requireSession(cwd);
651
+ const snapshot = await loadSnapshot(paths, 120);
652
+ const artifacts = await listTaskArtifacts(paths);
653
+ const summary = buildWorkflowSummary(snapshot, artifacts);
654
+ const result = buildWorkflowResult(snapshot, artifacts);
655
+ if (args.includes("--json")) {
656
+ console.log(JSON.stringify(summary, null, 2));
657
+ return;
658
+ }
659
+ console.log(`Goal: ${summary.goal ?? "-"}`);
660
+ console.log(`Stage: ${summary.stage.label} | ${summary.stage.detail}`);
661
+ console.log(`Headline: ${result.headline}`);
662
+ console.log(`Tasks: pending=${summary.taskCounts.pending} running=${summary.taskCounts.running} blocked=${summary.taskCounts.blocked} completed=${summary.taskCounts.completed} failed=${summary.taskCounts.failed}`);
663
+ console.log(`Approvals: pending=${summary.approvalCounts.pending} | Reviews: open=${summary.reviewCounts.open} | Recommendations: active=${summary.recommendationCounts.active} dismissed=${summary.recommendationCounts.dismissed}`);
664
+ console.log(`Land readiness: ${summary.landReadiness.state}`);
665
+ if (summary.latestLandReport) {
666
+ console.log(`Latest landed result: ${summary.latestLandReport.createdAt} -> ${summary.latestLandReport.targetBranch}`);
667
+ }
668
+ if (summary.landReadiness.blockers.length > 0) {
669
+ console.log("Blockers:");
670
+ for (const blocker of summary.landReadiness.blockers){
671
+ console.log(`- ${blocker}`);
672
+ }
673
+ }
674
+ if (summary.landReadiness.warnings.length > 0) {
675
+ console.log("Warnings:");
676
+ for (const warning of summary.landReadiness.warnings){
677
+ console.log(`- ${warning}`);
678
+ }
679
+ }
680
+ console.log("Current changes:");
681
+ for (const changeSet of summary.changedByAgent){
682
+ console.log(`- ${changeSet.agent}: ${changeSet.paths.length} path(s)${changeSet.paths.length > 0 ? ` | ${changeSet.paths.join(", ")}` : ""}`);
683
+ }
684
+ if (summary.completedTasks.length > 0) {
685
+ console.log("Completed results:");
686
+ for (const task of summary.completedTasks.slice(0, 8)){
687
+ console.log(`- ${task.taskId} | ${task.owner} | ${task.title} | ${task.summary}${task.claimedPaths.length > 0 ? ` | paths=${task.claimedPaths.join(", ")}` : ""}`);
688
+ }
689
+ }
690
+ if (summary.recentActivity.length > 0) {
691
+ console.log("Recent activity:");
692
+ for (const entry of summary.recentActivity.slice(0, 8)){
693
+ console.log(`- ${entry.timestamp} | ${entry.title} | ${entry.detail}`);
694
+ }
695
+ }
696
+ if (summary.landReadiness.nextActions.length > 0) {
697
+ console.log("Next actions:");
698
+ for (const action of summary.landReadiness.nextActions){
699
+ console.log(`- ${action}`);
700
+ }
701
+ }
702
+ }
703
+ async function commandResult(cwd, args) {
704
+ const { paths } = await requireSession(cwd);
705
+ const snapshot = await loadSnapshot(paths, 120);
706
+ const artifacts = await listTaskArtifacts(paths);
707
+ const result = buildWorkflowResult(snapshot, artifacts);
708
+ if (args.includes("--json")) {
709
+ console.log(JSON.stringify(result, null, 2));
710
+ return;
711
+ }
712
+ console.log(`Goal: ${result.goal ?? "-"}`);
713
+ console.log(`Stage: ${result.stage.label} | ${result.stage.detail}`);
714
+ console.log(`Headline: ${result.headline}`);
715
+ if (result.latestLandReport) {
716
+ console.log(`Latest land: ${result.latestLandReport.createdAt} | ${result.latestLandReport.targetBranch}`);
717
+ console.log(`Validation: ${result.latestLandReport.validationCommand.trim() || "(none configured)"} | ${result.latestLandReport.validationStatus} | ${result.latestLandReport.validationDetail}`);
718
+ console.log(`Review threads landed: ${result.latestLandReport.reviewThreadsLanded}`);
719
+ } else {
720
+ console.log("Latest land: none yet");
721
+ }
722
+ console.log("Agent results:");
723
+ for (const agent of result.agentResults){
724
+ console.log(`- ${agent.agent}: completed=${agent.completedTaskCount} | latest=${agent.latestTaskTitle ?? "-"} | ${agent.latestSummary ?? "No completed result yet."}`);
725
+ if (agent.changedPaths.length > 0) {
726
+ console.log(` unlanded: ${agent.changedPaths.join(", ")}`);
727
+ } else if (agent.landedPaths.length > 0) {
728
+ console.log(` landed: ${agent.landedPaths.join(", ")}`);
729
+ }
730
+ }
731
+ if (result.completedTasks.length > 0) {
732
+ console.log("Completed outputs:");
733
+ for (const task of result.completedTasks.slice(0, 8)){
734
+ console.log(`- ${task.owner} | ${task.title} | ${task.summary}${task.claimedPaths.length > 0 ? ` | paths=${task.claimedPaths.join(", ")}` : ""}`);
735
+ }
736
+ }
737
+ if (result.latestLandReport?.summary.length) {
738
+ console.log("Merged result summary:");
739
+ for (const line of result.latestLandReport.summary){
740
+ console.log(`- ${line}`);
741
+ }
742
+ } else {
743
+ console.log("Result summary:");
744
+ for (const line of result.summaryLines.slice(0, 6)){
745
+ console.log(`- ${line}`);
746
+ }
747
+ }
748
+ if (result.nextActions.length > 0) {
749
+ console.log("Next actions:");
750
+ for (const action of result.nextActions){
751
+ console.log(`- ${action}`);
752
+ }
753
+ }
754
+ }
755
+ async function commandActivity(cwd, args) {
756
+ const { paths } = await requireSession(cwd);
757
+ const limitArg = getFlag(args, "--limit");
758
+ const limit = limitArg ? Number(limitArg) : 30;
759
+ const snapshot = await loadSnapshot(paths, Math.max(50, Number.isFinite(limit) ? limit : 30));
760
+ const artifacts = await listTaskArtifacts(paths);
761
+ const activity = buildWorkflowActivity(snapshot, artifacts, Math.max(1, Number.isFinite(limit) ? limit : 30));
762
+ if (args.includes("--json")) {
763
+ console.log(JSON.stringify(activity, null, 2));
764
+ return;
765
+ }
766
+ for (const entry of activity){
767
+ console.log(`${entry.timestamp} ${entry.title}`);
768
+ console.log(` ${entry.detail}`);
769
+ }
770
+ }
564
771
  async function commandRoute(cwd, args) {
565
772
  const prompt = getGoal(args);
566
773
  if (!prompt) {
@@ -645,7 +852,18 @@ async function commandRecommend(cwd, args) {
645
852
  const { paths } = await requireSession(cwd);
646
853
  const rpcSnapshot = await tryRpcSnapshot(paths);
647
854
  const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
648
- const recommendations = buildOperatorRecommendations(session);
855
+ const kind = parseRecommendationKind(getOptionalFilter(args, "--kind"));
856
+ const status = parseRecommendationStatus(getOptionalFilter(args, "--status"));
857
+ const targetAgent = getOptionalFilter(args, "--agent");
858
+ if (targetAgent && targetAgent !== "codex" && targetAgent !== "claude" && targetAgent !== "operator" && targetAgent !== "all") {
859
+ throw new Error(`Unsupported recommendation agent "${targetAgent}".`);
860
+ }
861
+ const recommendations = buildOperatorRecommendations(session, {
862
+ includeDismissed: args.includes("--all") || status === "dismissed" || status === "all",
863
+ kind: kind ?? undefined,
864
+ status: status ?? undefined,
865
+ targetAgent: targetAgent ?? undefined
866
+ });
649
867
  if (args.includes("--json")) {
650
868
  console.log(JSON.stringify(recommendations, null, 2));
651
869
  return;
@@ -655,9 +873,13 @@ async function commandRecommend(cwd, args) {
655
873
  return;
656
874
  }
657
875
  for (const recommendation of recommendations){
658
- console.log(`${recommendation.id} | ${recommendation.kind} | ${recommendation.targetAgent ?? "-"}`);
876
+ console.log(`${recommendation.id} | ${recommendation.status} | ${recommendation.kind} | ${recommendation.targetAgent ?? "-"}`);
659
877
  console.log(` title: ${recommendation.title}`);
660
878
  console.log(` detail: ${recommendation.detail}`);
879
+ console.log(` open follow-ups: ${recommendation.openFollowUpTaskIds.join(", ") || "-"}`);
880
+ if (recommendation.dismissedReason) {
881
+ console.log(` dismissed reason: ${recommendation.dismissedReason}`);
882
+ }
661
883
  console.log(` command: ${recommendation.commandHint}`);
662
884
  }
663
885
  }
@@ -669,65 +891,82 @@ async function commandRecommendApply(cwd, args) {
669
891
  if (!recommendationId) {
670
892
  throw new Error("A recommendation id is required. Example: kavi recommend-apply integration:src/ui/App.tsx");
671
893
  }
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
- };
894
+ const plan = buildRecommendationActionPlan(session, recommendationId, {
895
+ force: args.includes("--force")
896
+ });
702
897
  if (rpcSnapshot) {
703
898
  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
899
+ owner: plan.owner,
900
+ prompt: plan.prompt,
901
+ routeReason: plan.routeReason,
902
+ routeMetadata: plan.routeMetadata,
903
+ claimedPaths: plan.claimedPaths,
904
+ routeStrategy: plan.routeStrategy,
905
+ routeConfidence: plan.routeConfidence,
906
+ recommendationId: plan.recommendation.id,
907
+ recommendationKind: plan.recommendation.kind
711
908
  });
712
909
  } else {
713
910
  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
911
+ owner: plan.owner,
912
+ prompt: plan.prompt,
913
+ routeReason: plan.routeReason,
914
+ routeMetadata: plan.routeMetadata,
915
+ claimedPaths: plan.claimedPaths,
916
+ routeStrategy: plan.routeStrategy,
917
+ routeConfidence: plan.routeConfidence,
918
+ recommendationId: plan.recommendation.id,
919
+ recommendationKind: plan.recommendation.kind
721
920
  });
722
921
  }
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}.`);
922
+ console.log(`Queued ${plan.owner} task from recommendation ${plan.recommendation.id}.`);
923
+ }
924
+ async function commandRecommendDismiss(cwd, args) {
925
+ const { paths } = await requireSession(cwd);
926
+ const recommendationId = args.find((arg)=>!arg.startsWith("--"));
927
+ if (!recommendationId) {
928
+ throw new Error("A recommendation id is required. Example: kavi recommend-dismiss integration:src/ui");
929
+ }
930
+ const reason = getOptionalFilter(args, "--reason");
931
+ const rpcSnapshot = await tryRpcSnapshot(paths);
932
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
933
+ const recommendation = dismissOperatorRecommendation(session, recommendationId, reason);
934
+ if (rpcSnapshot) {
935
+ await rpcDismissRecommendation(paths, {
936
+ recommendationId,
937
+ reason
938
+ });
939
+ } else {
940
+ await saveSessionRecord(paths, session);
941
+ await recordEvent(paths, session.id, "recommendation.dismissed", {
942
+ recommendationId,
943
+ reason
944
+ });
945
+ await notifyOperatorSurface(paths, "recommendation.dismissed");
946
+ }
947
+ console.log(`Dismissed recommendation ${recommendation.id}.`);
948
+ }
949
+ async function commandRecommendRestore(cwd, args) {
950
+ const { paths } = await requireSession(cwd);
951
+ const recommendationId = args.find((arg)=>!arg.startsWith("--"));
952
+ if (!recommendationId) {
953
+ throw new Error("A recommendation id is required. Example: kavi recommend-restore integration:src/ui");
954
+ }
955
+ const rpcSnapshot = await tryRpcSnapshot(paths);
956
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
957
+ const recommendation = restoreOperatorRecommendation(session, recommendationId);
958
+ if (rpcSnapshot) {
959
+ await rpcRestoreRecommendation(paths, {
960
+ recommendationId
961
+ });
962
+ } else {
963
+ await saveSessionRecord(paths, session);
964
+ await recordEvent(paths, session.id, "recommendation.restored", {
965
+ recommendationId
966
+ });
967
+ await notifyOperatorSurface(paths, "recommendation.restored");
968
+ }
969
+ console.log(`Restored recommendation ${recommendation.id}.`);
731
970
  }
732
971
  async function commandPaths(cwd, args) {
733
972
  const repoRoot = await findRepoRoot(cwd) ?? cwd;
@@ -743,6 +982,7 @@ async function commandPaths(cwd, args) {
743
982
  integrationRoot: paths.integrationRoot,
744
983
  stateFile: paths.stateFile,
745
984
  eventsFile: paths.eventsFile,
985
+ reportsDir: paths.reportsDir,
746
986
  approvalsFile: paths.approvalsFile,
747
987
  commandsFile: paths.commandsFile,
748
988
  socketPath: paths.socketPath,
@@ -764,6 +1004,7 @@ async function commandPaths(cwd, args) {
764
1004
  console.log(`Integration: ${payload.integrationRoot}`);
765
1005
  console.log(`State file: ${payload.stateFile}`);
766
1006
  console.log(`Events file: ${payload.eventsFile}`);
1007
+ console.log(`Reports: ${payload.reportsDir}`);
767
1008
  console.log(`Approvals file: ${payload.approvalsFile}`);
768
1009
  console.log(`Command queue: ${payload.commandsFile}`);
769
1010
  console.log(`Control socket: ${payload.socketPath}`);
@@ -1113,6 +1354,10 @@ async function commandLand(cwd) {
1113
1354
  const paths = resolveAppPaths(repoRoot);
1114
1355
  const session = await loadSessionRecord(paths);
1115
1356
  const targetBranch = await resolveTargetBranch(repoRoot, session.config.baseBranch);
1357
+ const preLandChanges = await Promise.all(session.worktrees.map(async (worktree)=>({
1358
+ agent: worktree.agent,
1359
+ paths: await listWorktreeChangedPaths(worktree.path, session.baseCommit)
1360
+ })));
1116
1361
  const overlappingPaths = await findOverlappingWorktreePaths(session.worktrees, session.baseCommit);
1117
1362
  if (overlappingPaths.length > 0) {
1118
1363
  const existing = session.tasks.find((task)=>task.status === "pending" && task.title === "Resolve integration overlap");
@@ -1158,6 +1403,10 @@ async function commandLand(cwd) {
1158
1403
  overlappingPaths
1159
1404
  });
1160
1405
  console.log("Landing blocked because both agent worktrees changed overlapping paths.");
1406
+ console.log("Current change surface:");
1407
+ for (const changeSet of preLandChanges){
1408
+ console.log(`- ${changeSet.agent}: ${changeSet.paths.length} path(s)${changeSet.paths.length > 0 ? ` | ${changeSet.paths.join(", ")}` : ""}`);
1409
+ }
1161
1410
  console.log("Queued integration task for codex:");
1162
1411
  for (const filePath of overlappingPaths){
1163
1412
  console.log(`- ${filePath}`);
@@ -1175,6 +1424,7 @@ async function commandLand(cwd) {
1175
1424
  const releasedClaims = releasePathClaims(session, {
1176
1425
  note: `Released after landing into ${targetBranch}.`
1177
1426
  });
1427
+ session.baseCommit = await getBranchCommit(repoRoot, targetBranch);
1178
1428
  for (const claim of releasedClaims){
1179
1429
  addDecisionRecord(session, {
1180
1430
  kind: "integration",
@@ -1236,10 +1486,40 @@ async function commandLand(cwd) {
1236
1486
  artifact.reviewNotes = session.reviewNotes.filter((note)=>note.taskId === taskId);
1237
1487
  await saveTaskArtifact(paths, artifact);
1238
1488
  }
1489
+ const artifacts = await listTaskArtifacts(paths);
1490
+ const postLandSnapshot = await loadSnapshot(paths, 60);
1491
+ const postLandSummary = buildWorkflowSummary(postLandSnapshot, artifacts);
1492
+ const landReport = buildLandReport({
1493
+ id: buildSessionId(),
1494
+ sessionId: session.id,
1495
+ goal: session.goal,
1496
+ createdAt: new Date().toISOString(),
1497
+ targetBranch,
1498
+ integrationBranch: result.integrationBranch,
1499
+ integrationPath: result.integrationPath,
1500
+ validationCommand: session.config.validationCommand,
1501
+ validationStatus: result.validation.status,
1502
+ validationDetail: result.validation.detail,
1503
+ changedByAgent: preLandChanges,
1504
+ completedTasks: postLandSummary.completedTasks,
1505
+ snapshotCommits: result.snapshotCommits,
1506
+ commandsRun: result.commandsRun,
1507
+ reviewThreadsLanded: landedReviewNotes.length,
1508
+ openReviewThreadsRemaining: session.reviewNotes.filter((note)=>note.status === "open").length
1509
+ });
1510
+ await saveLandReport(paths, landReport);
1239
1511
  await notifyOperatorSurface(paths, "land.completed");
1240
1512
  console.log(`Landed branches into ${targetBranch}`);
1241
1513
  console.log(`Integration branch: ${result.integrationBranch}`);
1242
1514
  console.log(`Integration worktree: ${result.integrationPath}`);
1515
+ console.log("Merged change surface:");
1516
+ for (const changeSet of preLandChanges){
1517
+ console.log(`- ${changeSet.agent}: ${changeSet.paths.length} path(s)${changeSet.paths.length > 0 ? ` | ${changeSet.paths.join(", ")}` : ""}`);
1518
+ }
1519
+ console.log(`Validation: ${result.validation.command || "(none configured)"} | ${result.validation.status} | ${result.validation.detail}`);
1520
+ console.log(`Review threads landed: ${landedReviewNotes.length}`);
1521
+ console.log(`Result report: ${landReport.id}`);
1522
+ console.log("Inspect result: kavi result");
1243
1523
  for (const snapshot of result.snapshotCommits){
1244
1524
  console.log(`Snapshot ${snapshot.agent}: ${snapshot.commit}${snapshot.createdCommit ? " (created)" : " (unchanged)"}`);
1245
1525
  }
@@ -1269,6 +1549,18 @@ async function commandHook(args) {
1269
1549
  };
1270
1550
  if (session && agent === "claude" && eventName === "PreToolUse") {
1271
1551
  const descriptor = describeToolUse(payload);
1552
+ if (session.fullAccessMode) {
1553
+ console.log(JSON.stringify({
1554
+ continue: true,
1555
+ suppressOutput: true,
1556
+ hookSpecificOutput: {
1557
+ hookEventName: "PreToolUse",
1558
+ permissionDecision: "allow",
1559
+ permissionDecisionReason: `Kavi approve-all bypassed approval: ${descriptor.summary}`
1560
+ }
1561
+ }));
1562
+ return;
1563
+ }
1272
1564
  if (CLAUDE_AUTO_ALLOW_TOOLS.has(descriptor.toolName)) {
1273
1565
  await recordEvent(paths, session.id, "approval.auto_allowed", {
1274
1566
  agent,
@@ -1386,9 +1678,18 @@ async function main() {
1386
1678
  case "resume":
1387
1679
  await commandResume(cwd);
1388
1680
  break;
1681
+ case "summary":
1682
+ await commandSummary(cwd, args);
1683
+ break;
1684
+ case "result":
1685
+ await commandResult(cwd, args);
1686
+ break;
1389
1687
  case "status":
1390
1688
  await commandStatus(cwd, args);
1391
1689
  break;
1690
+ case "activity":
1691
+ await commandActivity(cwd, args);
1692
+ break;
1392
1693
  case "route":
1393
1694
  await commandRoute(cwd, args);
1394
1695
  break;
@@ -1407,6 +1708,12 @@ async function main() {
1407
1708
  case "recommend-apply":
1408
1709
  await commandRecommendApply(cwd, args);
1409
1710
  break;
1711
+ case "recommend-dismiss":
1712
+ await commandRecommendDismiss(cwd, args);
1713
+ break;
1714
+ case "recommend-restore":
1715
+ await commandRecommendRestore(cwd, args);
1716
+ break;
1410
1717
  case "tasks":
1411
1718
  await commandTasks(cwd, args);
1412
1719
  break;