@mandipadk7/kavi 0.1.6 → 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 CHANGED
@@ -3,18 +3,26 @@
3
3
  Kavi is a local terminal control plane for managed Codex and Claude collaboration.
4
4
 
5
5
  Current capabilities:
6
+ - `kavi version` and `kavi --version`: print the installed package version.
6
7
  - `kavi init`: create repo-local `.kavi` config, prompt files, ignore rules, and bootstrap git if the folder is not already a repository.
7
8
  - `kavi init --home`: also scaffold the user-local config file used for binary overrides.
8
9
  - `kavi init --no-commit`: skip the bootstrap commit and let `kavi open` or `kavi start` create the first base commit later.
9
10
  - `kavi doctor`: verify Node, Codex, Claude, git worktree support, and local readiness.
11
+ - `kavi update`: check for and install a newer published Kavi package from npm.
10
12
  - `kavi start`: start a managed session without attaching the TUI.
11
13
  - `kavi open`: create a managed session with separate Codex and Claude worktrees and open the full-screen operator console, even from an empty folder or a repo with no `HEAD` yet.
12
14
  - `kavi resume`: reopen the operator console for the current repo session.
15
+ - `kavi summary`: show a cohesive session-level view of progress, current changes, recent activity, and landing readiness.
16
+ - `kavi result`: show the current or latest landed outcome as one cohesive result surface, including per-agent output and merged landing details.
13
17
  - `kavi status`: inspect session health, task counts, and configured routing ownership rules from any terminal.
18
+ - `kavi activity`: show the session as a linear activity stream instead of raw daemon events.
14
19
  - `kavi route`: preview how Kavi would route a prompt before enqueuing it.
15
20
  - `kavi routes`: inspect recent task routing decisions with strategy, confidence, and metadata.
16
21
  - `kavi paths`: inspect resolved repo-local, user-local, worktree, and runtime paths.
17
22
  - `kavi task`: enqueue a task for `codex`, `claude`, or `auto` routing.
23
+ - `kavi recommend`: inspect integration, handoff, and ownership-configuration recommendations derived from live claims, reviews, and routing state, with filters for kind, target agent, and active vs dismissed status.
24
+ - `kavi recommend-apply`: turn an actionable handoff or integration recommendation into a queued managed task.
25
+ - `kavi recommend-dismiss` and `kavi recommend-restore`: manage the recommendation inbox without losing the underlying session context.
18
26
  - `kavi tasks`: inspect the session task list with summaries and artifact availability.
19
27
  - `kavi task-output`: inspect the normalized envelope and raw output for a completed task.
20
28
  - `kavi decisions`: inspect the persisted routing, approval, task, and integration decisions.
@@ -33,8 +41,10 @@ Runtime model:
33
41
  - User-local runtime overrides live in `~/.config/kavi/config.toml` and can point Kavi at custom `node`, `codex`, and `claude` binaries.
34
42
  - The operator surface talks to the daemon over a local control socket under the machine-local state root.
35
43
  - SQLite event mirroring is opt-in with `KAVI_ENABLE_SQLITE_HISTORY=1`; the default event log is JSONL under `.kavi/state/events.jsonl`.
36
- - The operator console exposes a task board, dual agent lanes, a live inspector pane, approval actions, inline task composition, worktree diff review with file and hunk navigation, and persisted operator review threads on files or hunks.
44
+ - Landed result reports are persisted under `.kavi/state/reports/`, so the latest merged outcome remains inspectable after the landing command exits.
45
+ - The operator console now opens on an activity-first view so the session reads like a linear progress log, while still exposing dedicated results, task board, dual agent lanes, approvals, diff review, and persisted operator review threads when you need to drill in.
37
46
  - Routing can now use explicit path ownership rules from `.kavi/config.toml` via `[routing].codex_paths` and `[routing].claude_paths`, so known parts of the tree can bypass looser keyword or AI routing.
47
+ - Validation is optional by default for new repos. Set `validation_command` in `.kavi/config.toml` once the project has a real test or build command you want Kavi to enforce during `land`.
38
48
  - Ownership routing now prefers the strongest matching rule, not just the side with the most raw glob hits, and route metadata records the winning rule when one exists.
39
49
  - Diff-based path claims now release older overlapping same-agent claims automatically, so the active claim set stays closer to each managed worktree's current unlanded surface.
