@mandipadk7/kavi 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/dist/daemon.js +322 -0
- package/dist/git.js +101 -2
- package/dist/main.js +168 -23
- package/dist/reviews.js +159 -0
- package/dist/rpc.js +36 -0
- package/dist/session.js +25 -0
- package/dist/task-artifacts.js +26 -1
- package/dist/tui.js +317 -8
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -9,14 +9,15 @@ import { KaviDaemon } from "./daemon.js";
|
|
|
9
9
|
import { addDecisionRecord, upsertPathClaim } from "./decision-ledger.js";
|
|
10
10
|
import { runDoctor } from "./doctor.js";
|
|
11
11
|
import { writeJson } from "./fs.js";
|
|
12
|
-
import { createGitignoreEntries, detectRepoRoot, ensureWorktrees,
|
|
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
17
|
import { resolveSessionRuntime } from "./runtime.js";
|
|
17
18
|
import { buildAdHocTask, extractPromptPathHints, routeTask } from "./router.js";
|
|
18
|
-
import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
|
|
19
|
-
import { listTaskArtifacts, loadTaskArtifact } from "./task-artifacts.js";
|
|
19
|
+
import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
|
|
20
|
+
import { listTaskArtifacts, loadTaskArtifact, saveTaskArtifact } from "./task-artifacts.js";
|
|
20
21
|
import { attachTui } from "./tui.js";
|
|
21
22
|
const HEARTBEAT_STALE_MS = 10_000;
|
|
22
23
|
const CLAUDE_AUTO_ALLOW_TOOLS = new Set([
|
|
@@ -59,7 +60,7 @@ async function readStdinText() {
|
|
|
59
60
|
function renderUsage() {
|
|
60
61
|
return [
|
|
61
62
|
"Usage:",
|
|
62
|
-
" kavi init [--home]",
|
|
63
|
+
" kavi init [--home] [--no-commit]",
|
|
63
64
|
" kavi doctor [--json]",
|
|
64
65
|
" kavi start [--goal \"...\"]",
|
|
65
66
|
" kavi open [--goal \"...\"]",
|
|
@@ -71,6 +72,7 @@ function renderUsage() {
|
|
|
71
72
|
" kavi task-output <task-id|latest> [--json]",
|
|
72
73
|
" kavi decisions [--json] [--limit N]",
|
|
73
74
|
" kavi claims [--json] [--all]",
|
|
75
|
+
" kavi reviews [--json] [--all]",
|
|
74
76
|
" kavi approvals [--json] [--all]",
|
|
75
77
|
" kavi approve <request-id|latest> [--remember]",
|
|
76
78
|
" kavi deny <request-id|latest> [--remember]",
|
|
@@ -111,22 +113,65 @@ async function waitForSession(paths, expectedState = "running") {
|
|
|
111
113
|
}
|
|
112
114
|
throw new Error(`Timed out waiting for session state ${expectedState} in ${paths.stateFile}.`);
|
|
113
115
|
}
|
|
114
|
-
async function
|
|
115
|
-
|
|
116
|
+
async function prepareProjectContext(cwd, options) {
|
|
117
|
+
let repoRoot;
|
|
118
|
+
let createdRepository = false;
|
|
119
|
+
let hasGitRepository = false;
|
|
120
|
+
if (options.createRepository) {
|
|
121
|
+
const repository = await ensureGitRepository(cwd);
|
|
122
|
+
repoRoot = repository.repoRoot;
|
|
123
|
+
createdRepository = repository.createdRepository;
|
|
124
|
+
hasGitRepository = true;
|
|
125
|
+
} else {
|
|
126
|
+
const existingRepoRoot = await findRepoRoot(cwd);
|
|
127
|
+
repoRoot = existingRepoRoot ?? cwd;
|
|
128
|
+
hasGitRepository = existingRepoRoot !== null;
|
|
129
|
+
}
|
|
116
130
|
const paths = resolveAppPaths(repoRoot);
|
|
117
131
|
await ensureProjectScaffold(paths);
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
if (hasGitRepository) {
|
|
133
|
+
await createGitignoreEntries(repoRoot);
|
|
134
|
+
}
|
|
135
|
+
if (options.ensureHomeConfig) {
|
|
120
136
|
await ensureHomeConfig(paths);
|
|
121
|
-
console.log(`Initialized user-local Kavi config in ${paths.homeConfigFile}`);
|
|
122
137
|
}
|
|
123
|
-
|
|
138
|
+
return {
|
|
139
|
+
repoRoot,
|
|
140
|
+
paths,
|
|
141
|
+
createdRepository,
|
|
142
|
+
bootstrapCommit: options.ensureHeadCommit ? await ensureBootstrapCommit(repoRoot) : null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function commandInit(cwd, args) {
|
|
146
|
+
const skipCommit = args.includes("--no-commit");
|
|
147
|
+
const prepared = await prepareProjectContext(cwd, {
|
|
148
|
+
createRepository: true,
|
|
149
|
+
ensureHeadCommit: !skipCommit,
|
|
150
|
+
ensureHomeConfig: args.includes("--home")
|
|
151
|
+
});
|
|
152
|
+
if (args.includes("--home")) {
|
|
153
|
+
console.log(`Initialized user-local Kavi config in ${prepared.paths.homeConfigFile}`);
|
|
154
|
+
}
|
|
155
|
+
if (prepared.createdRepository) {
|
|
156
|
+
console.log(`Initialized git repository in ${prepared.repoRoot}`);
|
|
157
|
+
}
|
|
158
|
+
console.log(`Initialized Kavi project scaffold in ${prepared.paths.kaviDir}`);
|
|
159
|
+
if (skipCommit) {
|
|
160
|
+
console.log("Skipped bootstrap commit creation (--no-commit).");
|
|
161
|
+
console.log('Kavi will create the first base commit automatically on "kavi open" or "kavi start".');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (prepared.bootstrapCommit?.createdCommit) {
|
|
165
|
+
console.log(`Created bootstrap commit ${prepared.bootstrapCommit.commit.slice(0, 12)} with ${prepared.bootstrapCommit.stagedPaths.length} tracked path${prepared.bootstrapCommit.stagedPaths.length === 1 ? "" : "s"}.`);
|
|
166
|
+
}
|
|
124
167
|
}
|
|
125
168
|
async function commandDoctor(cwd, args) {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
169
|
+
const prepared = await prepareProjectContext(cwd, {
|
|
170
|
+
createRepository: false,
|
|
171
|
+
ensureHeadCommit: false,
|
|
172
|
+
ensureHomeConfig: false
|
|
173
|
+
});
|
|
174
|
+
const checks = await runDoctor(prepared.repoRoot, prepared.paths);
|
|
130
175
|
if (args.includes("--json")) {
|
|
131
176
|
console.log(JSON.stringify(checks, null, 2));
|
|
132
177
|
process.exitCode = checks.some((check)=>!check.ok) ? 1 : 0;
|
|
@@ -142,12 +187,12 @@ async function commandDoctor(cwd, args) {
|
|
|
142
187
|
process.exitCode = failed ? 1 : 0;
|
|
143
188
|
}
|
|
144
189
|
async function startOrAttachSession(cwd, goal) {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
190
|
+
const prepared = await prepareProjectContext(cwd, {
|
|
191
|
+
createRepository: true,
|
|
192
|
+
ensureHeadCommit: false,
|
|
193
|
+
ensureHomeConfig: true
|
|
194
|
+
});
|
|
195
|
+
const { repoRoot, paths } = prepared;
|
|
151
196
|
if (await sessionExists(paths)) {
|
|
152
197
|
try {
|
|
153
198
|
const session = await loadSessionRecord(paths);
|
|
@@ -163,14 +208,27 @@ async function startOrAttachSession(cwd, goal) {
|
|
|
163
208
|
});
|
|
164
209
|
} catch {}
|
|
165
210
|
}
|
|
211
|
+
await ensureStartupReady(repoRoot, paths);
|
|
166
212
|
const config = await loadConfig(paths);
|
|
167
213
|
const runtime = await resolveSessionRuntime(paths);
|
|
168
|
-
const
|
|
214
|
+
const bootstrapCommit = await ensureBootstrapCommit(repoRoot);
|
|
215
|
+
const baseCommit = bootstrapCommit.commit;
|
|
169
216
|
const sessionId = buildSessionId();
|
|
170
217
|
const rpcEndpoint = paths.socketPath;
|
|
171
218
|
await fs.writeFile(paths.commandsFile, "", "utf8");
|
|
172
219
|
const worktrees = await ensureWorktrees(repoRoot, paths, sessionId, config, baseCommit);
|
|
173
220
|
await createSessionRecord(paths, config, runtime, sessionId, baseCommit, worktrees, goal, rpcEndpoint);
|
|
221
|
+
if (prepared.createdRepository) {
|
|
222
|
+
await recordEvent(paths, sessionId, "repo.initialized", {
|
|
223
|
+
repoRoot
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (bootstrapCommit.createdCommit) {
|
|
227
|
+
await recordEvent(paths, sessionId, "repo.bootstrap_committed", {
|
|
228
|
+
commit: bootstrapCommit.commit,
|
|
229
|
+
stagedPaths: bootstrapCommit.stagedPaths
|
|
230
|
+
});
|
|
231
|
+
}
|
|
174
232
|
const pid = spawnDetachedNode(runtime.nodeExecutable, [
|
|
175
233
|
fileURLToPath(import.meta.url),
|
|
176
234
|
"__daemon",
|
|
@@ -239,8 +297,8 @@ async function commandResume(cwd) {
|
|
|
239
297
|
}
|
|
240
298
|
async function commandStart(cwd, args) {
|
|
241
299
|
const goal = getGoal(args);
|
|
242
|
-
const repoRoot = await detectRepoRoot(cwd);
|
|
243
300
|
const socketPath = await startOrAttachSession(cwd, goal);
|
|
301
|
+
const repoRoot = await detectRepoRoot(cwd);
|
|
244
302
|
const paths = resolveAppPaths(repoRoot);
|
|
245
303
|
const session = await loadSessionRecord(paths);
|
|
246
304
|
console.log(`Started Kavi session ${session.id}`);
|
|
@@ -283,6 +341,10 @@ async function commandStatus(cwd, args) {
|
|
|
283
341
|
decisionCounts: {
|
|
284
342
|
total: session.decisions.length
|
|
285
343
|
},
|
|
344
|
+
reviewCounts: {
|
|
345
|
+
open: session.reviewNotes.filter((note)=>note.status === "open").length,
|
|
346
|
+
total: session.reviewNotes.length
|
|
347
|
+
},
|
|
286
348
|
pathClaimCounts: {
|
|
287
349
|
active: session.pathClaims.filter((claim)=>claim.status === "active").length
|
|
288
350
|
},
|
|
@@ -302,6 +364,7 @@ async function commandStatus(cwd, args) {
|
|
|
302
364
|
console.log(`Runtime: node=${payload.runtime.nodeExecutable} codex=${payload.runtime.codexExecutable} claude=${payload.runtime.claudeExecutable}`);
|
|
303
365
|
console.log(`Tasks: total=${payload.taskCounts.total} pending=${payload.taskCounts.pending} running=${payload.taskCounts.running} blocked=${payload.taskCounts.blocked} completed=${payload.taskCounts.completed} failed=${payload.taskCounts.failed}`);
|
|
304
366
|
console.log(`Approvals: pending=${payload.approvalCounts.pending}`);
|
|
367
|
+
console.log(`Reviews: open=${payload.reviewCounts.open} total=${payload.reviewCounts.total}`);
|
|
305
368
|
console.log(`Decisions: total=${payload.decisionCounts.total}`);
|
|
306
369
|
console.log(`Path claims: active=${payload.pathClaimCounts.active}`);
|
|
307
370
|
for (const worktree of payload.worktrees){
|
|
@@ -309,7 +372,7 @@ async function commandStatus(cwd, args) {
|
|
|
309
372
|
}
|
|
310
373
|
}
|
|
311
374
|
async function commandPaths(cwd, args) {
|
|
312
|
-
const repoRoot = await
|
|
375
|
+
const repoRoot = await findRepoRoot(cwd) ?? cwd;
|
|
313
376
|
const paths = resolveAppPaths(repoRoot);
|
|
314
377
|
const runtime = await resolveSessionRuntime(paths);
|
|
315
378
|
const payload = {
|
|
@@ -474,6 +537,20 @@ async function commandTaskOutput(cwd, args) {
|
|
|
474
537
|
}
|
|
475
538
|
console.log("Envelope:");
|
|
476
539
|
console.log(JSON.stringify(artifact.envelope, null, 2));
|
|
540
|
+
console.log("Review Notes:");
|
|
541
|
+
if (artifact.reviewNotes.length === 0) {
|
|
542
|
+
console.log("-");
|
|
543
|
+
} else {
|
|
544
|
+
for (const note of artifact.reviewNotes){
|
|
545
|
+
console.log(`${note.createdAt} | ${note.disposition} | ${note.status} | ${note.filePath}${note.hunkIndex === null ? "" : ` | hunk ${note.hunkIndex + 1}`}`);
|
|
546
|
+
console.log(` comments: ${note.comments.length}`);
|
|
547
|
+
for (const [index, comment] of note.comments.entries()){
|
|
548
|
+
console.log(` ${index === 0 ? "root" : `reply-${index}`}: ${comment.body}`);
|
|
549
|
+
}
|
|
550
|
+
console.log(` landed: ${note.landedAt ?? "-"}`);
|
|
551
|
+
console.log(` follow-ups: ${note.followUpTaskIds.join(", ") || "-"}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
477
554
|
console.log("Raw Output:");
|
|
478
555
|
console.log(artifact.rawOutput ?? "");
|
|
479
556
|
}
|
|
@@ -520,6 +597,31 @@ async function commandClaims(cwd, args) {
|
|
|
520
597
|
console.log(` note: ${claim.note ?? "-"}`);
|
|
521
598
|
}
|
|
522
599
|
}
|
|
600
|
+
async function commandReviews(cwd, args) {
|
|
601
|
+
const { paths } = await requireSession(cwd);
|
|
602
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
603
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
604
|
+
const notes = args.includes("--all") ? [
|
|
605
|
+
...session.reviewNotes
|
|
606
|
+
] : session.reviewNotes.filter((note)=>note.status === "open");
|
|
607
|
+
if (args.includes("--json")) {
|
|
608
|
+
console.log(JSON.stringify(notes, null, 2));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (notes.length === 0) {
|
|
612
|
+
console.log("No review notes recorded.");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
for (const note of notes){
|
|
616
|
+
console.log(`${note.id} | ${note.agent} | ${note.status} | ${note.disposition} | ${note.filePath}${note.hunkIndex === null ? "" : ` | hunk ${note.hunkIndex + 1}`}`);
|
|
617
|
+
console.log(` task: ${note.taskId ?? "-"}`);
|
|
618
|
+
console.log(` updated: ${note.updatedAt}`);
|
|
619
|
+
console.log(` comments: ${note.comments.length}`);
|
|
620
|
+
console.log(` landed: ${note.landedAt ?? "-"}`);
|
|
621
|
+
console.log(` follow-ups: ${note.followUpTaskIds.join(", ") || "-"}`);
|
|
622
|
+
console.log(` body: ${note.body}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
523
625
|
async function commandApprovals(cwd, args) {
|
|
524
626
|
const { paths } = await requireSession(cwd);
|
|
525
627
|
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
@@ -683,6 +785,46 @@ async function commandLand(cwd) {
|
|
|
683
785
|
snapshotCommits: result.snapshotCommits,
|
|
684
786
|
commands: result.commandsRun
|
|
685
787
|
});
|
|
788
|
+
const landedReviewNotes = markReviewNotesLandedForTasks(session, session.tasks.filter((task)=>task.status === "completed").map((task)=>task.id));
|
|
789
|
+
for (const note of landedReviewNotes){
|
|
790
|
+
addDecisionRecord(session, {
|
|
791
|
+
kind: "review",
|
|
792
|
+
agent: note.agent,
|
|
793
|
+
taskId: note.taskId,
|
|
794
|
+
summary: `Marked review note ${note.id} as landed`,
|
|
795
|
+
detail: `Follow-up work for ${note.filePath} is now part of ${targetBranch}.`,
|
|
796
|
+
metadata: {
|
|
797
|
+
reviewNoteId: note.id,
|
|
798
|
+
filePath: note.filePath,
|
|
799
|
+
landedAt: note.landedAt,
|
|
800
|
+
targetBranch
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
await recordEvent(paths, session.id, "review.note_landed", {
|
|
804
|
+
reviewNoteId: note.id,
|
|
805
|
+
taskId: note.taskId,
|
|
806
|
+
followUpTaskIds: note.followUpTaskIds,
|
|
807
|
+
agent: note.agent,
|
|
808
|
+
filePath: note.filePath,
|
|
809
|
+
landedAt: note.landedAt,
|
|
810
|
+
targetBranch
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
await saveSessionRecord(paths, session);
|
|
814
|
+
const artifactTaskIds = [
|
|
815
|
+
...new Set(landedReviewNotes.flatMap((note)=>note.taskId ? [
|
|
816
|
+
note.taskId
|
|
817
|
+
] : []))
|
|
818
|
+
];
|
|
819
|
+
for (const taskId of artifactTaskIds){
|
|
820
|
+
const artifact = await loadTaskArtifact(paths, taskId);
|
|
821
|
+
if (!artifact) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
artifact.reviewNotes = session.reviewNotes.filter((note)=>note.taskId === taskId);
|
|
825
|
+
await saveTaskArtifact(paths, artifact);
|
|
826
|
+
}
|
|
827
|
+
await notifyOperatorSurface(paths, "land.completed");
|
|
686
828
|
console.log(`Landed branches into ${targetBranch}`);
|
|
687
829
|
console.log(`Integration branch: ${result.integrationBranch}`);
|
|
688
830
|
console.log(`Integration worktree: ${result.integrationPath}`);
|
|
@@ -845,6 +987,9 @@ async function main() {
|
|
|
845
987
|
case "claims":
|
|
846
988
|
await commandClaims(cwd, args);
|
|
847
989
|
break;
|
|
990
|
+
case "reviews":
|
|
991
|
+
await commandReviews(cwd, args);
|
|
992
|
+
break;
|
|
848
993
|
case "approvals":
|
|
849
994
|
await commandApprovals(cwd, args);
|
|
850
995
|
break;
|
package/dist/reviews.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { nowIso } from "./paths.js";
|
|
3
|
+
const MAX_REVIEW_NOTES = 200;
|
|
4
|
+
function trimBody(value) {
|
|
5
|
+
return value.trim();
|
|
6
|
+
}
|
|
7
|
+
function summarizeReviewNote(disposition, filePath, hunkHeader, body) {
|
|
8
|
+
const label = disposition[0].toUpperCase() + disposition.slice(1);
|
|
9
|
+
const scope = hunkHeader ? `${filePath} ${hunkHeader}` : filePath;
|
|
10
|
+
const firstLine = trimBody(body).split("\n")[0]?.trim() ?? "";
|
|
11
|
+
return firstLine ? `${label} ${scope}: ${firstLine}` : `${label} ${scope}`;
|
|
12
|
+
}
|
|
13
|
+
function createReviewComment(body) {
|
|
14
|
+
const timestamp = nowIso();
|
|
15
|
+
return {
|
|
16
|
+
id: randomUUID(),
|
|
17
|
+
body: trimBody(body),
|
|
18
|
+
createdAt: timestamp,
|
|
19
|
+
updatedAt: timestamp
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function addReviewNote(session, input) {
|
|
23
|
+
const timestamp = nowIso();
|
|
24
|
+
const note = {
|
|
25
|
+
id: randomUUID(),
|
|
26
|
+
agent: input.agent,
|
|
27
|
+
taskId: input.taskId ?? null,
|
|
28
|
+
filePath: input.filePath,
|
|
29
|
+
hunkIndex: input.hunkIndex ?? null,
|
|
30
|
+
hunkHeader: input.hunkHeader ?? null,
|
|
31
|
+
disposition: input.disposition,
|
|
32
|
+
status: "open",
|
|
33
|
+
summary: summarizeReviewNote(input.disposition, input.filePath, input.hunkHeader ?? null, input.body),
|
|
34
|
+
body: trimBody(input.body),
|
|
35
|
+
comments: [
|
|
36
|
+
createReviewComment(input.body)
|
|
37
|
+
],
|
|
38
|
+
resolvedAt: null,
|
|
39
|
+
landedAt: null,
|
|
40
|
+
followUpTaskIds: [],
|
|
41
|
+
createdAt: timestamp,
|
|
42
|
+
updatedAt: timestamp
|
|
43
|
+
};
|
|
44
|
+
session.reviewNotes = [
|
|
45
|
+
...session.reviewNotes,
|
|
46
|
+
note
|
|
47
|
+
].slice(-MAX_REVIEW_NOTES);
|
|
48
|
+
return note;
|
|
49
|
+
}
|
|
50
|
+
export function reviewNotesForTask(session, taskId) {
|
|
51
|
+
return session.reviewNotes.filter((note)=>note.taskId === taskId);
|
|
52
|
+
}
|
|
53
|
+
export function reviewNotesForPath(session, agent, filePath, hunkIndex) {
|
|
54
|
+
return session.reviewNotes.filter((note)=>{
|
|
55
|
+
if (note.agent !== agent || note.filePath !== filePath) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (hunkIndex === undefined) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return note.hunkIndex === hunkIndex;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export function updateReviewNote(session, noteId, input) {
|
|
65
|
+
const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
|
|
66
|
+
if (!note) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const nextBody = typeof input.body === "string" ? trimBody(input.body) : note.body;
|
|
70
|
+
const nextDisposition = input.disposition ?? note.disposition;
|
|
71
|
+
note.body = nextBody;
|
|
72
|
+
note.disposition = nextDisposition;
|
|
73
|
+
if (note.comments.length === 0) {
|
|
74
|
+
note.comments.push(createReviewComment(nextBody));
|
|
75
|
+
} else if (typeof input.body === "string") {
|
|
76
|
+
note.comments[0] = {
|
|
77
|
+
...note.comments[0],
|
|
78
|
+
body: nextBody,
|
|
79
|
+
updatedAt: nowIso()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
note.summary = summarizeReviewNote(nextDisposition, note.filePath, note.hunkHeader, nextBody);
|
|
83
|
+
note.updatedAt = nowIso();
|
|
84
|
+
return note;
|
|
85
|
+
}
|
|
86
|
+
export function setReviewNoteStatus(session, noteId, status) {
|
|
87
|
+
const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
|
|
88
|
+
if (!note) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
note.status = status;
|
|
92
|
+
note.resolvedAt = status === "resolved" ? nowIso() : null;
|
|
93
|
+
if (status === "open") {
|
|
94
|
+
note.landedAt = null;
|
|
95
|
+
}
|
|
96
|
+
note.updatedAt = nowIso();
|
|
97
|
+
return note;
|
|
98
|
+
}
|
|
99
|
+
export function linkReviewFollowUpTask(session, noteId, taskId) {
|
|
100
|
+
const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
|
|
101
|
+
if (!note) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
note.followUpTaskIds = [
|
|
105
|
+
...new Set([
|
|
106
|
+
...note.followUpTaskIds,
|
|
107
|
+
taskId
|
|
108
|
+
])
|
|
109
|
+
];
|
|
110
|
+
note.updatedAt = nowIso();
|
|
111
|
+
return note;
|
|
112
|
+
}
|
|
113
|
+
export function addReviewReply(session, noteId, body) {
|
|
114
|
+
const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
|
|
115
|
+
if (!note) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
note.comments.push(createReviewComment(body));
|
|
119
|
+
note.status = "open";
|
|
120
|
+
note.resolvedAt = null;
|
|
121
|
+
note.landedAt = null;
|
|
122
|
+
note.updatedAt = nowIso();
|
|
123
|
+
return note;
|
|
124
|
+
}
|
|
125
|
+
export function autoResolveReviewNotesForCompletedTask(session, taskId) {
|
|
126
|
+
const resolved = [];
|
|
127
|
+
for (const note of session.reviewNotes){
|
|
128
|
+
if (note.status !== "open" || !note.followUpTaskIds.includes(taskId)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
note.status = "resolved";
|
|
132
|
+
note.resolvedAt = nowIso();
|
|
133
|
+
note.updatedAt = nowIso();
|
|
134
|
+
resolved.push(note);
|
|
135
|
+
}
|
|
136
|
+
return resolved;
|
|
137
|
+
}
|
|
138
|
+
export function markReviewNotesLandedForTasks(session, taskIds) {
|
|
139
|
+
if (taskIds.length === 0) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const landedTaskIds = new Set(taskIds);
|
|
143
|
+
const landed = [];
|
|
144
|
+
for (const note of session.reviewNotes){
|
|
145
|
+
if (note.status !== "resolved" || note.landedAt !== null) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!note.followUpTaskIds.some((taskId)=>landedTaskIds.has(taskId))) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
note.landedAt = nowIso();
|
|
152
|
+
note.updatedAt = nowIso();
|
|
153
|
+
landed.push(note);
|
|
154
|
+
}
|
|
155
|
+
return landed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
//# sourceURL=reviews.ts
|
package/dist/rpc.js
CHANGED
|
@@ -122,6 +122,42 @@ export async function rpcRecentEvents(paths, limit) {
|
|
|
122
122
|
limit
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
|
+
export async function rpcAddReviewNote(paths, params) {
|
|
126
|
+
await sendRpcRequest(paths, "addReviewNote", {
|
|
127
|
+
agent: params.agent,
|
|
128
|
+
taskId: params.taskId,
|
|
129
|
+
filePath: params.filePath,
|
|
130
|
+
hunkIndex: params.hunkIndex,
|
|
131
|
+
hunkHeader: params.hunkHeader,
|
|
132
|
+
disposition: params.disposition,
|
|
133
|
+
body: params.body
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export async function rpcUpdateReviewNote(paths, params) {
|
|
137
|
+
await sendRpcRequest(paths, "updateReviewNote", {
|
|
138
|
+
noteId: params.noteId,
|
|
139
|
+
body: params.body
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export async function rpcAddReviewReply(paths, params) {
|
|
143
|
+
await sendRpcRequest(paths, "addReviewReply", {
|
|
144
|
+
noteId: params.noteId,
|
|
145
|
+
body: params.body
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
export async function rpcSetReviewNoteStatus(paths, params) {
|
|
149
|
+
await sendRpcRequest(paths, "setReviewNoteStatus", {
|
|
150
|
+
noteId: params.noteId,
|
|
151
|
+
status: params.status
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
export async function rpcEnqueueReviewFollowUp(paths, params) {
|
|
155
|
+
await sendRpcRequest(paths, "enqueueReviewFollowUp", {
|
|
156
|
+
noteId: params.noteId,
|
|
157
|
+
owner: params.owner,
|
|
158
|
+
mode: params.mode
|
|
159
|
+
});
|
|
160
|
+
}
|
|
125
161
|
export async function rpcWorktreeDiff(paths, agent, filePath) {
|
|
126
162
|
return await sendRpcRequest(paths, "worktreeDiff", {
|
|
127
163
|
agent,
|
package/dist/session.js
CHANGED
|
@@ -35,6 +35,7 @@ export async function createSessionRecord(paths, config, runtime, sessionId, bas
|
|
|
35
35
|
peerMessages: [],
|
|
36
36
|
decisions: [],
|
|
37
37
|
pathClaims: [],
|
|
38
|
+
reviewNotes: [],
|
|
38
39
|
agentStatus: {
|
|
39
40
|
codex: initialAgentStatus("codex", "codex-app-server"),
|
|
40
41
|
claude: initialAgentStatus("claude", "claude-print")
|
|
@@ -55,6 +56,30 @@ export async function loadSessionRecord(paths) {
|
|
|
55
56
|
})) : [];
|
|
56
57
|
record.decisions = Array.isArray(record.decisions) ? record.decisions : [];
|
|
57
58
|
record.pathClaims = Array.isArray(record.pathClaims) ? record.pathClaims : [];
|
|
59
|
+
record.reviewNotes = Array.isArray(record.reviewNotes) ? record.reviewNotes.map((note)=>({
|
|
60
|
+
...note,
|
|
61
|
+
body: typeof note.body === "string" ? note.body : "",
|
|
62
|
+
taskId: typeof note.taskId === "string" ? note.taskId : null,
|
|
63
|
+
hunkIndex: typeof note.hunkIndex === "number" ? note.hunkIndex : null,
|
|
64
|
+
hunkHeader: typeof note.hunkHeader === "string" ? note.hunkHeader : null,
|
|
65
|
+
status: note.status === "resolved" ? "resolved" : "open",
|
|
66
|
+
comments: Array.isArray(note.comments) ? note.comments.map((comment)=>({
|
|
67
|
+
id: String(comment.id),
|
|
68
|
+
body: typeof comment.body === "string" ? comment.body : "",
|
|
69
|
+
createdAt: String(comment.createdAt),
|
|
70
|
+
updatedAt: String(comment.updatedAt)
|
|
71
|
+
})) : typeof note.body === "string" && note.body ? [
|
|
72
|
+
{
|
|
73
|
+
id: `${note.id}-root`,
|
|
74
|
+
body: note.body,
|
|
75
|
+
createdAt: typeof note.createdAt === "string" ? note.createdAt : nowIso(),
|
|
76
|
+
updatedAt: typeof note.updatedAt === "string" ? note.updatedAt : nowIso()
|
|
77
|
+
}
|
|
78
|
+
] : [],
|
|
79
|
+
resolvedAt: typeof note.resolvedAt === "string" ? note.resolvedAt : null,
|
|
80
|
+
landedAt: typeof note.landedAt === "string" ? note.landedAt : null,
|
|
81
|
+
followUpTaskIds: Array.isArray(note.followUpTaskIds) ? note.followUpTaskIds.map((item)=>String(item)) : []
|
|
82
|
+
})) : [];
|
|
58
83
|
return record;
|
|
59
84
|
}
|
|
60
85
|
export async function saveSessionRecord(paths, record) {
|
package/dist/task-artifacts.js
CHANGED
|
@@ -9,7 +9,32 @@ function normalizeArtifact(artifact) {
|
|
|
9
9
|
...artifact,
|
|
10
10
|
routeReason: typeof artifact.routeReason === "string" ? artifact.routeReason : null,
|
|
11
11
|
claimedPaths: Array.isArray(artifact.claimedPaths) ? artifact.claimedPaths.map((item)=>String(item)) : [],
|
|
12
|
-
decisionReplay: Array.isArray(artifact.decisionReplay) ? artifact.decisionReplay.map((item)=>String(item)) : []
|
|
12
|
+
decisionReplay: Array.isArray(artifact.decisionReplay) ? artifact.decisionReplay.map((item)=>String(item)) : [],
|
|
13
|
+
reviewNotes: Array.isArray(artifact.reviewNotes) ? artifact.reviewNotes.map((note)=>({
|
|
14
|
+
...note,
|
|
15
|
+
taskId: typeof note.taskId === "string" ? note.taskId : null,
|
|
16
|
+
hunkIndex: typeof note.hunkIndex === "number" ? note.hunkIndex : null,
|
|
17
|
+
hunkHeader: typeof note.hunkHeader === "string" ? note.hunkHeader : null,
|
|
18
|
+
status: note.status === "resolved" ? "resolved" : "open",
|
|
19
|
+
summary: typeof note.summary === "string" ? note.summary : "",
|
|
20
|
+
body: typeof note.body === "string" ? note.body : "",
|
|
21
|
+
comments: Array.isArray(note.comments) ? note.comments.map((comment)=>({
|
|
22
|
+
id: String(comment.id),
|
|
23
|
+
body: typeof comment.body === "string" ? comment.body : "",
|
|
24
|
+
createdAt: String(comment.createdAt),
|
|
25
|
+
updatedAt: String(comment.updatedAt)
|
|
26
|
+
})) : typeof note.body === "string" && note.body ? [
|
|
27
|
+
{
|
|
28
|
+
id: `${note.id}-root`,
|
|
29
|
+
body: note.body,
|
|
30
|
+
createdAt: typeof note.createdAt === "string" ? note.createdAt : artifact.startedAt,
|
|
31
|
+
updatedAt: typeof note.updatedAt === "string" ? note.updatedAt : artifact.finishedAt
|
|
32
|
+
}
|
|
33
|
+
] : [],
|
|
34
|
+
resolvedAt: typeof note.resolvedAt === "string" ? note.resolvedAt : null,
|
|
35
|
+
landedAt: typeof note.landedAt === "string" ? note.landedAt : null,
|
|
36
|
+
followUpTaskIds: Array.isArray(note.followUpTaskIds) ? note.followUpTaskIds.map((item)=>String(item)) : []
|
|
37
|
+
})) : []
|
|
13
38
|
};
|
|
14
39
|
}
|
|
15
40
|
export async function saveTaskArtifact(paths, artifact) {
|