@mandipadk7/kavi 0.1.5 → 0.1.7
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 +12 -1
- package/dist/adapters/shared.js +2 -0
- package/dist/daemon.js +18 -1
- package/dist/decision-ledger.js +90 -0
- package/dist/doctor.js +5 -0
- package/dist/main.js +376 -3
- package/dist/ownership.js +276 -0
- package/dist/package-info.js +15 -0
- package/dist/process.js +16 -0
- package/dist/recommendations.js +139 -0
- package/dist/router.js +41 -53
- package/dist/tui.js +74 -18
- package/dist/update.js +36 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,16 +3,22 @@
|
|
|
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.
|
|
13
15
|
- `kavi status`: inspect session health, task counts, and configured routing ownership rules from any terminal.
|
|
16
|
+
- `kavi route`: preview how Kavi would route a prompt before enqueuing it.
|
|
17
|
+
- `kavi routes`: inspect recent task routing decisions with strategy, confidence, and metadata.
|
|
14
18
|
- `kavi paths`: inspect resolved repo-local, user-local, worktree, and runtime paths.
|
|
15
19
|
- `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.
|
|
21
|
+
- `kavi recommend-apply`: turn an actionable handoff or integration recommendation into a queued managed task.
|
|
16
22
|
- `kavi tasks`: inspect the session task list with summaries and artifact availability.
|
|
17
23
|
- `kavi task-output`: inspect the normalized envelope and raw output for a completed task.
|
|
18
24
|
- `kavi decisions`: inspect the persisted routing, approval, task, and integration decisions.
|
|
@@ -33,6 +39,7 @@ Runtime model:
|
|
|
33
39
|
- SQLite event mirroring is opt-in with `KAVI_ENABLE_SQLITE_HISTORY=1`; the default event log is JSONL under `.kavi/state/events.jsonl`.
|
|
34
40
|
- 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.
|
|
35
41
|
- 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.
|
|
42
|
+
- 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.
|
|
36
43
|
- 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.
|
|
37
44
|
|
|
38
45
|
Notes:
|
|
@@ -41,12 +48,16 @@ Notes:
|
|
|
41
48
|
- Claude still runs through the installed `claude` CLI with Kavi-managed hooks and approval decisions.
|
|
42
49
|
- `kavi doctor` now checks Claude auth readiness with `claude auth status`, and startup blocks if Claude is installed but not authenticated.
|
|
43
50
|
- `kavi doctor` also validates ownership path rules for duplicates, repo escapes, and absolute-path mistakes before those rules affect routing.
|
|
51
|
+
- `kavi doctor` now also flags overlapping cross-agent ownership rules that do not produce a clear specificity winner.
|
|
44
52
|
- 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.
|
|
45
53
|
- The socket is machine-local rather than repo-local to avoid Unix socket path-length issues in deep repos and temp directories.
|
|
46
|
-
- 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.
|
|
54
|
+
- 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.
|
|
47
55
|
- 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
|
+
- The claims inspector now shows active overlap hotspots and ownership-rule conflicts so routing pressure points are visible directly in the operator surface.
|
|
57
|
+
- 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.
|
|
48
58
|
- 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.
|
|
49
59
|
- 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
|
+
- `kavi update --check` reports newer published builds without installing them, and `kavi update` can apply a chosen `latest`, `beta`, or exact version after confirmation.
|
|
50
61
|
|
|
51
62
|
Install commands for testers:
|
|
52
63
|
|
package/dist/adapters/shared.js
CHANGED
|
@@ -4,11 +4,13 @@ function formatDecisionLine(summary, detail) {
|
|
|
4
4
|
return detail.trim() ? `- ${summary}: ${detail}` : `- ${summary}`;
|
|
5
5
|
}
|
|
6
6
|
export function buildDecisionReplay(session, task, agent) {
|
|
7
|
+
const winningRule = task.routeMetadata?.winningRule && typeof task.routeMetadata.winningRule === "object" && !Array.isArray(task.routeMetadata.winningRule) && typeof task.routeMetadata.winningRule.pattern === "string" ? String(task.routeMetadata.winningRule.pattern) : null;
|
|
7
8
|
const taskDecisions = session.decisions.filter((decision)=>decision.taskId === task.id).slice(-6).map((decision)=>formatDecisionLine(`[${decision.kind}] ${decision.summary}`, decision.detail));
|
|
8
9
|
const sharedDecisions = session.decisions.filter((decision)=>decision.taskId !== task.id && (decision.agent === agent || decision.agent === null)).slice(-4).map((decision)=>formatDecisionLine(`[${decision.kind}] ${decision.summary}`, decision.detail));
|
|
9
10
|
const relevantClaims = session.pathClaims.filter((claim)=>claim.status === "active" && (claim.taskId === task.id || claim.agent !== agent)).slice(-6).map((claim)=>`- ${claim.agent} ${claim.source} claim on ${claim.paths.join(", ")}${claim.note ? `: ${claim.note}` : ""}`);
|
|
10
11
|
const replay = [
|
|
11
12
|
`- Current route reason: ${task.routeReason ?? "not recorded"}`,
|
|
13
|
+
`- Winning ownership rule: ${winningRule ?? "none"}`,
|
|
12
14
|
`- Current claimed paths: ${task.claimedPaths.join(", ") || "none"}`,
|
|
13
15
|
...taskDecisions,
|
|
14
16
|
...sharedDecisions,
|
package/dist/daemon.js
CHANGED
|
@@ -1002,7 +1002,10 @@ export class KaviDaemon {
|
|
|
1002
1002
|
return;
|
|
1003
1003
|
}
|
|
1004
1004
|
const changedPaths = await listWorktreeChangedPaths(worktree.path, this.session.baseCommit);
|
|
1005
|
-
const
|
|
1005
|
+
const previousClaimedPaths = [
|
|
1006
|
+
...task.claimedPaths
|
|
1007
|
+
];
|
|
1008
|
+
const hadClaimedSurface = previousClaimedPaths.length > 0 || this.session.pathClaims.some((claim)=>claim.taskId === task.id && claim.status === "active");
|
|
1006
1009
|
task.claimedPaths = changedPaths;
|
|
1007
1010
|
const claim = upsertPathClaim(this.session, {
|
|
1008
1011
|
taskId: task.id,
|
|
@@ -1031,6 +1034,14 @@ export class KaviDaemon {
|
|
|
1031
1034
|
supersededByPaths: changedPaths
|
|
1032
1035
|
}
|
|
1033
1036
|
});
|
|
1037
|
+
await recordEvent(this.paths, this.session.id, "claim.superseded", {
|
|
1038
|
+
claimId: releasedClaim.id,
|
|
1039
|
+
taskId: releasedClaim.taskId,
|
|
1040
|
+
agent: releasedClaim.agent,
|
|
1041
|
+
paths: releasedClaim.paths,
|
|
1042
|
+
supersededByTaskId: task.id,
|
|
1043
|
+
supersededByPaths: changedPaths
|
|
1044
|
+
});
|
|
1034
1045
|
}
|
|
1035
1046
|
return;
|
|
1036
1047
|
}
|
|
@@ -1045,6 +1056,12 @@ export class KaviDaemon {
|
|
|
1045
1056
|
releaseReason: "empty-diff-claim"
|
|
1046
1057
|
}
|
|
1047
1058
|
});
|
|
1059
|
+
await recordEvent(this.paths, this.session.id, "claim.released", {
|
|
1060
|
+
taskId: task.id,
|
|
1061
|
+
agent: task.owner,
|
|
1062
|
+
paths: previousClaimedPaths,
|
|
1063
|
+
reason: "empty-diff-claim"
|
|
1064
|
+
});
|
|
1048
1065
|
}
|
|
1049
1066
|
}
|
|
1050
1067
|
}
|
package/dist/decision-ledger.js
CHANGED
|
@@ -21,6 +21,17 @@ function pathOverlaps(left, right) {
|
|
|
21
21
|
}
|
|
22
22
|
return leftPath === rightPath || leftPath.startsWith(`${rightPath}/`) || rightPath.startsWith(`${leftPath}/`);
|
|
23
23
|
}
|
|
24
|
+
function overlapPath(left, right) {
|
|
25
|
+
const leftPath = normalizePath(left);
|
|
26
|
+
const rightPath = normalizePath(right);
|
|
27
|
+
if (!pathOverlaps(leftPath, rightPath)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (leftPath === rightPath) {
|
|
31
|
+
return leftPath;
|
|
32
|
+
}
|
|
33
|
+
return leftPath.startsWith(`${rightPath}/`) ? leftPath : rightPath;
|
|
34
|
+
}
|
|
24
35
|
export function addDecisionRecord(session, input) {
|
|
25
36
|
const record = {
|
|
26
37
|
id: randomUUID(),
|
|
@@ -126,6 +137,85 @@ export function findClaimConflicts(session, owner, claimedPaths) {
|
|
|
126
137
|
}
|
|
127
138
|
return activePathClaims(session).filter((claim)=>claim.agent !== owner && claim.paths.some((item)=>normalizedPaths.some((candidate)=>pathOverlaps(item, candidate))));
|
|
128
139
|
}
|
|
140
|
+
export function buildClaimHotspots(session) {
|
|
141
|
+
const hotspots = new Map();
|
|
142
|
+
const claims = activePathClaims(session);
|
|
143
|
+
for(let index = 0; index < claims.length; index += 1){
|
|
144
|
+
const left = claims[index];
|
|
145
|
+
if (!left) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
for(let inner = index + 1; inner < claims.length; inner += 1){
|
|
149
|
+
const right = claims[inner];
|
|
150
|
+
if (!right || left.taskId === right.taskId) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const overlappingPaths = left.paths.flatMap((leftPath)=>right.paths.map((rightPath)=>overlapPath(leftPath, rightPath)).filter((value)=>Boolean(value)));
|
|
154
|
+
if (overlappingPaths.length === 0) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
for (const hotspotPath of overlappingPaths){
|
|
158
|
+
const existing = hotspots.get(hotspotPath);
|
|
159
|
+
if (existing) {
|
|
160
|
+
existing.overlapCount += 1;
|
|
161
|
+
existing.agents = [
|
|
162
|
+
...new Set([
|
|
163
|
+
...existing.agents,
|
|
164
|
+
left.agent,
|
|
165
|
+
right.agent
|
|
166
|
+
])
|
|
167
|
+
];
|
|
168
|
+
existing.taskIds = [
|
|
169
|
+
...new Set([
|
|
170
|
+
...existing.taskIds,
|
|
171
|
+
left.taskId,
|
|
172
|
+
right.taskId
|
|
173
|
+
])
|
|
174
|
+
];
|
|
175
|
+
existing.claimIds = [
|
|
176
|
+
...new Set([
|
|
177
|
+
...existing.claimIds,
|
|
178
|
+
left.id,
|
|
179
|
+
right.id
|
|
180
|
+
])
|
|
181
|
+
];
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
hotspots.set(hotspotPath, {
|
|
185
|
+
path: hotspotPath,
|
|
186
|
+
agents: [
|
|
187
|
+
...new Set([
|
|
188
|
+
left.agent,
|
|
189
|
+
right.agent
|
|
190
|
+
])
|
|
191
|
+
],
|
|
192
|
+
taskIds: [
|
|
193
|
+
...new Set([
|
|
194
|
+
left.taskId,
|
|
195
|
+
right.taskId
|
|
196
|
+
])
|
|
197
|
+
],
|
|
198
|
+
claimIds: [
|
|
199
|
+
...new Set([
|
|
200
|
+
left.id,
|
|
201
|
+
right.id
|
|
202
|
+
])
|
|
203
|
+
],
|
|
204
|
+
overlapCount: 1
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return [
|
|
210
|
+
...hotspots.values()
|
|
211
|
+
].sort((left, right)=>{
|
|
212
|
+
const overlapDelta = right.overlapCount - left.overlapCount;
|
|
213
|
+
if (overlapDelta !== 0) {
|
|
214
|
+
return overlapDelta;
|
|
215
|
+
}
|
|
216
|
+
return left.path.localeCompare(right.path);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
129
219
|
|
|
130
220
|
|
|
131
221
|
//# sourceURL=decision-ledger.ts
|
package/dist/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { loadConfig } from "./config.js";
|
|
3
3
|
import { fileExists } from "./fs.js";
|
|
4
|
+
import { findOwnershipRuleConflicts } from "./ownership.js";
|
|
4
5
|
import { runCommand } from "./process.js";
|
|
5
6
|
import { hasSupportedNode, minimumNodeMajor, resolveSessionRuntime } from "./runtime.js";
|
|
6
7
|
export function parseClaudeAuthStatus(output) {
|
|
@@ -87,6 +88,10 @@ export function validateRoutingPathRules(config) {
|
|
|
87
88
|
...new Set(overlappingRules)
|
|
88
89
|
].join(", ")}`);
|
|
89
90
|
}
|
|
91
|
+
const ambiguousConflicts = findOwnershipRuleConflicts(config).filter((conflict)=>conflict.kind === "ambiguous-overlap").map((conflict)=>`${conflict.leftOwner}:${conflict.leftPattern} <> ${conflict.rightOwner}:${conflict.rightPattern}`);
|
|
92
|
+
if (ambiguousConflicts.length > 0) {
|
|
93
|
+
issues.push(`Ownership rules have ambiguous overlaps without a specificity winner: ${ambiguousConflicts.join(", ")}`);
|
|
94
|
+
}
|
|
90
95
|
return issues;
|
|
91
96
|
}
|
|
92
97
|
function normalizeVersion(output) {
|