@mandipadk7/kavi 0.1.4 → 0.1.6
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 +11 -3
- package/dist/adapters/shared.js +2 -0
- package/dist/daemon.js +97 -27
- package/dist/decision-ledger.js +148 -2
- package/dist/doctor.js +82 -0
- package/dist/main.js +182 -12
- package/dist/ownership.js +276 -0
- package/dist/reviews.js +24 -0
- package/dist/router.js +86 -46
- package/dist/rpc.js +1 -0
- package/dist/session.js +3 -0
- package/dist/task-artifacts.js +3 -0
- package/dist/tui.js +142 -29
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -6,16 +6,17 @@ import { createApprovalRequest, describeToolUse, findApprovalRule, listApprovalR
|
|
|
6
6
|
import { appendCommand } from "./command-queue.js";
|
|
7
7
|
import { ensureHomeConfig, ensureProjectScaffold, loadConfig } from "./config.js";
|
|
8
8
|
import { KaviDaemon } from "./daemon.js";
|
|
9
|
-
import { addDecisionRecord, upsertPathClaim } from "./decision-ledger.js";
|
|
9
|
+
import { addDecisionRecord, buildClaimHotspots, releasePathClaims, upsertPathClaim } from "./decision-ledger.js";
|
|
10
10
|
import { runDoctor } from "./doctor.js";
|
|
11
11
|
import { writeJson } from "./fs.js";
|
|
12
12
|
import { createGitignoreEntries, detectRepoRoot, ensureBootstrapCommit, ensureGitRepository, ensureWorktrees, findRepoRoot, findOverlappingWorktreePaths, landBranches, resolveTargetBranch } from "./git.js";
|
|
13
13
|
import { buildSessionId, resolveAppPaths } from "./paths.js";
|
|
14
14
|
import { isProcessAlive, spawnDetachedNode } from "./process.js";
|
|
15
15
|
import { pingRpc, readSnapshot, rpcEnqueueTask, rpcNotifyExternalUpdate, rpcKickoff, rpcRecentEvents, rpcResolveApproval, rpcShutdown, rpcTaskArtifact } from "./rpc.js";
|
|
16
|
-
import {
|
|
16
|
+
import { findOwnershipRuleConflicts } from "./ownership.js";
|
|
17
|
+
import { filterReviewNotes, markReviewNotesLandedForTasks } from "./reviews.js";
|
|
17
18
|
import { resolveSessionRuntime } from "./runtime.js";
|
|
18
|
-
import { buildAdHocTask, extractPromptPathHints, routeTask } from "./router.js";
|
|
19
|
+
import { buildAdHocTask, extractPromptPathHints, previewRouteDecision, routeTask } from "./router.js";
|
|
19
20
|
import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
|
|
20
21
|
import { listTaskArtifacts, loadTaskArtifact, saveTaskArtifact } from "./task-artifacts.js";
|
|
21
22
|
import { attachTui } from "./tui.js";
|
|
@@ -46,6 +47,13 @@ function getGoal(args) {
|
|
|
46
47
|
});
|
|
47
48
|
return filtered.length > 0 ? filtered.join(" ") : null;
|
|
48
49
|
}
|
|
50
|
+
function getOptionalFilter(args, name) {
|
|
51
|
+
const value = getFlag(args, name);
|
|
52
|
+
if (!value || value.startsWith("--")) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
49
57
|
async function readStdinText() {
|
|
50
58
|
if (process.stdin.isTTY) {
|
|
51
59
|
return "";
|
|
@@ -66,13 +74,15 @@ function renderUsage() {
|
|
|
66
74
|
" kavi open [--goal \"...\"]",
|
|
67
75
|
" kavi resume",
|
|
68
76
|
" kavi status [--json]",
|
|
77
|
+
" kavi route [--json] [--no-ai] <prompt>",
|
|
78
|
+
" kavi routes [--json] [--limit N]",
|
|
69
79
|
" kavi paths [--json]",
|
|
70
80
|
" kavi task [--agent codex|claude|auto] <prompt>",
|
|
71
81
|
" kavi tasks [--json]",
|
|
72
82
|
" kavi task-output <task-id|latest> [--json]",
|
|
73
83
|
" kavi decisions [--json] [--limit N]",
|
|
74
84
|
" kavi claims [--json] [--all]",
|
|
75
|
-
" kavi reviews [--json] [--all]",
|
|
85
|
+
" kavi reviews [--json] [--all] [--agent codex|claude] [--assignee codex|claude|operator|unassigned] [--status open|resolved] [--disposition approve|concern|question|note|accepted_risk|wont_fix]",
|
|
76
86
|
" kavi approvals [--json] [--all]",
|
|
77
87
|
" kavi approve <request-id|latest> [--remember]",
|
|
78
88
|
" kavi deny <request-id|latest> [--remember]",
|
|
@@ -284,6 +294,25 @@ async function notifyOperatorSurface(paths, reason) {
|
|
|
284
294
|
await rpcNotifyExternalUpdate(paths, reason);
|
|
285
295
|
} catch {}
|
|
286
296
|
}
|
|
297
|
+
function buildRouteAnalytics(tasks) {
|
|
298
|
+
const byOwner = {
|
|
299
|
+
codex: 0,
|
|
300
|
+
claude: 0
|
|
301
|
+
};
|
|
302
|
+
const byStrategy = {};
|
|
303
|
+
for (const task of tasks){
|
|
304
|
+
if (task.owner === "codex" || task.owner === "claude") {
|
|
305
|
+
byOwner[task.owner] += 1;
|
|
306
|
+
}
|
|
307
|
+
if (task.routeStrategy) {
|
|
308
|
+
byStrategy[task.routeStrategy] = (byStrategy[task.routeStrategy] ?? 0) + 1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
byOwner,
|
|
313
|
+
byStrategy
|
|
314
|
+
};
|
|
315
|
+
}
|
|
287
316
|
async function commandOpen(cwd, args) {
|
|
288
317
|
const goal = getGoal(args);
|
|
289
318
|
await startOrAttachSession(cwd, goal);
|
|
@@ -316,6 +345,9 @@ async function commandStatus(cwd, args) {
|
|
|
316
345
|
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
317
346
|
const pendingApprovals = rpcSnapshot?.approvals.filter((item)=>item.status === "pending") ?? await listApprovalRequests(paths);
|
|
318
347
|
const heartbeatAgeMs = sessionHeartbeatAgeMs(session);
|
|
348
|
+
const routeAnalytics = buildRouteAnalytics(session.tasks);
|
|
349
|
+
const ownershipConflicts = findOwnershipRuleConflicts(session.config);
|
|
350
|
+
const claimHotspots = buildClaimHotspots(session);
|
|
319
351
|
const payload = {
|
|
320
352
|
id: session.id,
|
|
321
353
|
status: session.status,
|
|
@@ -349,6 +381,13 @@ async function commandStatus(cwd, args) {
|
|
|
349
381
|
pathClaimCounts: {
|
|
350
382
|
active: session.pathClaims.filter((claim)=>claim.status === "active").length
|
|
351
383
|
},
|
|
384
|
+
routeCounts: routeAnalytics,
|
|
385
|
+
ownershipConflicts: ownershipConflicts.length,
|
|
386
|
+
claimHotspots: claimHotspots.length,
|
|
387
|
+
routingOwnership: {
|
|
388
|
+
codexPaths: session.config.routing.codexPaths,
|
|
389
|
+
claudePaths: session.config.routing.claudePaths
|
|
390
|
+
},
|
|
352
391
|
worktrees: session.worktrees
|
|
353
392
|
};
|
|
354
393
|
if (args.includes("--json")) {
|
|
@@ -368,10 +407,83 @@ async function commandStatus(cwd, args) {
|
|
|
368
407
|
console.log(`Reviews: open=${payload.reviewCounts.open} total=${payload.reviewCounts.total}`);
|
|
369
408
|
console.log(`Decisions: total=${payload.decisionCounts.total}`);
|
|
370
409
|
console.log(`Path claims: active=${payload.pathClaimCounts.active}`);
|
|
410
|
+
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(", ") || "-"}`);
|
|
411
|
+
console.log(`Ownership conflicts: ${payload.ownershipConflicts} | Claim hotspots: ${payload.claimHotspots}`);
|
|
412
|
+
console.log(`Routing ownership: codex=${payload.routingOwnership.codexPaths.join(", ") || "-"} | claude=${payload.routingOwnership.claudePaths.join(", ") || "-"}`);
|
|
371
413
|
for (const worktree of payload.worktrees){
|
|
372
414
|
console.log(`- ${worktree.agent}: ${worktree.path}`);
|
|
373
415
|
}
|
|
374
416
|
}
|
|
417
|
+
async function commandRoute(cwd, args) {
|
|
418
|
+
const prompt = getGoal(args);
|
|
419
|
+
if (!prompt) {
|
|
420
|
+
throw new Error("A route preview prompt is required. Example: kavi route \"Refactor src/ui/App.tsx\"");
|
|
421
|
+
}
|
|
422
|
+
const repoRoot = await findRepoRoot(cwd) ?? cwd;
|
|
423
|
+
const paths = resolveAppPaths(repoRoot);
|
|
424
|
+
const allowAi = !args.includes("--no-ai");
|
|
425
|
+
const hasSession = await sessionExists(paths);
|
|
426
|
+
const session = hasSession ? await loadSessionRecord(paths) : null;
|
|
427
|
+
const config = session?.config ?? await loadConfig(paths);
|
|
428
|
+
const routeDecision = allowAi && session ? await routeTask(prompt, session, paths) : previewRouteDecision(prompt, config, session);
|
|
429
|
+
const payload = {
|
|
430
|
+
prompt,
|
|
431
|
+
mode: allowAi && session ? "live" : "preview",
|
|
432
|
+
route: routeDecision
|
|
433
|
+
};
|
|
434
|
+
if (args.includes("--json")) {
|
|
435
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
console.log(`Mode: ${payload.mode}`);
|
|
439
|
+
console.log(`Owner: ${routeDecision.owner}`);
|
|
440
|
+
console.log(`Strategy: ${routeDecision.strategy}`);
|
|
441
|
+
console.log(`Confidence: ${routeDecision.confidence.toFixed(2)}`);
|
|
442
|
+
console.log(`Reason: ${routeDecision.reason}`);
|
|
443
|
+
console.log(`Claimed paths: ${routeDecision.claimedPaths.join(", ") || "-"}`);
|
|
444
|
+
if (Object.keys(routeDecision.metadata).length > 0) {
|
|
445
|
+
console.log(`Metadata: ${JSON.stringify(routeDecision.metadata)}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function commandRoutes(cwd, args) {
|
|
449
|
+
const { paths } = await requireSession(cwd);
|
|
450
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
451
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
452
|
+
const limitArg = getFlag(args, "--limit");
|
|
453
|
+
const limit = limitArg ? Number(limitArg) : 20;
|
|
454
|
+
const routes = [
|
|
455
|
+
...session.tasks
|
|
456
|
+
].filter((task)=>task.routeStrategy !== null).sort((left, right)=>right.updatedAt.localeCompare(left.updatedAt)).slice(0, Math.max(1, Number.isFinite(limit) ? limit : 20)).map((task)=>({
|
|
457
|
+
taskId: task.id,
|
|
458
|
+
title: task.title,
|
|
459
|
+
owner: task.owner,
|
|
460
|
+
status: task.status,
|
|
461
|
+
updatedAt: task.updatedAt,
|
|
462
|
+
routeStrategy: task.routeStrategy,
|
|
463
|
+
routeConfidence: task.routeConfidence,
|
|
464
|
+
routeReason: task.routeReason,
|
|
465
|
+
routeMetadata: task.routeMetadata,
|
|
466
|
+
claimedPaths: task.claimedPaths
|
|
467
|
+
}));
|
|
468
|
+
if (args.includes("--json")) {
|
|
469
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (routes.length === 0) {
|
|
473
|
+
console.log("No routed tasks recorded.");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
for (const route of routes){
|
|
477
|
+
console.log(`${route.taskId} | ${route.owner} | ${route.status} | ${route.routeStrategy ?? "-"}${route.routeConfidence === null ? "" : ` (${route.routeConfidence.toFixed(2)})`}`);
|
|
478
|
+
console.log(` title: ${route.title}`);
|
|
479
|
+
console.log(` updated: ${route.updatedAt}`);
|
|
480
|
+
console.log(` reason: ${route.routeReason ?? "-"}`);
|
|
481
|
+
console.log(` paths: ${route.claimedPaths.join(", ") || "-"}`);
|
|
482
|
+
if (Object.keys(route.routeMetadata).length > 0) {
|
|
483
|
+
console.log(` metadata: ${JSON.stringify(route.routeMetadata)}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
375
487
|
async function commandPaths(cwd, args) {
|
|
376
488
|
const repoRoot = await findRepoRoot(cwd) ?? cwd;
|
|
377
489
|
const paths = resolveAppPaths(repoRoot);
|
|
@@ -429,13 +541,18 @@ async function commandTask(cwd, args) {
|
|
|
429
541
|
strategy: "manual",
|
|
430
542
|
confidence: 1,
|
|
431
543
|
reason: `User explicitly assigned the task to ${requestedAgent}.`,
|
|
432
|
-
claimedPaths: extractPromptPathHints(prompt)
|
|
544
|
+
claimedPaths: extractPromptPathHints(prompt),
|
|
545
|
+
metadata: {
|
|
546
|
+
manualAssignment: true,
|
|
547
|
+
requestedAgent
|
|
548
|
+
}
|
|
433
549
|
} : await routeTask(prompt, session, paths);
|
|
434
550
|
if (rpcSnapshot) {
|
|
435
551
|
await rpcEnqueueTask(paths, {
|
|
436
552
|
owner: routeDecision.owner,
|
|
437
553
|
prompt,
|
|
438
554
|
routeReason: routeDecision.reason,
|
|
555
|
+
routeMetadata: routeDecision.metadata,
|
|
439
556
|
claimedPaths: routeDecision.claimedPaths,
|
|
440
557
|
routeStrategy: routeDecision.strategy,
|
|
441
558
|
routeConfidence: routeDecision.confidence
|
|
@@ -445,6 +562,7 @@ async function commandTask(cwd, args) {
|
|
|
445
562
|
owner: routeDecision.owner,
|
|
446
563
|
prompt,
|
|
447
564
|
routeReason: routeDecision.reason,
|
|
565
|
+
routeMetadata: routeDecision.metadata,
|
|
448
566
|
claimedPaths: routeDecision.claimedPaths,
|
|
449
567
|
routeStrategy: routeDecision.strategy,
|
|
450
568
|
routeConfidence: routeDecision.confidence
|
|
@@ -455,7 +573,8 @@ async function commandTask(cwd, args) {
|
|
|
455
573
|
prompt,
|
|
456
574
|
strategy: routeDecision.strategy,
|
|
457
575
|
confidence: routeDecision.confidence,
|
|
458
|
-
claimedPaths: routeDecision.claimedPaths
|
|
576
|
+
claimedPaths: routeDecision.claimedPaths,
|
|
577
|
+
routeMetadata: routeDecision.metadata
|
|
459
578
|
});
|
|
460
579
|
console.log(`Queued task for ${routeDecision.owner}: ${prompt}\nRoute: ${routeDecision.strategy} (${routeDecision.confidence.toFixed(2)}) ${routeDecision.reason}`);
|
|
461
580
|
}
|
|
@@ -477,6 +596,9 @@ async function commandTasks(cwd, args) {
|
|
|
477
596
|
updatedAt: task.updatedAt,
|
|
478
597
|
summary: task.summary,
|
|
479
598
|
routeReason: task.routeReason,
|
|
599
|
+
routeStrategy: task.routeStrategy,
|
|
600
|
+
routeConfidence: task.routeConfidence,
|
|
601
|
+
routeMetadata: task.routeMetadata,
|
|
480
602
|
claimedPaths: task.claimedPaths,
|
|
481
603
|
hasArtifact: artifactMap.has(task.id)
|
|
482
604
|
}));
|
|
@@ -488,8 +610,11 @@ async function commandTasks(cwd, args) {
|
|
|
488
610
|
console.log(`${task.id} | ${task.owner} | ${task.status} | artifact=${task.hasArtifact ? "yes" : "no"}`);
|
|
489
611
|
console.log(` title: ${task.title}`);
|
|
490
612
|
console.log(` updated: ${task.updatedAt}`);
|
|
491
|
-
console.log(` route: ${task.routeReason ?? "-"}`);
|
|
613
|
+
console.log(` route: ${task.routeStrategy ?? "-"}${task.routeConfidence === null ? "" : ` (${task.routeConfidence.toFixed(2)})`} ${task.routeReason ?? "-"}`);
|
|
492
614
|
console.log(` paths: ${task.claimedPaths.join(", ") || "-"}`);
|
|
615
|
+
if (Object.keys(task.routeMetadata).length > 0) {
|
|
616
|
+
console.log(` route-meta: ${JSON.stringify(task.routeMetadata)}`);
|
|
617
|
+
}
|
|
493
618
|
console.log(` summary: ${task.summary ?? "-"}`);
|
|
494
619
|
}
|
|
495
620
|
}
|
|
@@ -529,7 +654,8 @@ async function commandTaskOutput(cwd, args) {
|
|
|
529
654
|
console.log(`Started: ${artifact.startedAt}`);
|
|
530
655
|
console.log(`Finished: ${artifact.finishedAt}`);
|
|
531
656
|
console.log(`Summary: ${artifact.summary ?? "-"}`);
|
|
532
|
-
console.log(`Route: ${artifact.routeReason ?? "-"}`);
|
|
657
|
+
console.log(`Route: ${artifact.routeStrategy ?? "-"}${artifact.routeConfidence === null ? "" : ` (${artifact.routeConfidence.toFixed(2)})`} ${artifact.routeReason ?? "-"}`);
|
|
658
|
+
console.log(`Route Metadata: ${JSON.stringify(artifact.routeMetadata ?? {})}`);
|
|
533
659
|
console.log(`Claimed paths: ${artifact.claimedPaths.join(", ") || "-"}`);
|
|
534
660
|
console.log(`Error: ${artifact.error ?? "-"}`);
|
|
535
661
|
console.log("Decision Replay:");
|
|
@@ -577,6 +703,9 @@ async function commandDecisions(cwd, args) {
|
|
|
577
703
|
console.log(`${decision.createdAt} | ${decision.kind} | ${decision.agent ?? "-"} | ${decision.summary}`);
|
|
578
704
|
console.log(` task: ${decision.taskId ?? "-"}`);
|
|
579
705
|
console.log(` detail: ${decision.detail}`);
|
|
706
|
+
if (Object.keys(decision.metadata).length > 0) {
|
|
707
|
+
console.log(` metadata: ${JSON.stringify(decision.metadata)}`);
|
|
708
|
+
}
|
|
580
709
|
}
|
|
581
710
|
}
|
|
582
711
|
async function commandClaims(cwd, args) {
|
|
@@ -603,15 +732,19 @@ async function commandReviews(cwd, args) {
|
|
|
603
732
|
const { paths } = await requireSession(cwd);
|
|
604
733
|
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
605
734
|
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
735
|
+
const filters = {
|
|
736
|
+
agent: getOptionalFilter(args, "--agent"),
|
|
737
|
+
assignee: getOptionalFilter(args, "--assignee"),
|
|
738
|
+
disposition: getOptionalFilter(args, "--disposition"),
|
|
739
|
+
status: getOptionalFilter(args, "--status") ?? (args.includes("--all") ? null : "open")
|
|
740
|
+
};
|
|
741
|
+
const notes = filterReviewNotes(session.reviewNotes, filters);
|
|
609
742
|
if (args.includes("--json")) {
|
|
610
743
|
console.log(JSON.stringify(notes, null, 2));
|
|
611
744
|
return;
|
|
612
745
|
}
|
|
613
746
|
if (notes.length === 0) {
|
|
614
|
-
console.log("No review notes
|
|
747
|
+
console.log("No review notes matched the current filters.");
|
|
615
748
|
return;
|
|
616
749
|
}
|
|
617
750
|
for (const note of notes){
|
|
@@ -748,6 +881,12 @@ async function commandLand(cwd) {
|
|
|
748
881
|
].join("\n"), taskId, {
|
|
749
882
|
title: "Resolve integration overlap",
|
|
750
883
|
routeReason: "Created by kavi land because multiple agents changed the same paths.",
|
|
884
|
+
routeStrategy: "manual",
|
|
885
|
+
routeConfidence: 1,
|
|
886
|
+
routeMetadata: {
|
|
887
|
+
source: "land-overlap",
|
|
888
|
+
targetBranch
|
|
889
|
+
},
|
|
751
890
|
claimedPaths: overlappingPaths
|
|
752
891
|
}));
|
|
753
892
|
upsertPathClaim(session, {
|
|
@@ -788,6 +927,31 @@ async function commandLand(cwd) {
|
|
|
788
927
|
snapshotCommits: result.snapshotCommits,
|
|
789
928
|
commands: result.commandsRun
|
|
790
929
|
});
|
|
930
|
+
const releasedClaims = releasePathClaims(session, {
|
|
931
|
+
note: `Released after landing into ${targetBranch}.`
|
|
932
|
+
});
|
|
933
|
+
for (const claim of releasedClaims){
|
|
934
|
+
addDecisionRecord(session, {
|
|
935
|
+
kind: "integration",
|
|
936
|
+
agent: claim.agent,
|
|
937
|
+
taskId: claim.taskId,
|
|
938
|
+
summary: `Released path claim ${claim.id}`,
|
|
939
|
+
detail: claim.paths.join(", ") || "No claimed paths.",
|
|
940
|
+
metadata: {
|
|
941
|
+
claimId: claim.id,
|
|
942
|
+
targetBranch,
|
|
943
|
+
releaseReason: "land.completed"
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
await recordEvent(paths, session.id, "claim.released", {
|
|
947
|
+
claimId: claim.id,
|
|
948
|
+
taskId: claim.taskId,
|
|
949
|
+
agent: claim.agent,
|
|
950
|
+
paths: claim.paths,
|
|
951
|
+
reason: "land.completed",
|
|
952
|
+
targetBranch
|
|
953
|
+
});
|
|
954
|
+
}
|
|
791
955
|
const landedReviewNotes = markReviewNotesLandedForTasks(session, session.tasks.filter((task)=>task.status === "completed").map((task)=>task.id));
|
|
792
956
|
for (const note of landedReviewNotes){
|
|
793
957
|
addDecisionRecord(session, {
|
|
@@ -972,6 +1136,12 @@ async function main() {
|
|
|
972
1136
|
case "status":
|
|
973
1137
|
await commandStatus(cwd, args);
|
|
974
1138
|
break;
|
|
1139
|
+
case "route":
|
|
1140
|
+
await commandRoute(cwd, args);
|
|
1141
|
+
break;
|
|
1142
|
+
case "routes":
|
|
1143
|
+
await commandRoutes(cwd, args);
|
|
1144
|
+
break;
|
|
975
1145
|
case "paths":
|
|
976
1146
|
await commandPaths(cwd, args);
|
|
977
1147
|
break;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function normalizeOwnershipPattern(value) {
|
|
3
|
+
const trimmed = value.trim().replaceAll("\\", "/");
|
|
4
|
+
const withoutPrefix = trimmed.startsWith("./") ? trimmed.slice(2) : trimmed;
|
|
5
|
+
const normalized = path.posix.normalize(withoutPrefix);
|
|
6
|
+
return normalized === "." ? "" : normalized.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
7
|
+
}
|
|
8
|
+
export function globToRegex(pattern) {
|
|
9
|
+
const normalized = normalizeOwnershipPattern(pattern);
|
|
10
|
+
const escaped = normalized.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
11
|
+
const regexSource = escaped.replaceAll("**", "::double-star::").replaceAll("*", "[^/]*").replaceAll("::double-star::", ".*");
|
|
12
|
+
return new RegExp(`^${regexSource}$`);
|
|
13
|
+
}
|
|
14
|
+
export function matchesOwnershipPattern(filePath, pattern) {
|
|
15
|
+
const normalizedPath = normalizeOwnershipPattern(filePath);
|
|
16
|
+
const normalizedPattern = normalizeOwnershipPattern(pattern);
|
|
17
|
+
if (!normalizedPath || !normalizedPattern) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return globToRegex(normalizedPattern).test(normalizedPath);
|
|
21
|
+
}
|
|
22
|
+
function ownershipStaticPrefix(pattern) {
|
|
23
|
+
const normalized = normalizeOwnershipPattern(pattern);
|
|
24
|
+
const wildcardIndex = normalized.indexOf("*");
|
|
25
|
+
const prefix = wildcardIndex === -1 ? normalized : normalized.slice(0, wildcardIndex);
|
|
26
|
+
return prefix.replace(/\/+$/, "");
|
|
27
|
+
}
|
|
28
|
+
function wildcardCount(pattern) {
|
|
29
|
+
return (pattern.match(/\*/g) ?? []).length;
|
|
30
|
+
}
|
|
31
|
+
function literalLength(pattern) {
|
|
32
|
+
return pattern.replaceAll("*", "").length;
|
|
33
|
+
}
|
|
34
|
+
function segmentCount(pattern) {
|
|
35
|
+
return pattern.split("/").filter(Boolean).length;
|
|
36
|
+
}
|
|
37
|
+
function exactCoverage(pattern, matchedPaths) {
|
|
38
|
+
const normalizedPattern = normalizeOwnershipPattern(pattern);
|
|
39
|
+
if (normalizedPattern.includes("*")) {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
return matchedPaths.filter((filePath)=>normalizeOwnershipPattern(filePath) === normalizedPattern).length;
|
|
43
|
+
}
|
|
44
|
+
function buildOwnershipRuleCandidate(owner, pattern, declaredIndex, claimedPaths) {
|
|
45
|
+
const normalizedPattern = normalizeOwnershipPattern(pattern);
|
|
46
|
+
if (!normalizedPattern) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const matchedPaths = claimedPaths.filter((filePath)=>matchesOwnershipPattern(filePath, normalizedPattern));
|
|
50
|
+
if (matchedPaths.length === 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
owner,
|
|
55
|
+
pattern,
|
|
56
|
+
normalizedPattern,
|
|
57
|
+
declaredIndex,
|
|
58
|
+
matchedPaths,
|
|
59
|
+
coverage: matchedPaths.length,
|
|
60
|
+
exactCoverage: exactCoverage(normalizedPattern, matchedPaths),
|
|
61
|
+
staticPrefixLength: ownershipStaticPrefix(normalizedPattern).length,
|
|
62
|
+
literalLength: literalLength(normalizedPattern),
|
|
63
|
+
segmentCount: segmentCount(normalizedPattern),
|
|
64
|
+
wildcardCount: wildcardCount(normalizedPattern)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function compareRulePriority(left, right) {
|
|
68
|
+
const comparisons = [
|
|
69
|
+
[
|
|
70
|
+
left.coverage,
|
|
71
|
+
right.coverage,
|
|
72
|
+
false
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
left.exactCoverage,
|
|
76
|
+
right.exactCoverage,
|
|
77
|
+
false
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
left.staticPrefixLength,
|
|
81
|
+
right.staticPrefixLength,
|
|
82
|
+
false
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
left.literalLength,
|
|
86
|
+
right.literalLength,
|
|
87
|
+
false
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
left.segmentCount,
|
|
91
|
+
right.segmentCount,
|
|
92
|
+
false
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
left.wildcardCount,
|
|
96
|
+
right.wildcardCount,
|
|
97
|
+
true
|
|
98
|
+
]
|
|
99
|
+
];
|
|
100
|
+
for (const [leftValue, rightValue, preferLower] of comparisons){
|
|
101
|
+
if (leftValue === rightValue) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (preferLower) {
|
|
105
|
+
return leftValue < rightValue ? 1 : -1;
|
|
106
|
+
}
|
|
107
|
+
return leftValue > rightValue ? 1 : -1;
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
function compareRuleCandidates(left, right) {
|
|
112
|
+
const priority = compareRulePriority(left, right);
|
|
113
|
+
if (priority !== 0) {
|
|
114
|
+
return priority;
|
|
115
|
+
}
|
|
116
|
+
if (left.owner === right.owner && left.declaredIndex !== right.declaredIndex) {
|
|
117
|
+
return left.declaredIndex < right.declaredIndex ? 1 : -1;
|
|
118
|
+
}
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
function pathAncestorsOverlap(left, right) {
|
|
122
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
123
|
+
}
|
|
124
|
+
function patternsMayOverlap(leftPattern, rightPattern) {
|
|
125
|
+
const left = normalizeOwnershipPattern(leftPattern);
|
|
126
|
+
const right = normalizeOwnershipPattern(rightPattern);
|
|
127
|
+
if (!left || !right) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (left === right) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
const leftPrefix = ownershipStaticPrefix(left);
|
|
134
|
+
const rightPrefix = ownershipStaticPrefix(right);
|
|
135
|
+
if (!leftPrefix || !rightPrefix) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return pathAncestorsOverlap(leftPrefix, rightPrefix);
|
|
139
|
+
}
|
|
140
|
+
function summarizeCandidate(candidate) {
|
|
141
|
+
return {
|
|
142
|
+
owner: candidate.owner,
|
|
143
|
+
pattern: candidate.pattern,
|
|
144
|
+
declaredIndex: candidate.declaredIndex,
|
|
145
|
+
matchedPaths: candidate.matchedPaths,
|
|
146
|
+
coverage: candidate.coverage,
|
|
147
|
+
exactCoverage: candidate.exactCoverage,
|
|
148
|
+
staticPrefixLength: candidate.staticPrefixLength,
|
|
149
|
+
literalLength: candidate.literalLength,
|
|
150
|
+
segmentCount: candidate.segmentCount,
|
|
151
|
+
wildcardCount: candidate.wildcardCount
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function analyzeOwnershipRules(claimedPaths, config) {
|
|
155
|
+
const candidates = [
|
|
156
|
+
...config.routing.codexPaths.map((pattern, index)=>buildOwnershipRuleCandidate("codex", pattern, index, claimedPaths)).filter((candidate)=>candidate !== null),
|
|
157
|
+
...config.routing.claudePaths.map((pattern, index)=>buildOwnershipRuleCandidate("claude", pattern, index, claimedPaths)).filter((candidate)=>candidate !== null)
|
|
158
|
+
].sort((left, right)=>compareRuleCandidates(right, left));
|
|
159
|
+
const winningCandidate = candidates[0] ?? null;
|
|
160
|
+
if (!winningCandidate) {
|
|
161
|
+
return {
|
|
162
|
+
claimedPaths,
|
|
163
|
+
candidates,
|
|
164
|
+
winningCandidate: null,
|
|
165
|
+
ambiguousCandidates: []
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const ambiguousCandidates = candidates.filter((candidate)=>candidate.owner !== winningCandidate.owner && compareRulePriority(candidate, winningCandidate) === 0);
|
|
169
|
+
return {
|
|
170
|
+
claimedPaths,
|
|
171
|
+
candidates,
|
|
172
|
+
winningCandidate: ambiguousCandidates.length > 0 ? null : winningCandidate,
|
|
173
|
+
ambiguousCandidates
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function ownershipMetadataFromAnalysis(analysis) {
|
|
177
|
+
const metadata = {};
|
|
178
|
+
if (analysis.winningCandidate) {
|
|
179
|
+
metadata.winningRule = summarizeCandidate(analysis.winningCandidate);
|
|
180
|
+
metadata.matchedRules = analysis.candidates.slice(0, 4).map(summarizeCandidate);
|
|
181
|
+
}
|
|
182
|
+
if (analysis.ambiguousCandidates.length > 0 && analysis.candidates[0]) {
|
|
183
|
+
metadata.ownershipAmbiguity = {
|
|
184
|
+
claimedPaths: analysis.claimedPaths,
|
|
185
|
+
contenders: [
|
|
186
|
+
analysis.candidates[0],
|
|
187
|
+
...analysis.ambiguousCandidates
|
|
188
|
+
].map(summarizeCandidate)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return metadata;
|
|
192
|
+
}
|
|
193
|
+
export function buildOwnershipRouteDecision(claimedPaths, config) {
|
|
194
|
+
const analysis = analyzeOwnershipRules(claimedPaths, config);
|
|
195
|
+
if (!analysis.winningCandidate) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const candidate = analysis.winningCandidate;
|
|
199
|
+
return {
|
|
200
|
+
owner: candidate.owner,
|
|
201
|
+
strategy: "manual",
|
|
202
|
+
confidence: 0.97,
|
|
203
|
+
reason: `Matched explicit ${candidate.owner} ownership rule ${candidate.pattern} for: ${candidate.matchedPaths.join(", ")}.`,
|
|
204
|
+
claimedPaths,
|
|
205
|
+
metadata: {
|
|
206
|
+
ownershipSource: "config-routing-paths",
|
|
207
|
+
...ownershipMetadataFromAnalysis(analysis)
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export function findOwnershipRuleConflicts(config) {
|
|
212
|
+
const conflicts = [];
|
|
213
|
+
const leftRules = config.routing.codexPaths.map((pattern, index)=>({
|
|
214
|
+
owner: "codex",
|
|
215
|
+
pattern,
|
|
216
|
+
normalizedPattern: normalizeOwnershipPattern(pattern),
|
|
217
|
+
declaredIndex: index
|
|
218
|
+
}));
|
|
219
|
+
const rightRules = config.routing.claudePaths.map((pattern, index)=>({
|
|
220
|
+
owner: "claude",
|
|
221
|
+
pattern,
|
|
222
|
+
normalizedPattern: normalizeOwnershipPattern(pattern),
|
|
223
|
+
declaredIndex: index
|
|
224
|
+
}));
|
|
225
|
+
for (const left of leftRules){
|
|
226
|
+
if (!left.normalizedPattern) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
for (const right of rightRules){
|
|
230
|
+
if (!right.normalizedPattern || !patternsMayOverlap(left.pattern, right.pattern)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (left.normalizedPattern === right.normalizedPattern) {
|
|
234
|
+
conflicts.push({
|
|
235
|
+
leftOwner: left.owner,
|
|
236
|
+
leftPattern: left.pattern,
|
|
237
|
+
rightOwner: right.owner,
|
|
238
|
+
rightPattern: right.pattern,
|
|
239
|
+
kind: "exact",
|
|
240
|
+
detail: `Both agents claim the same ownership rule ${left.pattern}.`
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const syntheticPath = [
|
|
245
|
+
ownershipStaticPrefix(left.normalizedPattern),
|
|
246
|
+
ownershipStaticPrefix(right.normalizedPattern)
|
|
247
|
+
].filter(Boolean).sort((a, b)=>b.length - a.length)[0];
|
|
248
|
+
if (!syntheticPath) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const leftCandidate = buildOwnershipRuleCandidate(left.owner, left.pattern, left.declaredIndex, [
|
|
252
|
+
syntheticPath
|
|
253
|
+
]);
|
|
254
|
+
const rightCandidate = buildOwnershipRuleCandidate(right.owner, right.pattern, right.declaredIndex, [
|
|
255
|
+
syntheticPath
|
|
256
|
+
]);
|
|
257
|
+
if (!leftCandidate || !rightCandidate) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (compareRulePriority(leftCandidate, rightCandidate) === 0) {
|
|
261
|
+
conflicts.push({
|
|
262
|
+
leftOwner: left.owner,
|
|
263
|
+
leftPattern: left.pattern,
|
|
264
|
+
rightOwner: right.owner,
|
|
265
|
+
rightPattern: right.pattern,
|
|
266
|
+
kind: "ambiguous-overlap",
|
|
267
|
+
detail: `Ownership rules ${left.pattern} and ${right.pattern} can overlap without a specificity winner.`
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return conflicts;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
//# sourceURL=ownership.ts
|
package/dist/reviews.js
CHANGED
|
@@ -72,6 +72,30 @@ export function reviewNotesForPath(session, agent, filePath, hunkIndex) {
|
|
|
72
72
|
return note.hunkIndex === hunkIndex;
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
+
export function reviewNoteMatchesFilters(note, filters) {
|
|
76
|
+
if (filters.agent && note.agent !== filters.agent) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (filters.assignee) {
|
|
80
|
+
if (filters.assignee === "unassigned") {
|
|
81
|
+
if (note.assignee !== null) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
} else if (note.assignee !== filters.assignee) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (filters.disposition && note.disposition !== filters.disposition) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (filters.status && note.status !== filters.status) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
export function filterReviewNotes(notes, filters) {
|
|
97
|
+
return notes.filter((note)=>reviewNoteMatchesFilters(note, filters));
|
|
98
|
+
}
|
|
75
99
|
export function updateReviewNote(session, noteId, input) {
|
|
76
100
|
const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
|
|
77
101
|
if (!note) {
|