@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/README.md +9 -67
- package/dist/config.js +8 -2
- package/dist/daemon.js +126 -38
- package/dist/decision-ledger.js +58 -2
- package/dist/doctor.js +110 -0
- package/dist/main.js +70 -11
- package/dist/reviews.js +55 -2
- package/dist/router.js +126 -5
- package/dist/rpc.js +11 -1
- package/dist/session.js +5 -0
- package/dist/task-artifacts.js +5 -0
- package/dist/tui.js +156 -14
- package/package.json +2 -1
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
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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),
|
package/dist/task-artifacts.js
CHANGED
|
@@ -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 : "",
|