40
50
 
@@ -47,20 +57,29 @@ Notes:
47
57
  - `kavi doctor` now also flags overlapping cross-agent ownership rules that do not produce a clear specificity winner.
48
58
  - The dashboard and operator commands now use the daemon's local RPC socket instead of editing session files directly, and the TUI stays updated from pushed daemon snapshots rather than polling.
49
59
  - The socket is machine-local rather than repo-local to avoid Unix socket path-length issues in deep repos and temp directories.
50
- - The console is keyboard-driven: `1-7` switch views, `j/k` move selection, `[` and `]` cycle task detail sections, `,` and `.` cycle changed files, `{` and `}` cycle patch hunks, `A/C/Q/M` add review notes, `o/O` cycle existing threads, `T` reply, `E` edit, `R` resolve or reopen, `a` cycle thread assignee, `w` mark a thread as won't-fix, `x` mark it as accepted-risk, `F` queue a fix task, `H` queue a handoff task, `y/n` resolve approvals, and `c` opens the inline task composer with live route preview diagnostics.
60
+ - The console is keyboard-driven: `1-9` switch views, `j/k` move selection, `[` and `]` cycle task detail sections, `,` and `.` cycle changed files, `{` and `}` cycle patch hunks, `A/C/Q/M` add review notes, `o/O` cycle existing threads, `T` reply, `E` edit, `R` resolve or reopen, `a` cycle thread assignee, `w` mark a thread as won't-fix, `x` mark it as accepted-risk, `F` queue a fix task, `H` queue a handoff task, `y/n` resolve approvals, and `c` opens the inline task composer with live route preview diagnostics.
51
61
  - Review filters are available both in the CLI and the TUI: `kavi reviews --assignee operator --status open`, and inside the console use `u`, `v`, and `d` to cycle assignee, status, and disposition filters for the active diff context.
52
62
  - The claims inspector now shows active overlap hotspots and ownership-rule conflicts so routing pressure points are visible directly in the operator surface.
63
+ - The claims and decision inspectors now also show recommendation-driven next actions, so hotspots, cross-agent review pressure, and ownership-config problems can be turned into follow-up tasks directly from the operator surface.
64
+ - Recommendations now have a persisted lifecycle of their own: they can be dismissed, restored, and tracked across follow-up tasks, and repeated applies are guarded when an open follow-up task already exists.
53
65
  - Review threads now carry explicit assignees and richer dispositions, including `accepted risk` and `won't fix`, instead of relying only on free-form note text.
54
66
  - Successful follow-up tasks now auto-resolve linked open review threads, landed follow-up work marks those resolved threads as landed, and replying to a resolved thread reopens it.
67
+ - `kavi update --check` reports newer published builds without installing them, and `kavi update` can apply a chosen `latest`, `beta`, or exact version after confirmation.
68
+ - The operator console now includes a dedicated recommendations view. Use `8` to switch there, `Enter` to apply a recommendation, `P` to force an additional follow-up task when one is already open, `z` to dismiss, and `Z` to restore.
69
+ - `kavi land` now prints a clearer merged-result summary, including the pre-land change surface by agent, validation status, and how many review threads were landed as part of the merge.
70
+ - After `kavi land`, run `kavi result` to inspect the persisted merged-result report and the latest per-agent outcome in one place.
55
71
 
56
- Install commands for testers:
72
+ Install commands:
57
73
 
58
74
  ```bash
75
+ # stable
76
+ npm install -g @mandipadk7/kavi
77
+
59
78
  # beta channel
60
79
  npm install -g @mandipadk7/kavi@beta
61
80
 
62
81
  # one-off
63
- npx @mandipadk7/kavi@beta help
82
+ npx @mandipadk7/kavi help
64
83
  ```
65
84
 
66
85
  User-local config example:
@@ -202,9 +202,12 @@ export async function writeClaudeSettings(paths, session) {
202
202
  };
203
203
  await fs.writeFile(paths.claudeSettingsFile, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
204
204
  }
