@mandipadk7/kavi 0.1.3 → 0.1.5

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,14 +6,14 @@ 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, 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 { filterReviewNotes, markReviewNotesLandedForTasks } from "./reviews.js";
17
17
  import { resolveSessionRuntime } from "./runtime.js";
18
18
  import { buildAdHocTask, extractPromptPathHints, routeTask } from "./router.js";
19
19
  import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
@@ -46,6 +46,13 @@ function getGoal(args) {
46
46
  });
47
47
  return filtered.length > 0 ? filtered.join(" ") : null;
48
48
  }
49
+ function getOptionalFilter(args, name) {
50
+ const value = getFlag(args, name);
51
+ if (!value || value.startsWith("--")) {
52
+ return null;
53
+ }
54
+ return value;
55
+ }
49
56
  async function readStdinText() {
50
57
  if (process.stdin.isTTY) {
51
58
  return "";
@@ -72,7 +79,7 @@ function renderUsage() {
72
79
  " kavi task-output <task-id|latest> [--json]",
73
80
  " kavi decisions [--json] [--limit N]",
74
81
  " kavi claims [--json] [--all]",
75
- " kavi reviews [--json] [--all]",
82
+ " 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
83
  " kavi approvals [--json] [--all]",
77
84
  " kavi approve <request-id|latest> [--remember]",
78
85
  " kavi deny <request-id|latest> [--remember]",
@@ -247,6 +254,7 @@ async function ensureStartupReady(repoRoot, paths) {
247
254
  "node",
248
255
  "codex",
249
256
  "claude",
257
+ "claude-auth",
250
258
  "git-worktree",
251
259
  "codex-app-server",
252
260
  "codex-auth-file"
@@ -348,6 +356,10 @@ async function commandStatus(cwd, args) {
348
356
  pathClaimCounts: {
349
357
  active: session.pathClaims.filter((claim)=>claim.status === "active").length
350
358
  },
359
+ routingOwnership: {
360
+ codexPaths: session.config.routing.codexPaths,
361
+ claudePaths: session.config.routing.claudePaths
362
+ },
351
363
  worktrees: session.worktrees
352
364
  };
353
365
  if (args.includes("--json")) {
@@ -367,6 +379,7 @@ async function commandStatus(cwd, args) {
367
379
  console.log(`Reviews: open=${payload.reviewCounts.open} total=${payload.reviewCounts.total}`);
368
380
  console.log(`Decisions: total=${payload.decisionCounts.total}`);
369
381
  console.log(`Path claims: active=${payload.pathClaimCounts.active}`);
382
+ console.log(`Routing ownership: codex=${payload.routingOwnership.codexPaths.join(", ") || "-"} | claude=${payload.routingOwnership.claudePaths.join(", ") || "-"}`);
370
383
  for (const worktree of payload.worktrees){
371
384
  console.log(`- ${worktree.agent}: ${worktree.path}`);
372
385
  }
@@ -428,13 +441,18 @@ async function commandTask(cwd, args) {
428
441
  strategy: "manual",
429
442
  confidence: 1,
430
443
  reason: `User explicitly assigned the task to ${requestedAgent}.`,
431
- claimedPaths: extractPromptPathHints(prompt)
444
+ claimedPaths: extractPromptPathHints(prompt),
445
+ metadata: {
446
+ manualAssignment: true,
447
+ requestedAgent
448
+ }
432
449
  } : await routeTask(prompt, session, paths);
433
450
  if (rpcSnapshot) {
434
451
  await rpcEnqueueTask(paths, {
435
452
  owner: routeDecision.owner,
436
453
  prompt,
437
454
  routeReason: routeDecision.reason,
455
+ routeMetadata: routeDecision.metadata,
438
456
  claimedPaths: routeDecision.claimedPaths,
439
457
  routeStrategy: routeDecision.strategy,
440
458
  routeConfidence: routeDecision.confidence
@@ -444,6 +462,7 @@ async function commandTask(cwd, args) {
444
462
  owner: routeDecision.owner,
445
463
  prompt,
446
464
  routeReason: routeDecision.reason,
465
+ routeMetadata: routeDecision.metadata,
447
466
  claimedPaths: routeDecision.claimedPaths,
448
467
  routeStrategy: routeDecision.strategy,
449
468
  routeConfidence: routeDecision.confidence
@@ -454,7 +473,8 @@ async function commandTask(cwd, args) {
454
473
  prompt,
455
474
  strategy: routeDecision.strategy,
456
475
  confidence: routeDecision.confidence,
457
- claimedPaths: routeDecision.claimedPaths
476
+ claimedPaths: routeDecision.claimedPaths,
477
+ routeMetadata: routeDecision.metadata
458
478
  });
459
479
  console.log(`Queued task for ${routeDecision.owner}: ${prompt}\nRoute: ${routeDecision.strategy} (${routeDecision.confidence.toFixed(2)}) ${routeDecision.reason}`);
460
480
  }
@@ -476,6 +496,9 @@ async function commandTasks(cwd, args) {
476
496
  updatedAt: task.updatedAt,
477
497
  summary: task.summary,
478
498
  routeReason: task.routeReason,
499
+ routeStrategy: task.routeStrategy,
500
+ routeConfidence: task.routeConfidence,
501
+ routeMetadata: task.routeMetadata,
479
502
  claimedPaths: task.claimedPaths,
480
503
  hasArtifact: artifactMap.has(task.id)
481
504
  }));
@@ -487,8 +510,11 @@ async function commandTasks(cwd, args) {
487
510
  console.log(`${task.id} | ${task.owner} | ${task.status} | artifact=${task.hasArtifact ? "yes" : "no"}`);
488
511
  console.log(` title: ${task.title}`);
489
512
  console.log(` updated: ${task.updatedAt}`);
490
- console.log(` route: ${task.routeReason ?? "-"}`);
513
+ console.log(` route: ${task.routeStrategy ?? "-"}${task.routeConfidence === null ? "" : ` (${task.routeConfidence.toFixed(2)})`} ${task.routeReason ?? "-"}`);
491
514
  console.log(` paths: ${task.claimedPaths.join(", ") || "-"}`);
515
+ if (Object.keys(task.routeMetadata).length > 0) {
516
+ console.log(` route-meta: ${JSON.stringify(task.routeMetadata)}`);
517
+ }
492
518
  console.log(` summary: ${task.summary ?? "-"}`);
493
519
  }
494
520
  }
@@ -528,7 +554,8 @@ async function commandTaskOutput(cwd, args) {
528
554
  console.log(`Started: ${artifact.startedAt}`);
529
555
  console.log(`Finished: ${artifact.finishedAt}`);
530
556
  console.log(`Summary: ${artifact.summary ?? "-"}`);
531
- console.log(`Route: ${artifact.routeReason ?? "-"}`);
557
+ console.log(`Route: ${artifact.routeStrategy ?? "-"}${artifact.routeConfidence === null ? "" : ` (${artifact.routeConfidence.toFixed(2)})`} ${artifact.routeReason ?? "-"}`);
558
+ console.log(`Route Metadata: ${JSON.stringify(artifact.routeMetadata ?? {})}`);
532
559
  console.log(`Claimed paths: ${artifact.claimedPaths.join(", ") || "-"}`);
533
560
  console.log(`Error: ${artifact.error ?? "-"}`);
534
561
  console.log("Decision Replay:");
@@ -543,6 +570,7 @@ async function commandTaskOutput(cwd, args) {
543
570
  } else {
544
571
  for (const note of artifact.reviewNotes){
545
572
  console.log(`${note.createdAt} | ${note.disposition} | ${note.status} | ${note.filePath}${note.hunkIndex === null ? "" : ` | hunk ${note.hunkIndex + 1}`}`);
573
+ console.log(` assignee: ${note.assignee ?? "-"}`);
546
574
  console.log(` comments: ${note.comments.length}`);
547
575
  for (const [index, comment] of note.comments.entries()){
548
576
  console.log(` ${index === 0 ? "root" : `reply-${index}`}: ${comment.body}`);
@@ -575,6 +603,9 @@ async function commandDecisions(cwd, args) {
575
603
  console.log(`${decision.createdAt} | ${decision.kind} | ${decision.agent ?? "-"} | ${decision.summary}`);
576
604
  console.log(` task: ${decision.taskId ?? "-"}`);
577
605
  console.log(` detail: ${decision.detail}`);
606
+ if (Object.keys(decision.metadata).length > 0) {
607
+ console.log(` metadata: ${JSON.stringify(decision.metadata)}`);
608
+ }
578
609
  }
579
610
  }
580
611
  async function commandClaims(cwd, args) {
@@ -601,20 +632,25 @@ async function commandReviews(cwd, args) {
601
632
  const { paths } = await requireSession(cwd);
602
633
  const rpcSnapshot = await tryRpcSnapshot(paths);
603
634
  const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
604
- const notes = args.includes("--all") ? [
605
- ...session.reviewNotes
606
- ] : session.reviewNotes.filter((note)=>note.status === "open");
635
+ const filters = {
636
+ agent: getOptionalFilter(args, "--agent"),
637
+ assignee: getOptionalFilter(args, "--assignee"),
638
+ disposition: getOptionalFilter(args, "--disposition"),
639
+ status: getOptionalFilter(args, "--status") ?? (args.includes("--all") ? null : "open")
640
+ };
641
+ const notes = filterReviewNotes(session.reviewNotes, filters);
607
642
  if (args.includes("--json")) {
608
643
  console.log(JSON.stringify(notes, null, 2));
609
644
  return;
610
645
  }
611
646
  if (notes.length === 0) {
612
- console.log("No review notes recorded.");
647
+ console.log("No review notes matched the current filters.");
613
648
  return;
614
649
  }
615
650
  for (const note of notes){
616
651
  console.log(`${note.id} | ${note.agent} | ${note.status} | ${note.disposition} | ${note.filePath}${note.hunkIndex === null ? "" : ` | hunk ${note.hunkIndex + 1}`}`);
617
652
  console.log(` task: ${note.taskId ?? "-"}`);
653
+ console.log(` assignee: ${note.assignee ?? "-"}`);
618
654
  console.log(` updated: ${note.updatedAt}`);
619
655
  console.log(` comments: ${note.comments.length}`);
620
656
  console.log(` landed: ${note.landedAt ?? "-"}`);
@@ -745,6 +781,12 @@ async function commandLand(cwd) {
745
781
  ].join("\n"), taskId, {
746
782
  title: "Resolve integration overlap",
747
783
  routeReason: "Created by kavi land because multiple agents changed the same paths.",
784
+ routeStrategy: "manual",
785
+ routeConfidence: 1,
786
+ routeMetadata: {
787
+ source: "land-overlap",
788
+ targetBranch
789
+ },
748
790
  claimedPaths: overlappingPaths
749
791
  }));
750
792
  upsertPathClaim(session, {
@@ -785,6 +827,23 @@ async function commandLand(cwd) {
785
827
  snapshotCommits: result.snapshotCommits,
786
828
  commands: result.commandsRun
787
829
  });
830
+ const releasedClaims = releasePathClaims(session, {
831
+ note: `Released after landing into ${targetBranch}.`
832
+ });
833
+ for (const claim of releasedClaims){
834
+ addDecisionRecord(session, {
835
+ kind: "integration",
836
+ agent: claim.agent,
837
+ taskId: claim.taskId,
838
+ summary: `Released path claim ${claim.id}`,
839
+ detail: claim.paths.join(", ") || "No claimed paths.",
840
+ metadata: {
841
+ claimId: claim.id,
842
+ targetBranch,
843
+ releaseReason: "land.completed"
844
+ }
845
+ });
846
+ }
788
847
  const landedReviewNotes = markReviewNotesLandedForTasks(session, session.tasks.filter((task)=>task.status === "completed").map((task)=>task.id));
789
848
  for (const note of landedReviewNotes){
790
849
  addDecisionRecord(session, {
package/dist/reviews.js CHANGED
@@ -5,11 +5,21 @@ function trimBody(value) {
5
5
  return value.trim();
6
6
  }
7
7
  function summarizeReviewNote(disposition, filePath, hunkHeader, body) {
8
- const label = disposition[0].toUpperCase() + disposition.slice(1);
8
+ const label = reviewDispositionSummaryLabel(disposition);
9
9
  const scope = hunkHeader ? `${filePath} ${hunkHeader}` : filePath;
10
10
  const firstLine = trimBody(body).split("\n")[0]?.trim() ?? "";
11
11
  return firstLine ? `${label} ${scope}: ${firstLine}` : `${label} ${scope}`;
12
12
  }
13
+ function reviewDispositionSummaryLabel(disposition) {
14
+ switch(disposition){
15
+ case "accepted_risk":
16
+ return "Accepted Risk";
17
+ case "wont_fix":
18
+ return "Won't Fix";
19
+ default:
20
+ return disposition[0].toUpperCase() + disposition.slice(1);
21
+ }
22
+ }
13
23
  function createReviewComment(body) {
14
24
  const timestamp = nowIso();
15
25
  return {
@@ -24,6 +34,7 @@ export function addReviewNote(session, input) {
24
34
  const note = {
25
35
  id: randomUUID(),
26
36
  agent: input.agent,
37
+ assignee: input.assignee ?? input.agent,
27
38
  taskId: input.taskId ?? null,
28
39
  filePath: input.filePath,
29
40
  hunkIndex: input.hunkIndex ?? null,
@@ -61,6 +72,30 @@ export function reviewNotesForPath(session, agent, filePath, hunkIndex) {
61
72
  return note.hunkIndex === hunkIndex;
62
73
  });
63
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
+ }
64
99
  export function updateReviewNote(session, noteId, input) {
65
100
  const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
66
101
  if (!note) {
@@ -68,8 +103,10 @@ export function updateReviewNote(session, noteId, input) {
68
103
  }
69
104
  const nextBody = typeof input.body === "string" ? trimBody(input.body) : note.body;
70
105
  const nextDisposition = input.disposition ?? note.disposition;
106
+ const nextAssignee = input.assignee === undefined ? note.assignee : input.assignee;
71
107
  note.body = nextBody;
72
108
  note.disposition = nextDisposition;
109
+ note.assignee = nextAssignee;
73
110
  if (note.comments.length === 0) {
74
111
  note.comments.push(createReviewComment(nextBody));
75
112
  } else if (typeof input.body === "string") {
@@ -96,7 +133,7 @@ export function setReviewNoteStatus(session, noteId, status) {
96
133
  note.updatedAt = nowIso();
97
134
  return note;
98
135
  }
99
- export function linkReviewFollowUpTask(session, noteId, taskId) {
136
+ export function linkReviewFollowUpTask(session, noteId, taskId, assignee) {
100
137
  const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
101
138
  if (!note) {
102
139
  return null;
@@ -107,6 +144,9 @@ export function linkReviewFollowUpTask(session, noteId, taskId) {
107
144
  taskId
108
145
  ])
109
146
  ];
147
+ if (assignee !== undefined) {
148
+ note.assignee = assignee;
149
+ }
110
150
  note.updatedAt = nowIso();
111
151
  return note;
112
152
  }
@@ -122,6 +162,19 @@ export function addReviewReply(session, noteId, body) {
122
162
  note.updatedAt = nowIso();
123
163
  return note;
124
164
  }
165
+ export function cycleReviewAssignee(current, noteAgent) {
166
+ const sequence = [
167
+ noteAgent,
168
+ noteAgent === "codex" ? "claude" : "codex",
169
+ "operator",
170
+ null
171
+ ];
172
+ const index = sequence.findIndex((item)=>item === current);
173
+ if (index === -1) {
174
+ return noteAgent;
175
+ }
176
+ return sequence[(index + 1) % sequence.length] ?? null;
177
+ }
125
178
  export function autoResolveReviewNotesForCompletedTask(session, taskId) {
126
179
  const resolved = [];
127
180
  for (const note of session.reviewNotes){
package/dist/router.js CHANGED
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { CodexAppServerClient } from "./codex-app-server.js";
2
3
  import { findClaimConflicts } from "./decision-ledger.js";
3
4
  import { nowIso } from "./paths.js";
@@ -43,6 +44,65 @@ function normalizeClaimedPaths(paths) {
43
44
  ...new Set(paths.map((item)=>item.trim()).filter(Boolean))
44
45
  ].sort();
45
46
  }
47
+ function buildRouteMetadata(input = {}) {
48
+ return input;
49
+ }
50
+ function normalizePathPattern(value) {
51
+ const trimmed = value.trim().replaceAll("\\", "/");
52
+ const withoutPrefix = trimmed.startsWith("./") ? trimmed.slice(2) : trimmed;
53
+ const normalized = path.posix.normalize(withoutPrefix);
54
+ return normalized === "." ? "" : normalized.replace(/^\/+/, "");
55
+ }
56
+ function globToRegex(pattern) {
57
+ const normalized = normalizePathPattern(pattern);
58
+ const escaped = normalized.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
59
+ const regexSource = escaped.replaceAll("**", "::double-star::").replaceAll("*", "[^/]*").replaceAll("::double-star::", ".*");
60
+ return new RegExp(`^${regexSource}$`);
61
+ }
62
+ function matchesPattern(filePath, pattern) {
63
+ const normalizedPath = normalizePathPattern(filePath);
64
+ const normalizedPattern = normalizePathPattern(pattern);
65
+ if (!normalizedPath || !normalizedPattern) {
66
+ return false;
67
+ }
68
+ return globToRegex(normalizedPattern).test(normalizedPath);
69
+ }
70
+ function countPathMatches(filePaths, patterns) {
71
+ if (filePaths.length === 0 || patterns.length === 0) {
72
+ return 0;
73
+ }
74
+ return filePaths.filter((filePath)=>patterns.some((pattern)=>matchesPattern(filePath, pattern))).length;
75
+ }
76
+ function buildPathOwnershipDecision(prompt, config) {
77
+ const claimedPaths = extractPromptPathHints(prompt);
78
+ if (claimedPaths.length === 0) {
79
+ return null;
80
+ }
81
+ const codexMatches = countPathMatches(claimedPaths, config.routing.codexPaths);
82
+ const claudeMatches = countPathMatches(claimedPaths, config.routing.claudePaths);
83
+ if (codexMatches === 0 && claudeMatches === 0) {
84
+ return null;
85
+ }
86
+ if (codexMatches === claudeMatches) {
87
+ return null;
88
+ }
89
+ const owner = codexMatches > claudeMatches ? "codex" : "claude";
90
+ const ownerPatterns = owner === "codex" ? config.routing.codexPaths : config.routing.claudePaths;
91
+ const matchedPatterns = ownerPatterns.filter((pattern)=>claimedPaths.some((filePath)=>matchesPattern(filePath, pattern)));
92
+ return {
93
+ owner,
94
+ strategy: "manual",
95
+ confidence: 0.97,
96
+ reason: `Matched explicit ${owner} path ownership rules for: ${claimedPaths.join(", ")}.`,
97
+ claimedPaths,
98
+ metadata: buildRouteMetadata({
99
+ ownershipSource: "config-routing-paths",
100
+ matchedPatterns,
101
+ codexMatches,
102
+ claudeMatches
103
+ })
104
+ };
105
+ }
46
106
  export function extractPromptPathHints(prompt) {
47
107
  const candidates = [];
48
108
  const quotedMatches = prompt.matchAll(/[`'"]([^`'"\n]+)[`'"]/g);
@@ -59,6 +119,10 @@ export function extractPromptPathHints(prompt) {
59
119
  return normalizeClaimedPaths(candidates);
60
120
  }
61
121
  export function routePrompt(prompt, config) {
122
+ const pathDecision = buildPathOwnershipDecision(prompt, config);
123
+ if (pathDecision) {
124
+ return pathDecision.owner;
125
+ }
62
126
  if (containsKeyword(prompt, config.routing.frontendKeywords)) {
63
127
  return "claude";
64
128
  }
@@ -77,7 +141,11 @@ function buildKeywordDecision(prompt, config) {
77
141
  strategy: "keyword",
78
142
  confidence: 0.92,
79
143
  reason: "Matched frontend and UX routing keywords.",
80
- claimedPaths
144
+ claimedPaths,
145
+ metadata: buildRouteMetadata({
146
+ matchedKeywordSet: "frontend",
147
+ promptHints: claimedPaths
148
+ })
81
149
  };
82
150
  }
83
151
  if (backend && !frontend) {
@@ -86,12 +154,21 @@ function buildKeywordDecision(prompt, config) {
86
154
  strategy: "keyword",
87
155
  confidence: 0.92,
88
156
  reason: "Matched backend and architecture routing keywords.",
89
- claimedPaths
157
+ claimedPaths,
158
+ metadata: buildRouteMetadata({
159
+ matchedKeywordSet: "backend",
160
+ promptHints: claimedPaths
161
+ })
90
162
  };
91
163
  }
92
164
  return null;
93
165
  }
94
166
  function buildRouterPrompt(prompt, session) {
167
+ const ownershipRules = [
168
+ ...session.config.routing.codexPaths.map((pattern)=>`- codex: ${pattern}`),
169
+ ...session.config.routing.claudePaths.map((pattern)=>`- claude: ${pattern}`)
170
+ ].join("\n");
171
+ const promptHints = extractPromptPathHints(prompt);
95
172
  const activeClaims = session.pathClaims.filter((claim)=>claim.status === "active").map((claim)=>`- ${claim.agent}: ${claim.paths.join(", ")}`).join("\n");
96
173
  return [
97
174
  "Route this task between Codex and Claude.",
@@ -103,6 +180,12 @@ function buildRouterPrompt(prompt, session) {
103
180
  "Task prompt:",
104
181
  prompt,
105
182
  "",
183
+ "Explicit path ownership rules:",
184
+ ownershipRules || "- none",
185
+ "",
186
+ "Path hints extracted from the prompt:",
187
+ promptHints.length > 0 ? promptHints.map((item)=>`- ${item}`).join("\n") : "- none",
188
+ "",
106
189
  "Active path claims:",
107
190
  activeClaims || "- none"
108
191
  ].join("\n");
@@ -149,7 +232,11 @@ async function routeWithCodexAi(prompt, session) {
149
232
  strategy: "ai",
150
233
  confidence,
151
234
  reason,
152
- claimedPaths
235
+ claimedPaths,
236
+ metadata: buildRouteMetadata({
237
+ router: "codex-ai",
238
+ promptHints: extractPromptPathHints(prompt)
239
+ })
153
240
  };
154
241
  } finally{
155
242
  await client.close();
@@ -170,10 +257,25 @@ function applyClaimRouting(session, decision) {
170
257
  strategy: "path-claim",
171
258
  confidence: 1,
172
259
  reason: `Re-routed to ${owner} because active path claims overlap: ${overlappingPaths.join(", ")}`,
173
- claimedPaths: decision.claimedPaths.length > 0 ? decision.claimedPaths : overlappingPaths
260
+ claimedPaths: decision.claimedPaths.length > 0 ? decision.claimedPaths : overlappingPaths,
261
+ metadata: buildRouteMetadata({
262
+ ...decision.metadata,
263
+ reroutedFrom: decision.owner,
264
+ conflictingClaims: conflicts.map((claim)=>({
265
+ taskId: claim.taskId,
266
+ agent: claim.agent,
267
+ source: claim.source,
268
+ paths: claim.paths
269
+ })),
270
+ overlappingPaths
271
+ })
174
272
  };
175
273
  }
176
274
  export async function routeTask(prompt, session, _paths) {
275
+ const pathDecision = buildPathOwnershipDecision(prompt, session.config);
276
+ if (pathDecision) {
277
+ return applyClaimRouting(session, pathDecision);
278
+ }
177
279
  const heuristic = buildKeywordDecision(prompt, session.config);
178
280
  if (heuristic) {
179
281
  return applyClaimRouting(session, heuristic);
@@ -187,7 +289,11 @@ export async function routeTask(prompt, session, _paths) {
187
289
  strategy: "fallback",
188
290
  confidence: 0.4,
189
291
  reason: error instanceof Error ? `AI routing failed, defaulted to Codex: ${error.message}` : "AI routing failed, defaulted to Codex.",
190
- claimedPaths: extractPromptPathHints(prompt)
292
+ claimedPaths: extractPromptPathHints(prompt),
293
+ metadata: buildRouteMetadata({
294
+ router: "fallback",
295
+ error: error instanceof Error ? error.message : String(error)
296
+ })
191
297
  });
192
298
  }
193
299
  }
@@ -204,6 +310,12 @@ export function buildKickoffTasks(goal) {
204
310
  updatedAt: timestamp,
205
311
  summary: null,
206
312
  routeReason: "Kickoff task reserved for Codex planning.",
313
+ routeStrategy: "manual",
314
+ routeConfidence: 1,
315
+ routeMetadata: buildRouteMetadata({
316
+ kickoff: true,
317
+ reservedFor: "codex"
318
+ }),
207
319
  claimedPaths: []
208
320
  },
209
321
  {
@@ -216,6 +328,12 @@ export function buildKickoffTasks(goal) {
216
328
  updatedAt: timestamp,
217
329
  summary: null,
218
330
  routeReason: "Kickoff task reserved for Claude intent and UX interpretation.",
331
+ routeStrategy: "manual",
332
+ routeConfidence: 1,
333
+ routeMetadata: buildRouteMetadata({
334
+ kickoff: true,
335
+ reservedFor: "claude"
336
+ }),
219
337
  claimedPaths: []
220
338
  }
221
339
  ];
@@ -232,6 +350,9 @@ export function buildAdHocTask(owner, prompt, taskId, options = {}) {
232
350
  updatedAt: timestamp,
233
351
  summary: null,
234
352
  routeReason: options.routeReason ?? null,
353
+ routeStrategy: options.routeStrategy ?? null,
354
+ routeConfidence: typeof options.routeConfidence === "number" && Number.isFinite(options.routeConfidence) ? options.routeConfidence : null,
355
+ routeMetadata: options.routeMetadata ?? {},
235
356
  claimedPaths: normalizeClaimedPaths(options.claimedPaths ?? [])
236
357
  };
237
358
  }
package/dist/rpc.js CHANGED
@@ -96,6 +96,7 @@ export async function rpcEnqueueTask(paths, params) {
96
96
  owner: params.owner,
97
97
  prompt: params.prompt,
98
98
  routeReason: params.routeReason,
99
+ routeMetadata: params.routeMetadata,
99
100
  claimedPaths: params.claimedPaths,
100
101
  routeStrategy: params.routeStrategy,
101
102
  routeConfidence: params.routeConfidence
@@ -130,13 +131,22 @@ export async function rpcAddReviewNote(paths, params) {
130
131
  hunkIndex: params.hunkIndex,
131
132
  hunkHeader: params.hunkHeader,
132
133
  disposition: params.disposition,
134
+ assignee: params.assignee ?? null,
133
135
  body: params.body
134
136
  });
135
137
  }
136
138
  export async function rpcUpdateReviewNote(paths, params) {
137
139
  await sendRpcRequest(paths, "updateReviewNote", {
138
140
  noteId: params.noteId,
139
- body: params.body
141
+ ...typeof params.body === "string" ? {
142
+ body: params.body
143
+ } : {},
144
+ ...params.disposition ? {
145
+ disposition: params.disposition
146
+ } : {},
147
+ ...params.assignee === undefined ? {} : {
148
+ assignee: params.assignee
149
+ }
140
150
  });
141
151
  }
142
152
  export async function rpcAddReviewReply(paths, params) {
package/dist/session.js CHANGED
@@ -52,6 +52,9 @@ export async function loadSessionRecord(paths) {
52
52
  record.tasks = Array.isArray(record.tasks) ? record.tasks.map((task)=>({
53
53
  ...task,
54
54
  routeReason: typeof task.routeReason === "string" ? task.routeReason : null,
55
+ routeStrategy: task.routeStrategy === "manual" || task.routeStrategy === "keyword" || task.routeStrategy === "ai" || task.routeStrategy === "path-claim" || task.routeStrategy === "fallback" ? task.routeStrategy : null,
56
+ routeConfidence: typeof task.routeConfidence === "number" && Number.isFinite(task.routeConfidence) ? task.routeConfidence : null,
57
+ routeMetadata: task.routeMetadata && typeof task.routeMetadata === "object" && !Array.isArray(task.routeMetadata) ? task.routeMetadata : {},
55
58
  claimedPaths: Array.isArray(task.claimedPaths) ? task.claimedPaths.map((item)=>String(item)) : []
56
59
  })) : [];
57
60
  record.decisions = Array.isArray(record.decisions) ? record.decisions : [];
@@ -59,9 +62,11 @@ export async function loadSessionRecord(paths) {
59
62
  record.reviewNotes = Array.isArray(record.reviewNotes) ? record.reviewNotes.map((note)=>({
60
63
  ...note,
61
64
  body: typeof note.body === "string" ? note.body : "",
65
+ assignee: note.assignee === "codex" || note.assignee === "claude" || note.assignee === "operator" ? note.assignee : null,
62
66
  taskId: typeof note.taskId === "string" ? note.taskId : null,
63
67
  hunkIndex: typeof note.hunkIndex === "number" ? note.hunkIndex : null,
64
68
  hunkHeader: typeof note.hunkHeader === "string" ? note.hunkHeader : null,
69
+ disposition: note.disposition === "approve" || note.disposition === "concern" || note.disposition === "question" || note.disposition === "accepted_risk" || note.disposition === "wont_fix" ? note.disposition : "note",
65
70
  status: note.status === "resolved" ? "resolved" : "open",
66
71
  comments: Array.isArray(note.comments) ? note.comments.map((comment)=>({
67
72
  id: String(comment.id),
@@ -8,13 +8,18 @@ function normalizeArtifact(artifact) {
8
8
  return {
9
9
  ...artifact,
10
10
  routeReason: typeof artifact.routeReason === "string" ? artifact.routeReason : null,
11
+ routeStrategy: artifact.routeStrategy === "manual" || artifact.routeStrategy === "keyword" || artifact.routeStrategy === "ai" || artifact.routeStrategy === "path-claim" || artifact.routeStrategy === "fallback" ? artifact.routeStrategy : null,
12
+ routeConfidence: typeof artifact.routeConfidence === "number" && Number.isFinite(artifact.routeConfidence) ? artifact.routeConfidence : null,
13
+ routeMetadata: artifact.routeMetadata && typeof artifact.routeMetadata === "object" && !Array.isArray(artifact.routeMetadata) ? artifact.routeMetadata : {},
11
14
  claimedPaths: Array.isArray(artifact.claimedPaths) ? artifact.claimedPaths.map((item)=>String(item)) : [],
12
15
  decisionReplay: Array.isArray(artifact.decisionReplay) ? artifact.decisionReplay.map((item)=>String(item)) : [],
13
16
  reviewNotes: Array.isArray(artifact.reviewNotes) ? artifact.reviewNotes.map((note)=>({
14
17
  ...note,
18
+ assignee: note.assignee === "codex" || note.assignee === "claude" || note.assignee === "operator" ? note.assignee : null,
15
19
  taskId: typeof note.taskId === "string" ? note.taskId : null,
16
20
  hunkIndex: typeof note.hunkIndex === "number" ? note.hunkIndex : null,
17
21
  hunkHeader: typeof note.hunkHeader === "string" ? note.hunkHeader : null,
22
+ disposition: note.disposition === "approve" || note.disposition === "concern" || note.disposition === "question" || note.disposition === "accepted_risk" || note.disposition === "wont_fix" ? note.disposition : "note",
18
23
  status: note.status === "resolved" ? "resolved" : "open",
19
24
  summary: typeof note.summary === "string" ? note.summary : "",
20
25
  body: typeof note.body === "string" ? note.body : "",