@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.
@@ -1,5 +1,6 @@
1
1
  import { buildClaimHotspots } from "./decision-ledger.js";
2
2
  import { findOwnershipRuleConflicts } from "./ownership.js";
3
+ import { nowIso } from "./paths.js";
3
4
  function normalizePath(value) {
4
5
  return value.trim().replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
5
6
  }
@@ -11,7 +12,100 @@ function pathMatchesScope(scope, filePath) {
11
12
  function otherAgent(agent) {
12
13
  return agent === "codex" ? "claude" : "codex";
13
14
  }
14
- export function buildOperatorRecommendations(session) {
15
+ function stableSerialize(value) {
16
+ if (Array.isArray(value)) {
17
+ return `[${value.map((item)=>stableSerialize(item)).join(",")}]`;
18
+ }
19
+ if (value && typeof value === "object") {
20
+ return `{${Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, item])=>`${JSON.stringify(key)}:${stableSerialize(item)}`).join(",")}}`;
21
+ }
22
+ return JSON.stringify(value);
23
+ }
24
+ function recommendationFingerprint(draft) {
25
+ return stableSerialize({
26
+ id: draft.id,
27
+ kind: draft.kind,
28
+ title: draft.title,
29
+ detail: draft.detail,
30
+ targetAgent: draft.targetAgent,
31
+ filePath: draft.filePath,
32
+ taskIds: [
33
+ ...draft.taskIds
34
+ ].sort(),
35
+ reviewNoteIds: [
36
+ ...draft.reviewNoteIds
37
+ ].sort(),
38
+ metadata: draft.metadata
39
+ });
40
+ }
41
+ function recommendationPriority(kind) {
42
+ switch(kind){
43
+ case "integration":
44
+ return 0;
45
+ case "handoff":
46
+ return 1;
47
+ case "ownership-config":
48
+ return 2;
49
+ default:
50
+ return 3;
51
+ }
52
+ }
53
+ function recommendationToneSort(left, right) {
54
+ const statusDelta = Number(left.status === "dismissed") - Number(right.status === "dismissed");
55
+ if (statusDelta !== 0) {
56
+ return statusDelta;
57
+ }
58
+ const followUpDelta = Number(right.openFollowUpTaskIds.length > 0) - Number(left.openFollowUpTaskIds.length > 0);
59
+ if (followUpDelta !== 0) {
60
+ return followUpDelta;
61
+ }
62
+ const kindDelta = recommendationPriority(left.kind) - recommendationPriority(right.kind);
63
+ if (kindDelta !== 0) {
64
+ return kindDelta;
65
+ }
66
+ return left.title.localeCompare(right.title);
67
+ }
68
+ function recommendationStateFor(session, draft) {
69
+ return (session.recommendationStates ?? []).find((state)=>state.id === draft.id) ?? null;
70
+ }
71
+ function recommendationOpenFollowUpTaskIds(session, appliedTaskIds) {
72
+ return appliedTaskIds.filter((taskId)=>session.tasks.some((task)=>task.id === taskId && (task.status === "pending" || task.status === "running" || task.status === "blocked")));
73
+ }
74
+ function hydrateRecommendation(session, draft) {
75
+ const fingerprint = recommendationFingerprint(draft);
76
+ const persisted = recommendationStateFor(session, draft);
77
+ const dismissedStillMatches = persisted?.status === "dismissed" && persisted.fingerprint === fingerprint;
78
+ const status = dismissedStillMatches ? "dismissed" : "active";
79
+ const appliedTaskIds = persisted?.appliedTaskIds ?? [];
80
+ const openFollowUpTaskIds = recommendationOpenFollowUpTaskIds(session, appliedTaskIds);
81
+ return {
82
+ ...draft,
83
+ fingerprint,
84
+ status,
85
+ dismissedReason: status === "dismissed" ? persisted?.dismissedReason ?? null : null,
86
+ dismissedAt: status === "dismissed" ? persisted?.dismissedAt ?? null : null,
87
+ lastAppliedAt: persisted?.lastAppliedAt ?? null,
88
+ appliedTaskIds,
89
+ openFollowUpTaskIds
90
+ };
91
+ }
92
+ function recommendationMatchesQuery(recommendation, query) {
93
+ const includeDismissed = query.includeDismissed ?? false;
94
+ if (!includeDismissed && recommendation.status === "dismissed") {
95
+ return false;
96
+ }
97
+ if (query.kind && query.kind !== "all" && recommendation.kind !== query.kind) {
98
+ return false;
99
+ }
100
+ if (query.targetAgent && query.targetAgent !== "all" && recommendation.targetAgent !== query.targetAgent) {
101
+ return false;
102
+ }
103
+ if (query.status && query.status !== "all" && recommendation.status !== query.status) {
104
+ return false;
105
+ }
106
+ return true;
107
+ }
108
+ function buildRecommendationDrafts(session) {
15
109
  const recommendations = [];
16
110
  const hotspots = buildClaimHotspots(session);
17
111
  const ownershipConflicts = findOwnershipRuleConflicts(session.config);
@@ -112,27 +206,119 @@ export function buildOperatorRecommendations(session) {
112
206
  deduped.set(recommendation.id, recommendation);
113
207
  }
114
208
  }
115
- const priority = (kind)=>{
116
- switch(kind){
117
- case "integration":
118
- return 0;
119
- case "handoff":
120
- return 1;
121
- case "ownership-config":
122
- return 2;
123
- default:
124
- return 3;
125
- }
126
- };
127
209
  return [
128
210
  ...deduped.values()
129
- ].sort((left, right)=>{
130
- const kindDelta = priority(left.kind) - priority(right.kind);
131
- if (kindDelta !== 0) {
132
- return kindDelta;
133
- }
134
- return left.title.localeCompare(right.title);
211
+ ];
212
+ }
213
+ export function buildOperatorRecommendations(session, query = {}) {
214
+ return buildRecommendationDrafts(session).map((draft)=>hydrateRecommendation(session, draft)).filter((recommendation)=>recommendationMatchesQuery(recommendation, query)).sort(recommendationToneSort);
215
+ }
216
+ export function findOperatorRecommendation(session, recommendationId) {
217
+ return buildOperatorRecommendations(session, {
218
+ includeDismissed: true
219
+ }).find((recommendation)=>recommendation.id === recommendationId) ?? null;
220
+ }
221
+ function upsertRecommendationState(session, nextState) {
222
+ session.recommendationStates = [
223
+ ...(session.recommendationStates ?? []).filter((state)=>state.id !== nextState.id),
224
+ nextState
225
+ ].sort((left, right)=>left.id.localeCompare(right.id));
226
+ return nextState;
227
+ }
228
+ export function dismissOperatorRecommendation(session, recommendationId, reason) {
229
+ const recommendation = findOperatorRecommendation(session, recommendationId);
230
+ if (!recommendation) {
231
+ throw new Error(`Recommendation ${recommendationId} was not found.`);
232
+ }
233
+ const timestamp = nowIso();
234
+ upsertRecommendationState(session, {
235
+ id: recommendation.id,
236
+ fingerprint: recommendation.fingerprint,
237
+ status: "dismissed",
238
+ dismissedReason: reason,
239
+ dismissedAt: timestamp,
240
+ lastAppliedAt: recommendation.lastAppliedAt,
241
+ appliedTaskIds: recommendation.appliedTaskIds,
242
+ updatedAt: timestamp
243
+ });
244
+ return findOperatorRecommendation(session, recommendationId) ?? recommendation;
245
+ }
246
+ export function restoreOperatorRecommendation(session, recommendationId) {
247
+ const recommendation = findOperatorRecommendation(session, recommendationId);
248
+ if (!recommendation) {
249
+ throw new Error(`Recommendation ${recommendationId} was not found.`);
250
+ }
251
+ const timestamp = nowIso();
252
+ upsertRecommendationState(session, {
253
+ id: recommendation.id,
254
+ fingerprint: recommendation.fingerprint,
255
+ status: "active",
256
+ dismissedReason: null,
257
+ dismissedAt: null,
258
+ lastAppliedAt: recommendation.lastAppliedAt,
259
+ appliedTaskIds: recommendation.appliedTaskIds,
260
+ updatedAt: timestamp
135
261
  });
262
+ return findOperatorRecommendation(session, recommendationId) ?? recommendation;
263
+ }
264
+ export function recordRecommendationApplied(session, recommendationId, taskId) {
265
+ const recommendation = findOperatorRecommendation(session, recommendationId);
266
+ if (!recommendation) {
267
+ throw new Error(`Recommendation ${recommendationId} was not found.`);
268
+ }
269
+ const timestamp = nowIso();
270
+ const appliedTaskIds = recommendation.appliedTaskIds.includes(taskId) ? recommendation.appliedTaskIds : [
271
+ ...recommendation.appliedTaskIds,
272
+ taskId
273
+ ];
274
+ upsertRecommendationState(session, {
275
+ id: recommendation.id,
276
+ fingerprint: recommendation.fingerprint,
277
+ status: "active",
278
+ dismissedReason: null,
279
+ dismissedAt: null,
280
+ lastAppliedAt: timestamp,
281
+ appliedTaskIds,
282
+ updatedAt: timestamp
283
+ });
284
+ return findOperatorRecommendation(session, recommendationId) ?? recommendation;
285
+ }
286
+ export function buildRecommendationActionPlan(session, recommendationId, options = {}) {
287
+ const recommendation = findOperatorRecommendation(session, recommendationId);
288
+ if (!recommendation) {
289
+ throw new Error(`Recommendation ${recommendationId} was not found.`);
290
+ }
291
+ if (recommendation.kind === "ownership-config") {
292
+ throw new Error(`Recommendation ${recommendation.id} is advisory only. Run "${recommendation.commandHint}" and update the config manually.`);
293
+ }
294
+ if (recommendation.openFollowUpTaskIds.length > 0 && !options.force) {
295
+ throw new Error(`Recommendation ${recommendation.id} already has open follow-up task(s): ${recommendation.openFollowUpTaskIds.join(", ")}. Re-run with --force to enqueue another task.`);
296
+ }
297
+ const owner = recommendation.targetAgent === "operator" || recommendation.targetAgent === null ? "codex" : recommendation.targetAgent;
298
+ const prompt = recommendation.kind === "integration" ? [
299
+ "Coordinate and resolve overlapping agent work before landing.",
300
+ recommendation.detail,
301
+ recommendation.filePath ? `Primary hotspot: ${recommendation.filePath}` : null
302
+ ].filter(Boolean).join("\n") : [
303
+ "Pick up ownership-aware handoff work from Kavi.",
304
+ recommendation.detail,
305
+ recommendation.filePath ? `Focus path: ${recommendation.filePath}` : null
306
+ ].filter(Boolean).join("\n");
307
+ return {
308
+ recommendation,
309
+ owner,
310
+ prompt,
311
+ routeReason: `Queued from Kavi recommendation ${recommendation.id}.`,
312
+ routeStrategy: "manual",
313
+ routeConfidence: 1,
314
+ claimedPaths: recommendation.filePath ? [
315
+ recommendation.filePath
316
+ ] : [],
317
+ routeMetadata: {
318
+ recommendationId: recommendation.id,
319
+ recommendationKind: recommendation.kind
320
+ }
321
+ };
136
322
  }
137
323
 
138
324
 
@@ -0,0 +1,118 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ensureDir, fileExists, readJson, writeJson } from "./fs.js";
4
+ function reportPath(paths, reportId) {
5
+ return path.join(paths.reportsDir, `${reportId}.json`);
6
+ }
7
+ function asStringArray(value) {
8
+ return Array.isArray(value) ? value.map((item)=>String(item)) : [];
9
+ }
10
+ function normalizeTaskResult(value) {
11
+ return {
12
+ taskId: String(value.taskId),
13
+ owner: value.owner === "codex" || value.owner === "claude" || value.owner === "router" ? value.owner : "router",
14
+ title: typeof value.title === "string" ? value.title : "",
15
+ summary: typeof value.summary === "string" ? value.summary : "",
16
+ claimedPaths: asStringArray(value.claimedPaths),
17
+ finishedAt: typeof value.finishedAt === "string" ? value.finishedAt : ""
18
+ };
19
+ }
20
+ function normalizeAgentChange(value) {
21
+ return {
22
+ agent: value.agent === "claude" ? "claude" : "codex",
23
+ paths: asStringArray(value.paths)
24
+ };
25
+ }
26
+ function normalizeSnapshotCommit(value) {
27
+ return {
28
+ agent: value.agent === "claude" ? "claude" : "codex",
29
+ commit: typeof value.commit === "string" ? value.commit : "",
30
+ createdCommit: value.createdCommit === true
31
+ };
32
+ }
33
+ function normalizeLandReport(report) {
34
+ return {
35
+ id: String(report.id),
36
+ sessionId: String(report.sessionId),
37
+ goal: typeof report.goal === "string" ? report.goal : null,
38
+ createdAt: typeof report.createdAt === "string" ? report.createdAt : "",
39
+ targetBranch: typeof report.targetBranch === "string" ? report.targetBranch : "",
40
+ integrationBranch: typeof report.integrationBranch === "string" ? report.integrationBranch : "",
41
+ integrationPath: typeof report.integrationPath === "string" ? report.integrationPath : "",
42
+ validationCommand: typeof report.validationCommand === "string" ? report.validationCommand : "",
43
+ validationStatus: report.validationStatus === "ran" || report.validationStatus === "skipped" || report.validationStatus === "not_configured" ? report.validationStatus : "not_configured",
44
+ validationDetail: typeof report.validationDetail === "string" ? report.validationDetail : "",
45
+ changedByAgent: Array.isArray(report.changedByAgent) ? report.changedByAgent.map((item)=>normalizeAgentChange(item)) : [],
46
+ completedTasks: Array.isArray(report.completedTasks) ? report.completedTasks.map((item)=>normalizeTaskResult(item)) : [],
47
+ snapshotCommits: Array.isArray(report.snapshotCommits) ? report.snapshotCommits.map((item)=>normalizeSnapshotCommit(item)) : [],
48
+ commandsRun: asStringArray(report.commandsRun),
49
+ reviewThreadsLanded: typeof report.reviewThreadsLanded === "number" ? report.reviewThreadsLanded : 0,
50
+ openReviewThreadsRemaining: typeof report.openReviewThreadsRemaining === "number" ? report.openReviewThreadsRemaining : 0,
51
+ summary: asStringArray(report.summary)
52
+ };
53
+ }
54
+ export function buildLandReport(params) {
55
+ const changedSummary = params.changedByAgent.map((changeSet)=>`${changeSet.agent}: ${changeSet.paths.length} path(s)`).join(" | ");
56
+ const validation = params.validationCommand.trim() ? params.validationStatus === "ran" ? `Validation ran with "${params.validationCommand.trim()}".` : params.validationStatus === "skipped" ? params.validationDetail : "No validation command was configured." : "No validation command was configured.";
57
+ return {
58
+ id: params.id,
59
+ sessionId: params.sessionId,
60
+ goal: params.goal,
61
+ createdAt: params.createdAt,
62
+ targetBranch: params.targetBranch,
63
+ integrationBranch: params.integrationBranch,
64
+ integrationPath: params.integrationPath,
65
+ validationCommand: params.validationCommand,
66
+ validationStatus: params.validationStatus,
67
+ validationDetail: params.validationDetail,
68
+ changedByAgent: params.changedByAgent.map((item)=>normalizeAgentChange(item)),
69
+ completedTasks: params.completedTasks.map((item)=>normalizeTaskResult(item)),
70
+ snapshotCommits: params.snapshotCommits.map((item)=>normalizeSnapshotCommit(item)),
71
+ commandsRun: [
72
+ ...params.commandsRun
73
+ ],
74
+ reviewThreadsLanded: params.reviewThreadsLanded,
75
+ openReviewThreadsRemaining: params.openReviewThreadsRemaining,
76
+ summary: [
77
+ `Merged managed work into ${params.targetBranch}.`,
78
+ changedSummary || "No worktree changes were recorded before landing.",
79
+ validation,
80
+ params.reviewThreadsLanded > 0 ? `${params.reviewThreadsLanded} review thread(s) were marked as landed.` : "No review threads were marked as landed."
81
+ ]
82
+ };
83
+ }
84
+ export async function saveLandReport(paths, report) {
85
+ await ensureDir(paths.reportsDir);
86
+ await writeJson(reportPath(paths, report.id), report);
87
+ }
88
+ export async function loadLandReport(paths, reportId) {
89
+ const filePath = reportPath(paths, reportId);
90
+ if (!await fileExists(filePath)) {
91
+ return null;
92
+ }
93
+ return normalizeLandReport(await readJson(filePath));
94
+ }
95
+ export async function listLandReports(paths) {
96
+ if (!await fileExists(paths.reportsDir)) {
97
+ return [];
98
+ }
99
+ const entries = await fs.readdir(paths.reportsDir, {
100
+ withFileTypes: true
101
+ });
102
+ const reports = [];
103
+ for (const entry of entries){
104
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
105
+ continue;
106
+ }
107
+ const report = await readJson(path.join(paths.reportsDir, entry.name));
108
+ reports.push(normalizeLandReport(report));
109
+ }
110
+ return reports.sort((left, right)=>right.createdAt.localeCompare(left.createdAt));
111
+ }
112
+ export async function loadLatestLandReport(paths) {
113
+ const reports = await listLandReports(paths);
114
+ return reports[0] ?? null;
115
+ }
116
+
117
+
118
+ //# sourceURL=reports.ts
package/dist/rpc.js CHANGED
@@ -99,7 +99,26 @@ export async function rpcEnqueueTask(paths, params) {
99
99
  routeMetadata: params.routeMetadata,
100
100
  claimedPaths: params.claimedPaths,
101
101
  routeStrategy: params.routeStrategy,
102
- routeConfidence: params.routeConfidence
102
+ routeConfidence: params.routeConfidence,
103
+ ...params.recommendationId ? {
104
+ recommendationId: params.recommendationId
105
+ } : {},
106
+ ...params.recommendationKind ? {
107
+ recommendationKind: params.recommendationKind
108
+ } : {}
109
+ });
110
+ }
111
+ export async function rpcDismissRecommendation(paths, params) {
112
+ await sendRpcRequest(paths, "dismissRecommendation", {
113
+ recommendationId: params.recommendationId,
114
+ ...params.reason ? {
115
+ reason: params.reason
116
+ } : {}
117
+ });
118
+ }
119
+ export async function rpcRestoreRecommendation(paths, params) {
120
+ await sendRpcRequest(paths, "restoreRecommendation", {
121
+ recommendationId: params.recommendationId
103
122
  });
104
123
  }