205
+ export function resolveClaudeSessionId(session) {
206
+ return session.agentStatus.claude.sessionId ?? session.id;
207
+ }
205
208
  export async function runClaudeTask(session, task, paths) {
206
209
  const worktree = findWorktree(session, "claude");
207
- const claudeSessionId = session.agentStatus.claude.sessionId ?? `${session.id}-claude`;
210
+ const claudeSessionId = resolveClaudeSessionId(session);
208
211
  await writeClaudeSettings(paths, session);
209
212
  const repoPrompt = await loadAgentPrompt(paths, "claude");
210
213
  const prompt = [
package/dist/config.js CHANGED
@@ -4,7 +4,7 @@ import { ensureDir, fileExists } from "./fs.js";
4
4
  import { parseToml } from "./toml.js";
5
5
  const DEFAULT_CONFIG = `version = 1
6
6
  base_branch = "main"
7
- validation_command = "npm test"
7
+ validation_command = ""
8
8
  message_limit = 6
9
9
 
10
10
  [routing]
@@ -57,7 +57,7 @@ export function defaultConfig() {
57
57
  return {
58
58
  version: 1,
59
59
  baseBranch: "main",
60
- validationCommand: "npm test",
60
+ validationCommand: "",
61
61
  messageLimit: 6,
62
62
  routing: {
63
63
  frontendKeywords: [
@@ -109,6 +109,7 @@ export async function ensureProjectScaffold(paths) {
109
109
  await ensureDir(paths.kaviDir);
110
110
  await ensureDir(paths.promptsDir);
111
111
  await ensureDir(paths.stateDir);
112
+ await ensureDir(paths.reportsDir);
112
113
  await ensureDir(paths.runtimeDir);
113
114
  await ensureDir(paths.runsDir);
114
115
  if (!await fileExists(paths.configFile)) {
@@ -140,7 +141,7 @@ export async function loadConfig(paths) {
140
141
  return {
141
142
  version: asNumber(parsed.version, 1),
142
143
  baseBranch: asString(parsed.base_branch, "main"),
143
- validationCommand: asString(parsed.validation_command, "npm test"),
144
+ validationCommand: asString(parsed.validation_command, ""),
144
145
  messageLimit: asNumber(parsed.message_limit, 6),
145
146
  routing: {
146
147
  frontendKeywords: asStringArray(routing.frontend_keywords, defaultConfig().routing.frontendKeywords),
package/dist/daemon.js CHANGED
@@ -9,6 +9,8 @@ import { listApprovalRequests, resolveApprovalRequest } from "./approvals.js";
9
9
  import { addDecisionRecord, releaseSupersededClaims, upsertPathClaim } from "./decision-ledger.js";
10
10
  import { getWorktreeDiffReview, listWorktreeChangedPaths } from "./git.js";
11
11
  import { nowIso } from "./paths.js";
12
+ import { dismissOperatorRecommendation, recordRecommendationApplied, restoreOperatorRecommendation } from "./recommendations.js";
13
+ import { loadLatestLandReport } from "./reports.js";
12
14
  import { addReviewReply, addReviewNote, autoResolveReviewNotesForCompletedTask, linkReviewFollowUpTask, reviewNotesForTask, setReviewNoteStatus, updateReviewNote } from "./reviews.js";
13
15
  import { buildAdHocTask, buildKickoffTasks } from "./router.js";
14
16
  import { loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord } from "./session.js";
@@ -171,6 +173,20 @@ export class KaviDaemon {
171
173
  ok: true
172
174
  }
173
175
  };
176
+ case "dismissRecommendation":
177
+ await this.dismissRecommendationFromRpc(params);
178
+ return {
179
+ result: {
180
+ ok: true
181
+ }
182
+ };
183
+ case "restoreRecommendation":
184
+ await this.restoreRecommendationFromRpc(params);
185
+ return {
186
+ result: {
187
+ ok: true
188
+ }
189
+ };
174
190
  case "shutdown":
175
191
  return {
176
192
  result: {
@@ -253,11 +269,13 @@ export class KaviDaemon {
253
269
  agent: worktree.agent,
254
270
  paths: await listWorktreeChangedPaths(worktree.path, session.baseCommit).catch(()=>[])
255
271
  })));
272
+ const latestLandReport = await loadLatestLandReport(this.paths);
256
273
  return {
257
274
  session,
258
275
  events,
259
276
  approvals,
260
- worktreeDiffs
277
+ worktreeDiffs,
278
+ latestLandReport
261
279
  };
262
280
  }
263
281
  async runMutation(fn) {
@@ -310,14 +328,58 @@ export class KaviDaemon {
310
328
  paths: task.claimedPaths,
311
329
  note: task.routeReason
312
330
  });
331
+ if (typeof params.recommendationId === "string") {
332
+ recordRecommendationApplied(this.session, params.recommendationId, taskId);
333
+ }
313
334
  await saveSessionRecord(this.paths, this.session);
314
335
  await recordEvent(this.paths, this.session.id, "task.enqueued", {
315
336
  owner,
316
- via: "rpc"
337
+ via: "rpc",
338
+ recommendationId: typeof params.recommendationId === "string" ? params.recommendationId : null
317
339
  });
340
+ if (typeof params.recommendationId === "string" && (params.recommendationKind === "handoff" || params.recommendationKind === "integration" || params.recommendationKind === "ownership-config")) {
341
+ await recordEvent(this.paths, this.session.id, "recommendation.applied", {
342
+ recommendationId: params.recommendationId,
343
+ recommendationKind: params.recommendationKind,
344
+ owner,
345
+ taskId
346
+ });
347
+ }
318
348
  await this.publishSnapshot("task.enqueued");
319
349
  });
320
350
  }
351
+ async dismissRecommendationFromRpc(params) {
352
+ await this.runMutation(async ()=>{
353
+ const recommendationId = typeof params.recommendationId === "string" ? params.recommendationId : "";
354
+ if (!recommendationId) {
355
+ throw new Error("dismissRecommendation requires a recommendationId.");
356
+ }
357
+ const reason = typeof params.reason === "string" ? params.reason : null;
358
+ const recommendation = dismissOperatorRecommendation(this.session, recommendationId, reason);
359
+ await saveSessionRecord(this.paths, this.session);
360
+ await recordEvent(this.paths, this.session.id, "recommendation.dismissed", {
361
+ recommendationId,
362
+ kind: recommendation.kind,
363
+ reason
364
+ });
365
+ await this.publishSnapshot("recommendation.dismissed");
366
+ });
367
+ }
368
+ async restoreRecommendationFromRpc(params) {
369
+ await this.runMutation(async ()=>{
370
+ const recommendationId = typeof params.recommendationId === "string" ? params.recommendationId : "";
371
+ if (!recommendationId) {
372
+ throw new Error("restoreRecommendation requires a recommendationId.");
373
+ }
374
+ const recommendation = restoreOperatorRecommendation(this.session, recommendationId);
375
+ await saveSessionRecord(this.paths, this.session);
376
+ await recordEvent(this.paths, this.session.id, "recommendation.restored", {
377
+ recommendationId,
378
+ kind: recommendation.kind
379
+ });
380
+ await this.publishSnapshot("recommendation.restored");
381
+ });
382
+ }
321
383
  async kickoffFromRpc(params) {
322
384
  await this.runMutation(async ()=>{
323
385
  const prompt = typeof params.prompt === "string" ? params.prompt : "";
@@ -823,10 +885,22 @@ export class KaviDaemon {
823
885
  paths: task.claimedPaths,
824
886
  note: task.routeReason
825
887
  });
888
+ if (typeof command.payload.recommendationId === "string") {
889
+ recordRecommendationApplied(this.session, command.payload.recommendationId, taskId);
890
+ }
826
891
  await saveSessionRecord(this.paths, this.session);
827
892
  await recordEvent(this.paths, this.session.id, "task.enqueued", {
828
- owner
893
+ owner,
894
+ recommendationId: typeof command.payload.recommendationId === "string" ? command.payload.recommendationId : null
829
895
  });
896
+ if (typeof command.payload.recommendationId === "string" && (command.payload.recommendationKind === "handoff" || command.payload.recommendationKind === "integration" || command.payload.recommendationKind === "ownership-config")) {
897
+ await recordEvent(this.paths, this.session.id, "recommendation.applied", {
898
+ recommendationId: command.payload.recommendationId,
899
+ recommendationKind: command.payload.recommendationKind,
900
+ owner,
901
+ taskId
902
+ });
903
+ }
830
904
  await this.publishSnapshot("task.enqueued");
831
905
  }
832
906
  }
package/dist/git.js CHANGED
@@ -342,6 +342,32 @@ export async function listWorktreeChangedPaths(worktreePath, baseCommit) {
342
342
  ...parsePathList(untracked.stdout)
343
343
  ]);
344
344
  }
345
+ export async function resolveValidationPlan(integrationPath, validationCommand) {
346
+ const trimmed = validationCommand.trim();
347
+ if (!trimmed) {
348
+ return {
349
+ command: "",
350
+ status: "not_configured",
351
+ detail: "No validation command was configured."
352
+ };
353
+ }
354
+ if (trimmed === "npm test") {
355
+ try {
356
+ await fs.access(path.join(integrationPath, "package.json"));
357
+ } catch {
358
+ return {
359
+ command: trimmed,
360
+ status: "skipped",
361
+ detail: 'Skipped default validation command "npm test" because package.json is not present yet.'
362
+ };
363
+ }
364
+ }
365
+ return {
366
+ command: trimmed,
367
+ status: "ran",
368
+ detail: `Validation ran with "${trimmed}".`
369
+ };
370
+ }
345
371
  export async function getWorktreeDiffReview(agent, worktreePath, baseCommit, filePath) {
346
372
  const changedPaths = await listWorktreeChangedPaths(worktreePath, baseCommit);
347
373
  const selectedPath = filePath && changedPaths.includes(filePath) ? filePath : changedPaths[0] ?? null;
@@ -461,16 +487,20 @@ export async function landBranches(repoRoot, targetBranch, worktrees, validation
461
487
  throw new Error(merge.stderr.trim() || `Unable to merge branch ${worktree.branch} into integration branch ${integrationBranch}.`);
462
488
  }
463
489
  }
464
- if (validationCommand.trim()) {
465
- const validation = await runCommand("zsh", [
490
+ const validation = await resolveValidationPlan(integrationPath, validationCommand);
491
+ if (validation.status === "skipped") {
492
+ commandsRun.push(`SKIP ${validation.command} (${validation.detail})`);
493
+ }
494
+ if (validation.status === "ran") {
495
+ const validationRun = await runCommand("zsh", [
466
496
  "-lc",
467
- validationCommand
497
+ validation.command
468
498
  ], {
469
499
  cwd: integrationPath
470
500
  });
471
- commandsRun.push(validationCommand);
472
- if (validation.code !== 0) {
473
- throw new Error(`Validation command failed.\n${validation.stdout}\n${validation.stderr}`.trim());
501
+ commandsRun.push(validation.command);
502
+ if (validationRun.code !== 0) {
503
+ throw new Error(`Validation command failed.\n${validationRun.stdout}\n${validationRun.stderr}`.trim());
474
504
  }
475
505
  }
476
506
  const currentBranch = await getCurrentBranch(repoRoot).catch(()=>"");
@@ -500,10 +530,35 @@ export async function landBranches(repoRoot, targetBranch, worktrees, validation
500
530
  throw new Error(updateRef.stderr.trim() || `Unable to advance ${targetBranch}; it changed while landing was in progress.`);
501
531
  }
502
532
  }
533
+ const landedHead = await getBranchCommit(repoRoot, targetBranch);
534
+ for (const worktree of worktrees){
535
+ const reset = await runCommand("git", [
536
+ "reset",
537
+ "--hard",
538
+ landedHead
539
+ ], {
540
+ cwd: worktree.path
541
+ });
542
+ commandsRun.push(`git -C ${worktree.path} reset --hard ${landedHead}`);
543
+ if (reset.code !== 0) {
544
+ throw new Error(reset.stderr.trim() || `Unable to reset managed worktree ${worktree.path} to landed head ${landedHead}.`);
545
+ }
546
+ const clean = await runCommand("git", [
547
+ "clean",
548
+ "-fd"
549
+ ], {
550
+ cwd: worktree.path
551
+ });
552
+ commandsRun.push(`git -C ${worktree.path} clean -fd`);
553
+ if (clean.code !== 0) {
554
+ throw new Error(clean.stderr.trim() || `Unable to clean managed worktree ${worktree.path}.`);
555
+ }
556
+ }
503
557
  return {
504
558
  commandsRun,
505
559
  integrationBranch,
506
560
  integrationPath,
561
+ validation,
507
562
  snapshotCommits
508
563
  };
509
564
  }