@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/README.md
CHANGED
|
@@ -12,13 +12,17 @@ Current capabilities:
|
|
|
12
12
|
- `kavi start`: start a managed session without attaching the TUI.
|
|
13
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.
|
|
14
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.
|
|
15
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.
|
|
16
19
|
- `kavi route`: preview how Kavi would route a prompt before enqueuing it.
|
|
17
20
|
- `kavi routes`: inspect recent task routing decisions with strategy, confidence, and metadata.
|
|
18
21
|
- `kavi paths`: inspect resolved repo-local, user-local, worktree, and runtime paths.
|
|
19
22
|
- `kavi task`: enqueue a task for `codex`, `claude`, or `auto` routing.
|
|
20
|
-
- `kavi recommend`: inspect integration, handoff, and ownership-configuration recommendations derived from live claims, reviews, and routing state.
|
|
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.
|
|
21
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.
|
|
22
26
|
- `kavi tasks`: inspect the session task list with summaries and artifact availability.
|
|
23
27
|
- `kavi task-output`: inspect the normalized envelope and raw output for a completed task.
|
|
24
28
|
- `kavi decisions`: inspect the persisted routing, approval, task, and integration decisions.
|
|
@@ -37,8 +41,10 @@ Runtime model:
|
|
|
37
41
|
- User-local runtime overrides live in `~/.config/kavi/config.toml` and can point Kavi at custom `node`, `codex`, and `claude` binaries.
|
|
38
42
|
- The operator surface talks to the daemon over a local control socket under the machine-local state root.
|
|
39
43
|
- SQLite event mirroring is opt-in with `KAVI_ENABLE_SQLITE_HISTORY=1`; the default event log is JSONL under `.kavi/state/events.jsonl`.
|
|
40
|
-
-
|
|
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.
|
|
41
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`.
|
|
42
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.
|
|
43
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.
|
|
44
50
|
|
|
@@ -51,22 +57,29 @@ Notes:
|
|
|
51
57
|
- `kavi doctor` now also flags overlapping cross-agent ownership rules that do not produce a clear specificity winner.
|
|
52
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.
|
|
53
59
|
- The socket is machine-local rather than repo-local to avoid Unix socket path-length issues in deep repos and temp directories.
|
|
54
|
-
- The console is keyboard-driven: `1-
|
|
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.
|
|
55
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.
|
|
56
62
|
- The claims inspector now shows active overlap hotspots and ownership-rule conflicts so routing pressure points are visible directly in the operator surface.
|
|
57
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.
|
|
58
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.
|
|
59
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.
|
|
60
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.
|
|
61
71
|
|
|
62
|
-
Install commands
|
|
72
|
+
Install commands:
|
|
63
73
|
|
|
64
74
|
```bash
|
|
75
|
+
# stable
|
|
76
|
+
npm install -g @mandipadk7/kavi
|
|
77
|
+
|
|
65
78
|
# beta channel
|
|
66
79
|
npm install -g @mandipadk7/kavi@beta
|
|
67
80
|
|
|
68
81
|
# one-off
|
|
69
|
-
npx @mandipadk7/kavi
|
|
82
|
+
npx @mandipadk7/kavi help
|
|
70
83
|
```
|
|
71
84
|
|
|
72
85
|
User-local config example:
|
package/dist/adapters/claude.js
CHANGED
|
@@ -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
|
|
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 = "
|
|
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: "
|
|
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, "
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
497
|
+
validation.command
|
|
468
498
|
], {
|
|
469
499
|
cwd: integrationPath
|
|
470
500
|
});
|
|
471
|
-
commandsRun.push(
|
|
472
|
-
if (
|
|
473
|
-
throw new Error(`Validation command failed.\n${
|
|
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
|
}
|