105
124
  export async function rpcResolveApproval(paths, params) {
package/dist/session.js CHANGED
@@ -36,6 +36,7 @@ export async function createSessionRecord(paths, config, runtime, sessionId, bas
36
36
  decisions: [],
37
37
  pathClaims: [],
38
38
  reviewNotes: [],
39
+ recommendationStates: [],
39
40
  agentStatus: {
40
41
  codex: initialAgentStatus("codex", "codex-app-server"),
41
42
  claude: initialAgentStatus("claude", "claude-print")
@@ -85,6 +86,16 @@ export async function loadSessionRecord(paths) {
85
86
  landedAt: typeof note.landedAt === "string" ? note.landedAt : null,
86
87
  followUpTaskIds: Array.isArray(note.followUpTaskIds) ? note.followUpTaskIds.map((item)=>String(item)) : []
87
88
  })) : [];
89
+ record.recommendationStates = Array.isArray(record.recommendationStates) ? record.recommendationStates.map((state)=>({
90
+ id: String(state.id),
91
+ fingerprint: typeof state.fingerprint === "string" ? state.fingerprint : "",
92
+ status: state.status === "dismissed" ? "dismissed" : "active",
93
+ dismissedReason: typeof state.dismissedReason === "string" ? state.dismissedReason : null,
94
+ dismissedAt: typeof state.dismissedAt === "string" ? state.dismissedAt : null,
95
+ lastAppliedAt: typeof state.lastAppliedAt === "string" ? state.lastAppliedAt : null,
96
+ appliedTaskIds: Array.isArray(state.appliedTaskIds) ? state.appliedTaskIds.map((item)=>String(item)) : [],
97
+ updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : nowIso()
98
+ })) : [];
88
99
  return record;
89
100
  }
90
101
  export async function saveSessionRecord(paths, record) {