@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/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, findOverlappingWorktreePaths, getHeadCommit, landBranches, resolveTargetBranch } from "./git.js";
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 commandInit(cwd, args) {
115
- const repoRoot = await detectRepoRoot(cwd);
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
- await createGitignoreEntries(repoRoot);
119
- if (args.includes("--home")) {
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
- console.log(`Initialized Kavi project scaffold in ${paths.kaviDir}`);
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 repoRoot = await detectRepoRoot(cwd);
127
- const paths = resolveAppPaths(repoRoot);
128
- await ensureProjectScaffold(paths);
129
- const checks = await runDoctor(repoRoot, paths);
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 repoRoot = await detectRepoRoot(cwd);
146
- const paths = resolveAppPaths(repoRoot);
147
- await ensureProjectScaffold(paths);
148
- await createGitignoreEntries(repoRoot);
149
- await ensureHomeConfig(paths);
150
- await ensureStartupReady(repoRoot, paths);
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 baseCommit = await getHeadCommit(repoRoot);
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 detectRepoRoot(cwd);
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;
@@ -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) {
@@ -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) {