@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/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 { markReviewNotesLandedForTasks } from "./reviews.js";
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 notes = args.includes("--all") ? [
607
- ...session.reviewNotes
608
- ] : session.reviewNotes.filter((note)=>note.status === "open");
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 recorded.");
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) {