@mandipadk7/kavi 0.1.4 → 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 +5 -2
- package/dist/daemon.js +80 -27
- package/dist/decision-ledger.js +58 -2
- package/dist/doctor.js +77 -0
- package/dist/main.js +67 -11
- package/dist/reviews.js +24 -0
- package/dist/router.js +59 -7
- package/dist/rpc.js +1 -0
- package/dist/session.js +3 -0
- package/dist/task-artifacts.js +3 -0
- package/dist/tui.js +92 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,14 +10,14 @@ Current capabilities:
|
|
|
10
10
|
- `kavi start`: start a managed session without attaching the TUI.
|
|
11
11
|
- `kavi open`: create a managed session with separate Codex and Claude worktrees and open the full-screen operator console, even from an empty folder or a repo with no `HEAD` yet.
|
|
12
12
|
- `kavi resume`: reopen the operator console for the current repo session.
|
|
13
|
-
- `kavi status`: inspect session health
|
|
13
|
+
- `kavi status`: inspect session health, task counts, and configured routing ownership rules from any terminal.
|
|
14
14
|
- `kavi paths`: inspect resolved repo-local, user-local, worktree, and runtime paths.
|
|
15
15
|
- `kavi task`: enqueue a task for `codex`, `claude`, or `auto` routing.
|
|
16
16
|
- `kavi tasks`: inspect the session task list with summaries and artifact availability.
|
|
17
17
|
- `kavi task-output`: inspect the normalized envelope and raw output for a completed task.
|
|
18
18
|
- `kavi decisions`: inspect the persisted routing, approval, task, and integration decisions.
|
|
19
19
|
- `kavi claims`: inspect active or historical path claims.
|
|
20
|
-
- `kavi reviews`: inspect persisted operator review threads and linked follow-up tasks.
|
|
20
|
+
- `kavi reviews`: inspect persisted operator review threads and linked follow-up tasks, with filters for agent, assignee, disposition, and status.
|
|
21
21
|
- `kavi approvals`: inspect the approval inbox.
|
|
22
22
|
- `kavi approve` and `kavi deny`: resolve a pending approval request, optionally with `--remember`.
|
|
23
23
|
- `kavi events`: inspect recent daemon and task events.
|
|
@@ -33,15 +33,18 @@ Runtime model:
|
|
|
33
33
|
- SQLite event mirroring is opt-in with `KAVI_ENABLE_SQLITE_HISTORY=1`; the default event log is JSONL under `.kavi/state/events.jsonl`.
|
|
34
34
|
- The operator console exposes a task board, dual agent lanes, a live inspector pane, approval actions, inline task composition, worktree diff review with file and hunk navigation, and persisted operator review threads on files or hunks.
|
|
35
35
|
- Routing can now use explicit path ownership rules from `.kavi/config.toml` via `[routing].codex_paths` and `[routing].claude_paths`, so known parts of the tree can bypass looser keyword or AI routing.
|
|
36
|
+
- Diff-based path claims now release older overlapping same-agent claims automatically, so the active claim set stays closer to each managed worktree's current unlanded surface.
|
|
36
37
|
|
|
37
38
|
Notes:
|
|
38
39
|
- `kavi init` and `kavi open` now support the "empty folder to first managed session" path. If no git repo exists, Kavi initializes one; if git exists but no `HEAD` exists yet, Kavi creates the bootstrap commit it needs for worktrees.
|
|
39
40
|
- Codex runs through `codex app-server` in managed mode, so Codex-side approvals now land in the same Kavi inbox as Claude hook approvals.
|
|
40
41
|
- Claude still runs through the installed `claude` CLI with Kavi-managed hooks and approval decisions.
|
|
41
42
|
- `kavi doctor` now checks Claude auth readiness with `claude auth status`, and startup blocks if Claude is installed but not authenticated.
|
|
43
|
+
- `kavi doctor` also validates ownership path rules for duplicates, repo escapes, and absolute-path mistakes before those rules affect routing.
|
|
42
44
|
- The dashboard and operator commands now use the daemon's local RPC socket instead of editing session files directly, and the TUI stays updated from pushed daemon snapshots rather than polling.
|
|
43
45
|
- The socket is machine-local rather than repo-local to avoid Unix socket path-length issues in deep repos and temp directories.
|
|
44
46
|
- The console is keyboard-driven: `1-7` switch views, `j/k` move selection, `[` and `]` cycle task detail sections, `,` and `.` cycle changed files, `{` and `}` cycle patch hunks, `A/C/Q/M` add review notes, `o/O` cycle existing threads, `T` reply, `E` edit, `R` resolve or reopen, `a` cycle thread assignee, `w` mark a thread as won't-fix, `x` mark it as accepted-risk, `F` queue a fix task, `H` queue a handoff task, `y/n` resolve approvals, and `c` opens the inline task composer.
|
|
47
|
+
- Review filters are available both in the CLI and the TUI: `kavi reviews --assignee operator --status open`, and inside the console use `u`, `v`, and `d` to cycle assignee, status, and disposition filters for the active diff context.
|
|
45
48
|
- Review threads now carry explicit assignees and richer dispositions, including `accepted risk` and `won't fix`, instead of relying only on free-form note text.
|
|
46
49
|
- Successful follow-up tasks now auto-resolve linked open review threads, landed follow-up work marks those resolved threads as landed, and replying to a resolved thread reopens it.
|
|
47
50
|
|
package/dist/daemon.js
CHANGED
|
@@ -6,7 +6,7 @@ import { buildPeerMessages as buildClaudePeerMessages, runClaudeTask } from "./a
|
|
|
6
6
|
import { buildPeerMessages as buildCodexPeerMessages, runCodexTask } from "./adapters/codex.js";
|
|
7
7
|
import { buildDecisionReplay } from "./adapters/shared.js";
|
|
8
8
|
import { listApprovalRequests, resolveApprovalRequest } from "./approvals.js";
|
|
9
|
-
import { addDecisionRecord, upsertPathClaim } from "./decision-ledger.js";
|
|
9
|
+
import { addDecisionRecord, releaseSupersededClaims, upsertPathClaim } from "./decision-ledger.js";
|
|
10
10
|
import { getWorktreeDiffReview, listWorktreeChangedPaths } from "./git.js";
|
|
11
11
|
import { nowIso } from "./paths.js";
|
|
12
12
|
import { addReviewReply, addReviewNote, autoResolveReviewNotesForCompletedTask, linkReviewFollowUpTask, reviewNotesForTask, setReviewNoteStatus, updateReviewNote } from "./reviews.js";
|
|
@@ -284,6 +284,9 @@ export class KaviDaemon {
|
|
|
284
284
|
const taskId = `task-${commandId}`;
|
|
285
285
|
const task = buildAdHocTask(owner, prompt, taskId, {
|
|
286
286
|
routeReason: typeof params.routeReason === "string" ? params.routeReason : null,
|
|
287
|
+
routeStrategy: params.routeStrategy === "manual" || params.routeStrategy === "keyword" || params.routeStrategy === "ai" || params.routeStrategy === "path-claim" || params.routeStrategy === "fallback" ? params.routeStrategy : null,
|
|
288
|
+
routeConfidence: typeof params.routeConfidence === "number" ? params.routeConfidence : null,
|
|
289
|
+
routeMetadata: params.routeMetadata && typeof params.routeMetadata === "object" && !Array.isArray(params.routeMetadata) ? params.routeMetadata : {},
|
|
287
290
|
claimedPaths: Array.isArray(params.claimedPaths) ? params.claimedPaths.map((item)=>String(item)) : []
|
|
288
291
|
});
|
|
289
292
|
this.session.tasks.push(task);
|
|
@@ -296,7 +299,8 @@ export class KaviDaemon {
|
|
|
296
299
|
metadata: {
|
|
297
300
|
strategy: typeof params.routeStrategy === "string" ? params.routeStrategy : "unknown",
|
|
298
301
|
confidence: typeof params.routeConfidence === "number" ? params.routeConfidence : null,
|
|
299
|
-
claimedPaths: task.claimedPaths
|
|
302
|
+
claimedPaths: task.claimedPaths,
|
|
303
|
+
routeMetadata: params.routeMetadata && typeof params.routeMetadata === "object" && !Array.isArray(params.routeMetadata) ? params.routeMetadata : {}
|
|
300
304
|
}
|
|
301
305
|
});
|
|
302
306
|
upsertPathClaim(this.session, {
|
|
@@ -621,6 +625,13 @@ export class KaviDaemon {
|
|
|
621
625
|
promptLines.push(`Focus the change in ${note.filePath} and update the managed worktree accordingly.`);
|
|
622
626
|
const task = buildAdHocTask(owner, promptLines.join(" "), taskId, {
|
|
623
627
|
routeReason: mode === "handoff" ? `Operator handed off review note ${note.id} to ${owner}.` : `Operator created a follow-up task from review note ${note.id}.`,
|
|
628
|
+
routeStrategy: "manual",
|
|
629
|
+
routeConfidence: 1,
|
|
630
|
+
routeMetadata: {
|
|
631
|
+
source: "review-follow-up",
|
|
632
|
+
mode,
|
|
633
|
+
reviewNoteId: note.id
|
|
634
|
+
},
|
|
624
635
|
claimedPaths: [
|
|
625
636
|
note.filePath
|
|
626
637
|
]
|
|
@@ -786,6 +797,9 @@ export class KaviDaemon {
|
|
|
786
797
|
const taskId = `task-${command.id}`;
|
|
787
798
|
const task = buildAdHocTask(owner, command.payload.prompt, taskId, {
|
|
788
799
|
routeReason: typeof command.payload.routeReason === "string" ? command.payload.routeReason : null,
|
|
800
|
+
routeStrategy: command.payload.routeStrategy === "manual" || command.payload.routeStrategy === "keyword" || command.payload.routeStrategy === "ai" || command.payload.routeStrategy === "path-claim" || command.payload.routeStrategy === "fallback" ? command.payload.routeStrategy : null,
|
|
801
|
+
routeConfidence: typeof command.payload.routeConfidence === "number" ? command.payload.routeConfidence : null,
|
|
802
|
+
routeMetadata: command.payload.routeMetadata && typeof command.payload.routeMetadata === "object" && !Array.isArray(command.payload.routeMetadata) ? command.payload.routeMetadata : {},
|
|
789
803
|
claimedPaths: Array.isArray(command.payload.claimedPaths) ? command.payload.claimedPaths.map((item)=>String(item)) : []
|
|
790
804
|
});
|
|
791
805
|
this.session.tasks.push(task);
|
|
@@ -798,7 +812,8 @@ export class KaviDaemon {
|
|
|
798
812
|
metadata: {
|
|
799
813
|
strategy: typeof command.payload.routeStrategy === "string" ? command.payload.routeStrategy : "unknown",
|
|
800
814
|
confidence: typeof command.payload.routeConfidence === "number" ? command.payload.routeConfidence : null,
|
|
801
|
-
claimedPaths: task.claimedPaths
|
|
815
|
+
claimedPaths: task.claimedPaths,
|
|
816
|
+
routeMetadata: command.payload.routeMetadata && typeof command.payload.routeMetadata === "object" && !Array.isArray(command.payload.routeMetadata) ? command.payload.routeMetadata : {}
|
|
802
817
|
}
|
|
803
818
|
});
|
|
804
819
|
upsertPathClaim(this.session, {
|
|
@@ -849,18 +864,7 @@ export class KaviDaemon {
|
|
|
849
864
|
task.summary = envelope.summary;
|
|
850
865
|
task.updatedAt = new Date().toISOString();
|
|
851
866
|
if (task.owner === "codex" || task.owner === "claude") {
|
|
852
|
-
|
|
853
|
-
if (worktree) {
|
|
854
|
-
const changedPaths = await listWorktreeChangedPaths(worktree.path, this.session.baseCommit);
|
|
855
|
-
task.claimedPaths = changedPaths;
|
|
856
|
-
upsertPathClaim(this.session, {
|
|
857
|
-
taskId: task.id,
|
|
858
|
-
agent: task.owner,
|
|
859
|
-
source: "diff",
|
|
860
|
-
paths: changedPaths,
|
|
861
|
-
note: task.summary
|
|
862
|
-
});
|
|
863
|
-
}
|
|
867
|
+
await this.refreshTaskClaims(task);
|
|
864
868
|
}
|
|
865
869
|
addDecisionRecord(this.session, {
|
|
866
870
|
kind: "task",
|
|
@@ -901,6 +905,9 @@ export class KaviDaemon {
|
|
|
901
905
|
status: task.status,
|
|
902
906
|
summary: task.summary,
|
|
903
907
|
routeReason: task.routeReason,
|
|
908
|
+
routeStrategy: task.routeStrategy,
|
|
909
|
+
routeConfidence: task.routeConfidence,
|
|
910
|
+
routeMetadata: task.routeMetadata,
|
|
904
911
|
claimedPaths: task.claimedPaths,
|
|
905
912
|
decisionReplay,
|
|
906
913
|
rawOutput,
|
|
@@ -932,18 +939,7 @@ export class KaviDaemon {
|
|
|
932
939
|
task.summary = error instanceof Error ? error.message : String(error);
|
|
933
940
|
task.updatedAt = new Date().toISOString();
|
|
934
941
|
if (task.owner === "codex" || task.owner === "claude") {
|
|
935
|
-
|
|
936
|
-
if (worktree) {
|
|
937
|
-
const changedPaths = await listWorktreeChangedPaths(worktree.path, this.session.baseCommit);
|
|
938
|
-
task.claimedPaths = changedPaths;
|
|
939
|
-
upsertPathClaim(this.session, {
|
|
940
|
-
taskId: task.id,
|
|
941
|
-
agent: task.owner,
|
|
942
|
-
source: "diff",
|
|
943
|
-
paths: changedPaths,
|
|
944
|
-
note: task.summary
|
|
945
|
-
});
|
|
946
|
-
}
|
|
942
|
+
await this.refreshTaskClaims(task);
|
|
947
943
|
}
|
|
948
944
|
addDecisionRecord(this.session, {
|
|
949
945
|
kind: "task",
|
|
@@ -968,6 +964,9 @@ export class KaviDaemon {
|
|
|
968
964
|
status: task.status,
|
|
969
965
|
summary: task.summary,
|
|
970
966
|
routeReason: task.routeReason,
|
|
967
|
+
routeStrategy: task.routeStrategy,
|
|
968
|
+
routeConfidence: task.routeConfidence,
|
|
969
|
+
routeMetadata: task.routeMetadata,
|
|
971
970
|
claimedPaths: task.claimedPaths,
|
|
972
971
|
decisionReplay,
|
|
973
972
|
rawOutput: null,
|
|
@@ -994,6 +993,60 @@ export class KaviDaemon {
|
|
|
994
993
|
summary
|
|
995
994
|
};
|
|
996
995
|
}
|
|
996
|
+
async refreshTaskClaims(task) {
|
|
997
|
+
if (task.owner !== "codex" && task.owner !== "claude") {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const worktree = this.session.worktrees.find((item)=>item.agent === task.owner);
|
|
1001
|
+
if (!worktree) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const changedPaths = await listWorktreeChangedPaths(worktree.path, this.session.baseCommit);
|
|
1005
|
+
const hadClaimedSurface = task.claimedPaths.length > 0 || this.session.pathClaims.some((claim)=>claim.taskId === task.id && claim.status === "active");
|
|
1006
|
+
task.claimedPaths = changedPaths;
|
|
1007
|
+
const claim = upsertPathClaim(this.session, {
|
|
1008
|
+
taskId: task.id,
|
|
1009
|
+
agent: task.owner,
|
|
1010
|
+
source: "diff",
|
|
1011
|
+
paths: changedPaths,
|
|
1012
|
+
note: task.summary
|
|
1013
|
+
});
|
|
1014
|
+
if (claim && changedPaths.length > 0) {
|
|
1015
|
+
const releasedClaims = releaseSupersededClaims(this.session, {
|
|
1016
|
+
agent: task.owner,
|
|
1017
|
+
taskId: task.id,
|
|
1018
|
+
paths: changedPaths,
|
|
1019
|
+
note: `Superseded by newer ${task.owner} diff claim from task ${task.id}.`
|
|
1020
|
+
});
|
|
1021
|
+
for (const releasedClaim of releasedClaims){
|
|
1022
|
+
addDecisionRecord(this.session, {
|
|
1023
|
+
kind: "route",
|
|
1024
|
+
agent: task.owner,
|
|
1025
|
+
taskId: releasedClaim.taskId,
|
|
1026
|
+
summary: `Released superseded claim ${releasedClaim.id}`,
|
|
1027
|
+
detail: releasedClaim.paths.join(", ") || "No claimed paths.",
|
|
1028
|
+
metadata: {
|
|
1029
|
+
claimId: releasedClaim.id,
|
|
1030
|
+
supersededByTaskId: task.id,
|
|
1031
|
+
supersededByPaths: changedPaths
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (changedPaths.length === 0 && hadClaimedSurface) {
|
|
1038
|
+
addDecisionRecord(this.session, {
|
|
1039
|
+
kind: "route",
|
|
1040
|
+
agent: task.owner,
|
|
1041
|
+
taskId: task.id,
|
|
1042
|
+
summary: `Released empty claim surface for ${task.id}`,
|
|
1043
|
+
detail: "Task finished without a remaining worktree diff for its claimed paths.",
|
|
1044
|
+
metadata: {
|
|
1045
|
+
releaseReason: "empty-diff-claim"
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
997
1050
|
}
|
|
998
1051
|
|
|
999
1052
|
|
package/dist/decision-ledger.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { nowIso } from "./paths.js";
|
|
3
4
|
const MAX_DECISIONS = 80;
|
|
4
5
|
function normalizePaths(paths) {
|
|
5
6
|
return [
|
|
6
|
-
...new Set(paths.map(
|
|
7
|
+
...new Set(paths.map(normalizePath).filter(Boolean))
|
|
7
8
|
].sort();
|
|
8
9
|
}
|
|
10
|
+
function normalizePath(value) {
|
|
11
|
+
const trimmed = value.trim().replaceAll("\\", "/");
|
|
12
|
+
const withoutPrefix = trimmed.startsWith("./") ? trimmed.slice(2) : trimmed;
|
|
13
|
+
const normalized = path.posix.normalize(withoutPrefix);
|
|
14
|
+
return normalized === "." ? "" : normalized.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
15
|
+
}
|
|
16
|
+
function pathOverlaps(left, right) {
|
|
17
|
+
const leftPath = normalizePath(left);
|
|
18
|
+
const rightPath = normalizePath(right);
|
|
19
|
+
if (!leftPath || !rightPath) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return leftPath === rightPath || leftPath.startsWith(`${rightPath}/`) || rightPath.startsWith(`${leftPath}/`);
|
|
23
|
+
}
|
|
9
24
|
export function addDecisionRecord(session, input) {
|
|
10
25
|
const record = {
|
|
11
26
|
id: randomUUID(),
|
|
@@ -63,12 +78,53 @@ export function upsertPathClaim(session, input) {
|
|
|
63
78
|
export function activePathClaims(session) {
|
|
64
79
|
return session.pathClaims.filter((claim)=>claim.status === "active" && claim.paths.length > 0);
|
|
65
80
|
}
|
|
81
|
+
export function releasePathClaims(session, input = {}) {
|
|
82
|
+
const taskIds = input.taskIds ? new Set(input.taskIds) : null;
|
|
83
|
+
const released = [];
|
|
84
|
+
for (const claim of session.pathClaims){
|
|
85
|
+
if (claim.status !== "active") {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (taskIds && !taskIds.has(claim.taskId)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
claim.status = "released";
|
|
92
|
+
claim.updatedAt = nowIso();
|
|
93
|
+
if (input.note !== undefined) {
|
|
94
|
+
claim.note = input.note;
|
|
95
|
+
}
|
|
96
|
+
released.push(claim);
|
|
97
|
+
}
|
|
98
|
+
return released;
|
|
99
|
+
}
|
|
100
|
+
export function releaseSupersededClaims(session, input) {
|
|
101
|
+
const normalizedPaths = normalizePaths(input.paths);
|
|
102
|
+
if (normalizedPaths.length === 0) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const released = [];
|
|
106
|
+
for (const claim of session.pathClaims){
|
|
107
|
+
if (claim.status !== "active" || claim.agent !== input.agent || claim.taskId === input.taskId) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!claim.paths.some((item)=>normalizedPaths.some((candidate)=>pathOverlaps(item, candidate)))) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
claim.status = "released";
|
|
114
|
+
claim.updatedAt = nowIso();
|
|
115
|
+
if (input.note !== undefined) {
|
|
116
|
+
claim.note = input.note;
|
|
117
|
+
}
|
|
118
|
+
released.push(claim);
|
|
119
|
+
}
|
|
120
|
+
return released;
|
|
121
|
+
}
|
|
66
122
|
export function findClaimConflicts(session, owner, claimedPaths) {
|
|
67
123
|
const normalizedPaths = normalizePaths(claimedPaths);
|
|
68
124
|
if (normalizedPaths.length === 0) {
|
|
69
125
|
return [];
|
|
70
126
|
}
|
|
71
|
-
return activePathClaims(session).filter((claim)=>claim.agent !== owner && claim.paths.some((item)=>normalizedPaths.
|
|
127
|
+
return activePathClaims(session).filter((claim)=>claim.agent !== owner && claim.paths.some((item)=>normalizedPaths.some((candidate)=>pathOverlaps(item, candidate))));
|
|
72
128
|
}
|
|
73
129
|
|
|
74
130
|
|
package/dist/doctor.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
1
3
|
import { fileExists } from "./fs.js";
|
|
2
4
|
import { runCommand } from "./process.js";
|
|
3
5
|
import { hasSupportedNode, minimumNodeMajor, resolveSessionRuntime } from "./runtime.js";
|
|
@@ -19,12 +21,81 @@ export function parseClaudeAuthStatus(output) {
|
|
|
19
21
|
};
|
|
20
22
|
}
|
|
21
23
|
}
|
|
24
|
+
function normalizeRoutingPathRule(value) {
|
|
25
|
+
const trimmed = value.trim().replaceAll("\\", "/");
|
|
26
|
+
const withoutPrefix = trimmed.startsWith("./") ? trimmed.slice(2) : trimmed;
|
|
27
|
+
const normalized = path.posix.normalize(withoutPrefix);
|
|
28
|
+
return normalized === "." ? "" : normalized.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
29
|
+
}
|
|
30
|
+
export function validateRoutingPathRules(config) {
|
|
31
|
+
const issues = [];
|
|
32
|
+
const codexPaths = config.routing.codexPaths.map(normalizeRoutingPathRule);
|
|
33
|
+
const claudePaths = config.routing.claudePaths.map(normalizeRoutingPathRule);
|
|
34
|
+
const rawRules = [
|
|
35
|
+
...config.routing.codexPaths.map((rule)=>({
|
|
36
|
+
owner: "codex",
|
|
37
|
+
raw: rule,
|
|
38
|
+
normalized: normalizeRoutingPathRule(rule)
|
|
39
|
+
})),
|
|
40
|
+
...config.routing.claudePaths.map((rule)=>({
|
|
41
|
+
owner: "claude",
|
|
42
|
+
raw: rule,
|
|
43
|
+
normalized: normalizeRoutingPathRule(rule)
|
|
44
|
+
}))
|
|
45
|
+
];
|
|
46
|
+
const blankRules = [
|
|
47
|
+
...codexPaths,
|
|
48
|
+
...claudePaths
|
|
49
|
+
].filter((rule)=>!rule);
|
|
50
|
+
if (blankRules.length > 0) {
|
|
51
|
+
issues.push("Ownership path rules must not be empty.");
|
|
52
|
+
}
|
|
53
|
+
const absoluteRules = rawRules.filter(({ raw })=>{
|
|
54
|
+
const trimmed = raw.trim();
|
|
55
|
+
return trimmed.startsWith("/") || /^[A-Za-z]:[\\/]/.test(trimmed);
|
|
56
|
+
}).map(({ owner, raw })=>`${owner}:${raw}`);
|
|
57
|
+
if (absoluteRules.length > 0) {
|
|
58
|
+
issues.push(`Ownership path rules must be repo-relative, not absolute: ${absoluteRules.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
const parentTraversalRules = rawRules.filter(({ normalized })=>normalized === ".." || normalized.startsWith("../")).map(({ owner, raw })=>`${owner}:${raw}`);
|
|
61
|
+
if (parentTraversalRules.length > 0) {
|
|
62
|
+
issues.push(`Ownership path rules must stay inside the repo root: ${parentTraversalRules.join(", ")}`);
|
|
63
|
+
}
|
|
64
|
+
const duplicateRules = (rules, owner)=>{
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
const duplicates = new Set();
|
|
67
|
+
for (const rule of rules){
|
|
68
|
+
if (!rule) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (seen.has(rule)) {
|
|
72
|
+
duplicates.add(rule);
|
|
73
|
+
}
|
|
74
|
+
seen.add(rule);
|
|
75
|
+
}
|
|
76
|
+
if (duplicates.size > 0) {
|
|
77
|
+
issues.push(`${owner} has duplicate ownership rules: ${[
|
|
78
|
+
...duplicates
|
|
79
|
+
].join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
duplicateRules(codexPaths, "codex");
|
|
83
|
+
duplicateRules(claudePaths, "claude");
|
|
84
|
+
const overlappingRules = codexPaths.filter((rule)=>rule && claudePaths.includes(rule));
|
|
85
|
+
if (overlappingRules.length > 0) {
|
|
86
|
+
issues.push(`codex_paths and claude_paths overlap on the same exact rules: ${[
|
|
87
|
+
...new Set(overlappingRules)
|
|
88
|
+
].join(", ")}`);
|
|
89
|
+
}
|
|
90
|
+
return issues;
|
|
91
|
+
}
|
|
22
92
|
function normalizeVersion(output) {
|
|
23
93
|
return output.trim().split(/\s+/).slice(-1)[0] ?? output.trim();
|
|
24
94
|
}
|
|
25
95
|
export async function runDoctor(repoRoot, paths) {
|
|
26
96
|
const checks = [];
|
|
27
97
|
const runtime = await resolveSessionRuntime(paths);
|
|
98
|
+
const config = await loadConfig(paths);
|
|
28
99
|
const nodeVersion = await runCommand(runtime.nodeExecutable, [
|
|
29
100
|
"--version"
|
|
30
101
|
], {
|
|
@@ -104,6 +175,12 @@ export async function runDoctor(repoRoot, paths) {
|
|
|
104
175
|
ok: true,
|
|
105
176
|
detail: homeConfigCheck ? "present" : "will be created on demand"
|
|
106
177
|
});
|
|
178
|
+
const routingRuleIssues = validateRoutingPathRules(config);
|
|
179
|
+
checks.push({
|
|
180
|
+
name: "routing-path-rules",
|
|
181
|
+
ok: routingRuleIssues.length === 0,
|
|
182
|
+
detail: routingRuleIssues.length === 0 ? "valid" : routingRuleIssues.join("; ")
|
|
183
|
+
});
|
|
107
184
|
return checks;
|
|
108
185
|
}
|
|
109
186
|
|
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]",
|
|
@@ -349,6 +356,10 @@ async function commandStatus(cwd, args) {
|
|
|
349
356
|
pathClaimCounts: {
|
|
350
357
|
active: session.pathClaims.filter((claim)=>claim.status === "active").length
|
|
351
358
|
},
|
|
359
|
+
routingOwnership: {
|
|
360
|
+
codexPaths: session.config.routing.codexPaths,
|
|
361
|
+
claudePaths: session.config.routing.claudePaths
|
|
362
|
+
},
|
|
352
363
|
worktrees: session.worktrees
|
|
353
364
|
};
|
|
354
365
|
if (args.includes("--json")) {
|
|
@@ -368,6 +379,7 @@ async function commandStatus(cwd, args) {
|
|
|
368
379
|
console.log(`Reviews: open=${payload.reviewCounts.open} total=${payload.reviewCounts.total}`);
|
|
369
380
|
console.log(`Decisions: total=${payload.decisionCounts.total}`);
|
|
370
381
|
console.log(`Path claims: active=${payload.pathClaimCounts.active}`);
|
|
382
|
+
console.log(`Routing ownership: codex=${payload.routingOwnership.codexPaths.join(", ") || "-"} | claude=${payload.routingOwnership.claudePaths.join(", ") || "-"}`);
|
|
371
383
|
for (const worktree of payload.worktrees){
|
|
372
384
|
console.log(`- ${worktree.agent}: ${worktree.path}`);
|
|
373
385
|
}
|
|
@@ -429,13 +441,18 @@ async function commandTask(cwd, args) {
|
|
|
429
441
|
strategy: "manual",
|
|
430
442
|
confidence: 1,
|
|
431
443
|
reason: `User explicitly assigned the task to ${requestedAgent}.`,
|
|
432
|
-
claimedPaths: extractPromptPathHints(prompt)
|
|
444
|
+
claimedPaths: extractPromptPathHints(prompt),
|
|
445
|
+
metadata: {
|
|
446
|
+
manualAssignment: true,
|
|
447
|
+
requestedAgent
|
|
448
|
+
}
|
|
433
449
|
} : await routeTask(prompt, session, paths);
|
|
434
450
|
if (rpcSnapshot) {
|
|
435
451
|
await rpcEnqueueTask(paths, {
|
|
436
452
|
owner: routeDecision.owner,
|
|
437
453
|
prompt,
|
|
438
454
|
routeReason: routeDecision.reason,
|
|
455
|
+
routeMetadata: routeDecision.metadata,
|
|
439
456
|
claimedPaths: routeDecision.claimedPaths,
|
|
440
457
|
routeStrategy: routeDecision.strategy,
|
|
441
458
|
routeConfidence: routeDecision.confidence
|
|
@@ -445,6 +462,7 @@ async function commandTask(cwd, args) {
|
|
|
445
462
|
owner: routeDecision.owner,
|
|
446
463
|
prompt,
|
|
447
464
|
routeReason: routeDecision.reason,
|
|
465
|
+
routeMetadata: routeDecision.metadata,
|
|
448
466
|
claimedPaths: routeDecision.claimedPaths,
|
|
449
467
|
routeStrategy: routeDecision.strategy,
|
|
450
468
|
routeConfidence: routeDecision.confidence
|
|
@@ -455,7 +473,8 @@ async function commandTask(cwd, args) {
|
|
|
455
473
|
prompt,
|
|
456
474
|
strategy: routeDecision.strategy,
|
|
457
475
|
confidence: routeDecision.confidence,
|
|
458
|
-
claimedPaths: routeDecision.claimedPaths
|
|
476
|
+
claimedPaths: routeDecision.claimedPaths,
|
|
477
|
+
routeMetadata: routeDecision.metadata
|
|
459
478
|
});
|
|
460
479
|
console.log(`Queued task for ${routeDecision.owner}: ${prompt}\nRoute: ${routeDecision.strategy} (${routeDecision.confidence.toFixed(2)}) ${routeDecision.reason}`);
|
|
461
480
|
}
|
|
@@ -477,6 +496,9 @@ async function commandTasks(cwd, args) {
|
|
|
477
496
|
updatedAt: task.updatedAt,
|
|
478
497
|
summary: task.summary,
|
|
479
498
|
routeReason: task.routeReason,
|
|
499
|
+
routeStrategy: task.routeStrategy,
|
|
500
|
+
routeConfidence: task.routeConfidence,
|
|
501
|
+
routeMetadata: task.routeMetadata,
|
|
480
502
|
claimedPaths: task.claimedPaths,
|
|
481
503
|
hasArtifact: artifactMap.has(task.id)
|
|
482
504
|
}));
|
|
@@ -488,8 +510,11 @@ async function commandTasks(cwd, args) {
|
|
|
488
510
|
console.log(`${task.id} | ${task.owner} | ${task.status} | artifact=${task.hasArtifact ? "yes" : "no"}`);
|
|
489
511
|
console.log(` title: ${task.title}`);
|
|
490
512
|
console.log(` updated: ${task.updatedAt}`);
|
|
491
|
-
console.log(` route: ${task.routeReason ?? "-"}`);
|
|
513
|
+
console.log(` route: ${task.routeStrategy ?? "-"}${task.routeConfidence === null ? "" : ` (${task.routeConfidence.toFixed(2)})`} ${task.routeReason ?? "-"}`);
|
|
492
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
|
+
}
|
|
493
518
|
console.log(` summary: ${task.summary ?? "-"}`);
|
|
494
519
|
}
|
|
495
520
|
}
|
|
@@ -529,7 +554,8 @@ async function commandTaskOutput(cwd, args) {
|
|
|
529
554
|
console.log(`Started: ${artifact.startedAt}`);
|
|
530
555
|
console.log(`Finished: ${artifact.finishedAt}`);
|
|
531
556
|
console.log(`Summary: ${artifact.summary ?? "-"}`);
|
|
532
|
-
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 ?? {})}`);
|
|
533
559
|
console.log(`Claimed paths: ${artifact.claimedPaths.join(", ") || "-"}`);
|
|
534
560
|
console.log(`Error: ${artifact.error ?? "-"}`);
|
|
535
561
|
console.log("Decision Replay:");
|
|
@@ -577,6 +603,9 @@ async function commandDecisions(cwd, args) {
|
|
|
577
603
|
console.log(`${decision.createdAt} | ${decision.kind} | ${decision.agent ?? "-"} | ${decision.summary}`);
|
|
578
604
|
console.log(` task: ${decision.taskId ?? "-"}`);
|
|
579
605
|
console.log(` detail: ${decision.detail}`);
|
|
606
|
+
if (Object.keys(decision.metadata).length > 0) {
|
|
607
|
+
console.log(` metadata: ${JSON.stringify(decision.metadata)}`);
|
|
608
|
+
}
|
|
580
609
|
}
|
|
581
610
|
}
|
|
582
611
|
async function commandClaims(cwd, args) {
|
|
@@ -603,15 +632,19 @@ async function commandReviews(cwd, args) {
|
|
|
603
632
|
const { paths } = await requireSession(cwd);
|
|
604
633
|
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
605
634
|
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
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);
|
|
609
642
|
if (args.includes("--json")) {
|
|
610
643
|
console.log(JSON.stringify(notes, null, 2));
|
|
611
644
|
return;
|
|
612
645
|
}
|
|
613
646
|
if (notes.length === 0) {
|
|
614
|
-
console.log("No review notes
|
|
647
|
+
console.log("No review notes matched the current filters.");
|
|
615
648
|
return;
|
|
616
649
|
}
|
|
617
650
|
for (const note of notes){
|
|
@@ -748,6 +781,12 @@ async function commandLand(cwd) {
|
|
|
748
781
|
].join("\n"), taskId, {
|
|
749
782
|
title: "Resolve integration overlap",
|
|
750
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
|
+
},
|
|
751
790
|
claimedPaths: overlappingPaths
|
|
752
791
|
}));
|
|
753
792
|
upsertPathClaim(session, {
|
|
@@ -788,6 +827,23 @@ async function commandLand(cwd) {
|
|
|
788
827
|
snapshotCommits: result.snapshotCommits,
|
|
789
828
|
commands: result.commandsRun
|
|
790
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
|
+
}
|
|
791
847
|
const landedReviewNotes = markReviewNotesLandedForTasks(session, session.tasks.filter((task)=>task.status === "completed").map((task)=>task.id));
|
|
792
848
|
for (const note of landedReviewNotes){
|
|
793
849
|
addDecisionRecord(session, {
|
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) {
|
package/dist/router.js
CHANGED
|
@@ -44,6 +44,9 @@ function normalizeClaimedPaths(paths) {
|
|
|
44
44
|
...new Set(paths.map((item)=>item.trim()).filter(Boolean))
|
|
45
45
|
].sort();
|
|
46
46
|
}
|
|
47
|
+
function buildRouteMetadata(input = {}) {
|
|
48
|
+
return input;
|
|
49
|
+
}
|
|
47
50
|
function normalizePathPattern(value) {
|
|
48
51
|
const trimmed = value.trim().replaceAll("\\", "/");
|
|
49
52
|
const withoutPrefix = trimmed.startsWith("./") ? trimmed.slice(2) : trimmed;
|
|
@@ -84,13 +87,20 @@ function buildPathOwnershipDecision(prompt, config) {
|
|
|
84
87
|
return null;
|
|
85
88
|
}
|
|
86
89
|
const owner = codexMatches > claudeMatches ? "codex" : "claude";
|
|
87
|
-
const
|
|
90
|
+
const ownerPatterns = owner === "codex" ? config.routing.codexPaths : config.routing.claudePaths;
|
|
91
|
+
const matchedPatterns = ownerPatterns.filter((pattern)=>claimedPaths.some((filePath)=>matchesPattern(filePath, pattern)));
|
|
88
92
|
return {
|
|
89
93
|
owner,
|
|
90
94
|
strategy: "manual",
|
|
91
95
|
confidence: 0.97,
|
|
92
96
|
reason: `Matched explicit ${owner} path ownership rules for: ${claimedPaths.join(", ")}.`,
|
|
93
|
-
claimedPaths
|
|
97
|
+
claimedPaths,
|
|
98
|
+
metadata: buildRouteMetadata({
|
|
99
|
+
ownershipSource: "config-routing-paths",
|
|
100
|
+
matchedPatterns,
|
|
101
|
+
codexMatches,
|
|
102
|
+
claudeMatches
|
|
103
|
+
})
|
|
94
104
|
};
|
|
95
105
|
}
|
|
96
106
|
export function extractPromptPathHints(prompt) {
|
|
@@ -131,7 +141,11 @@ function buildKeywordDecision(prompt, config) {
|
|
|
131
141
|
strategy: "keyword",
|
|
132
142
|
confidence: 0.92,
|
|
133
143
|
reason: "Matched frontend and UX routing keywords.",
|
|
134
|
-
claimedPaths
|
|
144
|
+
claimedPaths,
|
|
145
|
+
metadata: buildRouteMetadata({
|
|
146
|
+
matchedKeywordSet: "frontend",
|
|
147
|
+
promptHints: claimedPaths
|
|
148
|
+
})
|
|
135
149
|
};
|
|
136
150
|
}
|
|
137
151
|
if (backend && !frontend) {
|
|
@@ -140,7 +154,11 @@ function buildKeywordDecision(prompt, config) {
|
|
|
140
154
|
strategy: "keyword",
|
|
141
155
|
confidence: 0.92,
|
|
142
156
|
reason: "Matched backend and architecture routing keywords.",
|
|
143
|
-
claimedPaths
|
|
157
|
+
claimedPaths,
|
|
158
|
+
metadata: buildRouteMetadata({
|
|
159
|
+
matchedKeywordSet: "backend",
|
|
160
|
+
promptHints: claimedPaths
|
|
161
|
+
})
|
|
144
162
|
};
|
|
145
163
|
}
|
|
146
164
|
return null;
|
|
@@ -214,7 +232,11 @@ async function routeWithCodexAi(prompt, session) {
|
|
|
214
232
|
strategy: "ai",
|
|
215
233
|
confidence,
|
|
216
234
|
reason,
|
|
217
|
-
claimedPaths
|
|
235
|
+
claimedPaths,
|
|
236
|
+
metadata: buildRouteMetadata({
|
|
237
|
+
router: "codex-ai",
|
|
238
|
+
promptHints: extractPromptPathHints(prompt)
|
|
239
|
+
})
|
|
218
240
|
};
|
|
219
241
|
} finally{
|
|
220
242
|
await client.close();
|
|
@@ -235,7 +257,18 @@ function applyClaimRouting(session, decision) {
|
|
|
235
257
|
strategy: "path-claim",
|
|
236
258
|
confidence: 1,
|
|
237
259
|
reason: `Re-routed to ${owner} because active path claims overlap: ${overlappingPaths.join(", ")}`,
|
|
238
|
-
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
|
+
})
|
|
239
272
|
};
|
|
240
273
|
}
|
|
241
274
|
export async function routeTask(prompt, session, _paths) {
|
|
@@ -256,7 +289,11 @@ export async function routeTask(prompt, session, _paths) {
|
|
|
256
289
|
strategy: "fallback",
|
|
257
290
|
confidence: 0.4,
|
|
258
291
|
reason: error instanceof Error ? `AI routing failed, defaulted to Codex: ${error.message}` : "AI routing failed, defaulted to Codex.",
|
|
259
|
-
claimedPaths: extractPromptPathHints(prompt)
|
|
292
|
+
claimedPaths: extractPromptPathHints(prompt),
|
|
293
|
+
metadata: buildRouteMetadata({
|
|
294
|
+
router: "fallback",
|
|
295
|
+
error: error instanceof Error ? error.message : String(error)
|
|
296
|
+
})
|
|
260
297
|
});
|
|
261
298
|
}
|
|
262
299
|
}
|
|
@@ -273,6 +310,12 @@ export function buildKickoffTasks(goal) {
|
|
|
273
310
|
updatedAt: timestamp,
|
|
274
311
|
summary: null,
|
|
275
312
|
routeReason: "Kickoff task reserved for Codex planning.",
|
|
313
|
+
routeStrategy: "manual",
|
|
314
|
+
routeConfidence: 1,
|
|
315
|
+
routeMetadata: buildRouteMetadata({
|
|
316
|
+
kickoff: true,
|
|
317
|
+
reservedFor: "codex"
|
|
318
|
+
}),
|
|
276
319
|
claimedPaths: []
|
|
277
320
|
},
|
|
278
321
|
{
|
|
@@ -285,6 +328,12 @@ export function buildKickoffTasks(goal) {
|
|
|
285
328
|
updatedAt: timestamp,
|
|
286
329
|
summary: null,
|
|
287
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
|
+
}),
|
|
288
337
|
claimedPaths: []
|
|
289
338
|
}
|
|
290
339
|
];
|
|
@@ -301,6 +350,9 @@ export function buildAdHocTask(owner, prompt, taskId, options = {}) {
|
|
|
301
350
|
updatedAt: timestamp,
|
|
302
351
|
summary: null,
|
|
303
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 ?? {},
|
|
304
356
|
claimedPaths: normalizeClaimedPaths(options.claimedPaths ?? [])
|
|
305
357
|
};
|
|
306
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
|
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 : [];
|
package/dist/task-artifacts.js
CHANGED
|
@@ -8,6 +8,9 @@ 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)=>({
|
package/dist/tui.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import readline from "node:readline";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { cycleReviewAssignee } from "./reviews.js";
|
|
4
|
+
import { cycleReviewAssignee, reviewNoteMatchesFilters } from "./reviews.js";
|
|
5
5
|
import { extractPromptPathHints, routeTask } from "./router.js";
|
|
6
6
|
import { pingRpc, rpcAddReviewNote, rpcAddReviewReply, rpcEnqueueReviewFollowUp, readSnapshot, rpcEnqueueTask, rpcResolveApproval, rpcSetReviewNoteStatus, rpcShutdown, rpcTaskArtifact, rpcUpdateReviewNote, rpcWorktreeDiff, subscribeSnapshotRpc } from "./rpc.js";
|
|
7
7
|
const RESET = "\u001b[0m";
|
|
@@ -569,6 +569,41 @@ function reviewAssigneeLabel(assignee) {
|
|
|
569
569
|
return "unassigned";
|
|
570
570
|
}
|
|
571
571
|
}
|
|
572
|
+
function reviewFilterLabel(filters) {
|
|
573
|
+
return `status=${filters.status} | assignee=${filters.assignee === "all" ? "all" : reviewAssigneeLabel(filters.assignee)} | disposition=${filters.disposition === "all" ? "all" : reviewDispositionLabel(filters.disposition)}`;
|
|
574
|
+
}
|
|
575
|
+
function cycleReviewFilterAssignee(current) {
|
|
576
|
+
const sequence = [
|
|
577
|
+
"all",
|
|
578
|
+
"codex",
|
|
579
|
+
"claude",
|
|
580
|
+
"operator"
|
|
581
|
+
];
|
|
582
|
+
const index = sequence.findIndex((item)=>item === current);
|
|
583
|
+
return sequence[(index + 1) % sequence.length] ?? "all";
|
|
584
|
+
}
|
|
585
|
+
function cycleReviewFilterDisposition(current) {
|
|
586
|
+
const sequence = [
|
|
587
|
+
"all",
|
|
588
|
+
"approve",
|
|
589
|
+
"concern",
|
|
590
|
+
"question",
|
|
591
|
+
"note",
|
|
592
|
+
"accepted_risk",
|
|
593
|
+
"wont_fix"
|
|
594
|
+
];
|
|
595
|
+
const index = sequence.findIndex((item)=>item === current);
|
|
596
|
+
return sequence[(index + 1) % sequence.length] ?? "all";
|
|
597
|
+
}
|
|
598
|
+
function cycleReviewFilterStatus(current) {
|
|
599
|
+
const sequence = [
|
|
600
|
+
"all",
|
|
601
|
+
"open",
|
|
602
|
+
"resolved"
|
|
603
|
+
];
|
|
604
|
+
const index = sequence.findIndex((item)=>item === current);
|
|
605
|
+
return sequence[(index + 1) % sequence.length] ?? "all";
|
|
606
|
+
}
|
|
572
607
|
function activeReviewContext(snapshot, ui) {
|
|
573
608
|
const agent = reviewAgentForUi(snapshot, ui);
|
|
574
609
|
if (!agent) {
|
|
@@ -590,7 +625,7 @@ function activeReviewContext(snapshot, ui) {
|
|
|
590
625
|
hunkHeader
|
|
591
626
|
};
|
|
592
627
|
}
|
|
593
|
-
function reviewNotesForContext(snapshot, context) {
|
|
628
|
+
function reviewNotesForContext(snapshot, context, filters) {
|
|
594
629
|
if (!snapshot || !context) {
|
|
595
630
|
return [];
|
|
596
631
|
}
|
|
@@ -600,18 +635,25 @@ function reviewNotesForContext(snapshot, context) {
|
|
|
600
635
|
if (note.agent !== context.agent || note.filePath !== context.filePath) {
|
|
601
636
|
return false;
|
|
602
637
|
}
|
|
603
|
-
if (context.hunkIndex !== null) {
|
|
604
|
-
return
|
|
638
|
+
if (context.hunkIndex !== null && !(note.hunkIndex === context.hunkIndex || note.hunkIndex === null)) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
if (!reviewNoteMatchesFilters(note, {
|
|
642
|
+
assignee: filters.assignee === "all" ? null : filters.assignee,
|
|
643
|
+
disposition: filters.disposition === "all" ? null : filters.disposition,
|
|
644
|
+
status: filters.status === "all" ? null : filters.status
|
|
645
|
+
})) {
|
|
646
|
+
return false;
|
|
605
647
|
}
|
|
606
648
|
return true;
|
|
607
649
|
}).sort((left, right)=>right.createdAt.localeCompare(left.createdAt));
|
|
608
650
|
}
|
|
609
651
|
function syncSelectedReviewNote(snapshot, ui) {
|
|
610
|
-
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
652
|
+
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui), ui.reviewFilters);
|
|
611
653
|
ui.selectedReviewNoteId = notes.some((note)=>note.id === ui.selectedReviewNoteId) ? ui.selectedReviewNoteId : notes[0]?.id ?? null;
|
|
612
654
|
}
|
|
613
655
|
function selectedReviewNote(snapshot, ui) {
|
|
614
|
-
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
656
|
+
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui), ui.reviewFilters);
|
|
615
657
|
return notes.find((note)=>note.id === ui.selectedReviewNoteId) ?? notes[0] ?? null;
|
|
616
658
|
}
|
|
617
659
|
function renderReviewNotesSection(notes, selectedNoteId, width) {
|
|
@@ -753,10 +795,13 @@ function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDif
|
|
|
753
795
|
`Owner: ${task.owner}`,
|
|
754
796
|
`Status: ${task.status}`,
|
|
755
797
|
`Updated: ${shortTime(task.updatedAt)}`,
|
|
756
|
-
`Route: ${task.routeReason ?? "-"}`,
|
|
798
|
+
`Route: ${task.routeStrategy ?? "-"}${task.routeConfidence === null ? "" : ` (${task.routeConfidence.toFixed(2)})`} ${task.routeReason ?? "-"}`,
|
|
757
799
|
`Claimed paths: ${task.claimedPaths.join(", ") || "-"}`,
|
|
758
800
|
`Summary: ${task.summary ?? "-"}`
|
|
759
801
|
].flatMap((line)=>wrapText(line, innerWidth))));
|
|
802
|
+
if (Object.keys(task.routeMetadata).length > 0) {
|
|
803
|
+
lines.push(...section("Route Metadata", wrapText(JSON.stringify(task.routeMetadata), innerWidth)));
|
|
804
|
+
}
|
|
760
805
|
if (loading) {
|
|
761
806
|
lines.push(...section("Artifact", [
|
|
762
807
|
"Loading task artifact..."
|
|
@@ -809,7 +854,7 @@ function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDif
|
|
|
809
854
|
lines.push(...section("Task Scope", [
|
|
810
855
|
`Owner: ${task.owner}`,
|
|
811
856
|
`Claimed paths: ${task.claimedPaths.join(", ") || "-"}`,
|
|
812
|
-
`Route reason: ${task.routeReason ?? "-"}`
|
|
857
|
+
`Route reason: ${task.routeStrategy ?? "-"}${task.routeConfidence === null ? "" : ` (${task.routeConfidence.toFixed(2)})`} ${task.routeReason ?? "-"}`
|
|
813
858
|
].flatMap((line)=>wrapText(line, innerWidth))));
|
|
814
859
|
if (!agent) {
|
|
815
860
|
lines.push(...section("Diff Review", [
|
|
@@ -826,7 +871,7 @@ function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDif
|
|
|
826
871
|
const hunks = parseDiffHunks(review.patch);
|
|
827
872
|
const hunkIndex = agent ? selectedHunkIndex(ui, agent, review) : null;
|
|
828
873
|
const selectedHunk = hunkIndex === null ? null : hunks[hunkIndex] ?? null;
|
|
829
|
-
const reviewNotes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
874
|
+
const reviewNotes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui), ui.reviewFilters);
|
|
830
875
|
lines.push(...section("Review", [
|
|
831
876
|
`Agent: ${review.agent}`,
|
|
832
877
|
`Selected file: ${review.selectedPath ?? "-"}`,
|
|
@@ -834,6 +879,7 @@ function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDif
|
|
|
834
879
|
`Hunks: ${hunks.length}`,
|
|
835
880
|
`Selected hunk: ${hunkIndex === null ? "-" : `${hunkIndex + 1}/${hunks.length}`}`,
|
|
836
881
|
`Notes: ${reviewNotes.length}`,
|
|
882
|
+
`Filters: ${reviewFilterLabel(ui.reviewFilters)}`,
|
|
837
883
|
`Stat: ${review.stat}`
|
|
838
884
|
].flatMap((line)=>wrapText(line, innerWidth))));
|
|
839
885
|
lines.push(...section("Changed Files", review.changedPaths.length ? review.changedPaths.flatMap((filePath)=>wrapText(`${filePath === review.selectedPath ? ">" : "-"} ${filePath}`, innerWidth)) : [
|
|
@@ -1024,12 +1070,13 @@ function renderWorktreeInspector(snapshot, worktree, diffEntry, loadingDiff, ui,
|
|
|
1024
1070
|
const hunks = parseDiffHunks(diffEntry.review.patch);
|
|
1025
1071
|
const hunkIndex = selectedHunkIndex(ui, worktree.agent, diffEntry.review);
|
|
1026
1072
|
const selectedHunk = hunkIndex === null ? null : hunks[hunkIndex] ?? null;
|
|
1027
|
-
const reviewNotes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
1073
|
+
const reviewNotes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui), ui.reviewFilters);
|
|
1028
1074
|
lines.push(...section("Review", [
|
|
1029
1075
|
`Selected file: ${diffEntry.review.selectedPath ?? "-"}`,
|
|
1030
1076
|
`Hunks: ${hunks.length}`,
|
|
1031
1077
|
`Selected hunk: ${hunkIndex === null ? "-" : `${hunkIndex + 1}/${hunks.length}`}`,
|
|
1032
1078
|
`Notes: ${reviewNotes.length}`,
|
|
1079
|
+
`Filters: ${reviewFilterLabel(ui.reviewFilters)}`,
|
|
1033
1080
|
`Stat: ${diffEntry.review.stat}`
|
|
1034
1081
|
].flatMap((line)=>wrapText(line, innerWidth))));
|
|
1035
1082
|
if (selectedHunk) {
|
|
@@ -1178,7 +1225,7 @@ function renderFooter(snapshot, ui, width) {
|
|
|
1178
1225
|
}
|
|
1179
1226
|
return [
|
|
1180
1227
|
fitAnsiLine("Keys: 1-7 tabs | h/l or Tab cycle tabs | j/k move | [ ] task detail | ,/. diff file | { } diff hunk | c compose | r refresh", width),
|
|
1181
|
-
fitAnsiLine("Actions: y/Y allow approval | n/N deny approval | A/C/Q/M add note | o/O select note | T reply | E edit | R resolve | a cycle assignee | w won't fix | x accepted risk | F fix task | H handoff | g/G top/bottom | s stop daemon | q quit", width),
|
|
1228
|
+
fitAnsiLine("Actions: y/Y allow approval | n/N deny approval | A/C/Q/M add note | o/O select note | T reply | E edit | R resolve | a cycle assignee | u filter assignee | v filter status | d filter disposition | w won't fix | x accepted risk | F fix task | H handoff | g/G top/bottom | s stop daemon | q quit", width),
|
|
1182
1229
|
footerSelectionSummary(snapshot, ui, width),
|
|
1183
1230
|
fitAnsiLine(toast ? styleLine(toast.message, toast.level === "error" ? "bad" : "good") : styleLine("Operator surface is live over the daemon socket with pushed snapshots.", "muted"), width)
|
|
1184
1231
|
];
|
|
@@ -1311,12 +1358,17 @@ async function queueManualTask(paths, view, ui) {
|
|
|
1311
1358
|
strategy: "manual",
|
|
1312
1359
|
confidence: 1,
|
|
1313
1360
|
reason: `Operator manually assigned the task to ${composer.owner}.`,
|
|
1314
|
-
claimedPaths: extractPromptPathHints(prompt)
|
|
1361
|
+
claimedPaths: extractPromptPathHints(prompt),
|
|
1362
|
+
metadata: {
|
|
1363
|
+
manualAssignment: true,
|
|
1364
|
+
composerOwner: composer.owner
|
|
1365
|
+
}
|
|
1315
1366
|
};
|
|
1316
1367
|
await rpcEnqueueTask(paths, {
|
|
1317
1368
|
owner: routeDecision.owner,
|
|
1318
1369
|
prompt,
|
|
1319
1370
|
routeReason: routeDecision.reason,
|
|
1371
|
+
routeMetadata: routeDecision.metadata,
|
|
1320
1372
|
claimedPaths: routeDecision.claimedPaths,
|
|
1321
1373
|
routeStrategy: routeDecision.strategy,
|
|
1322
1374
|
routeConfidence: routeDecision.confidence
|
|
@@ -1425,7 +1477,7 @@ async function resolveSelectedReviewNoteWithDisposition(paths, snapshot, ui, dis
|
|
|
1425
1477
|
setToast(ui, "info", `Marked review note ${note.id} as ${reviewDispositionLabel(disposition).toLowerCase()}.`);
|
|
1426
1478
|
}
|
|
1427
1479
|
function cycleSelectedReviewNote(snapshot, ui, delta) {
|
|
1428
|
-
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
1480
|
+
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui), ui.reviewFilters);
|
|
1429
1481
|
if (notes.length === 0) {
|
|
1430
1482
|
ui.selectedReviewNoteId = null;
|
|
1431
1483
|
return false;
|
|
@@ -1589,7 +1641,12 @@ export async function attachTui(paths) {
|
|
|
1589
1641
|
codex: 0,
|
|
1590
1642
|
claude: 0
|
|
1591
1643
|
},
|
|
1592
|
-
selectedReviewNoteId: null
|
|
1644
|
+
selectedReviewNoteId: null,
|
|
1645
|
+
reviewFilters: {
|
|
1646
|
+
assignee: "all",
|
|
1647
|
+
disposition: "all",
|
|
1648
|
+
status: "all"
|
|
1649
|
+
}
|
|
1593
1650
|
};
|
|
1594
1651
|
const render = ()=>{
|
|
1595
1652
|
process.stdout.write(renderScreen(view, ui, paths));
|
|
@@ -1984,6 +2041,27 @@ export async function attachTui(paths) {
|
|
|
1984
2041
|
});
|
|
1985
2042
|
return;
|
|
1986
2043
|
}
|
|
2044
|
+
if (input === "u") {
|
|
2045
|
+
ui.reviewFilters.assignee = cycleReviewFilterAssignee(ui.reviewFilters.assignee);
|
|
2046
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
2047
|
+
setToast(ui, "info", `Review assignee filter: ${ui.reviewFilters.assignee === "all" ? "all" : reviewAssigneeLabel(ui.reviewFilters.assignee)}.`);
|
|
2048
|
+
render();
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
if (input === "v") {
|
|
2052
|
+
ui.reviewFilters.status = cycleReviewFilterStatus(ui.reviewFilters.status);
|
|
2053
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
2054
|
+
setToast(ui, "info", `Review status filter: ${ui.reviewFilters.status}.`);
|
|
2055
|
+
render();
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (input === "d") {
|
|
2059
|
+
ui.reviewFilters.disposition = cycleReviewFilterDisposition(ui.reviewFilters.disposition);
|
|
2060
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
2061
|
+
setToast(ui, "info", `Review disposition filter: ${ui.reviewFilters.disposition === "all" ? "all" : reviewDispositionLabel(ui.reviewFilters.disposition)}.`);
|
|
2062
|
+
render();
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
1987
2065
|
if (input === "w" || input === "x") {
|
|
1988
2066
|
runAction(async ()=>{
|
|
1989
2067
|
await resolveSelectedReviewNoteWithDisposition(paths, view.snapshot, ui, input === "w" ? "wont_fix" : "accepted_risk");
|