@mandipadk7/kavi 0.1.2 → 0.1.4

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/git.js CHANGED
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { ensureDir } from "./fs.js";
4
4
  import { buildSessionId } from "./paths.js";
5
5
  import { runCommand } from "./process.js";
6
- export async function detectRepoRoot(cwd) {
6
+ export async function findRepoRoot(cwd) {
7
7
  const result = await runCommand("git", [
8
8
  "rev-parse",
9
9
  "--show-toplevel"
@@ -11,10 +11,17 @@ export async function detectRepoRoot(cwd) {
11
11
  cwd
12
12
  });
13
13
  if (result.code !== 0) {
14
- throw new Error(result.stderr.trim() || "Not inside a git repository.");
14
+ return null;
15
15
  }
16
16
  return result.stdout.trim();
17
17
  }
18
+ export async function detectRepoRoot(cwd) {
19
+ const repoRoot = await findRepoRoot(cwd);
20
+ if (!repoRoot) {
21
+ throw new Error("Not inside a git repository.");
22
+ }
23
+ return repoRoot;
24
+ }
18
25
  export async function getHeadCommit(repoRoot) {
19
26
  const result = await runCommand("git", [
20
27
  "rev-parse",
@@ -27,6 +34,98 @@ export async function getHeadCommit(repoRoot) {
27
34
  }
28
35
  return result.stdout.trim();
29
36
  }
37
+ async function initializeRepository(repoRoot) {
38
+ const preferred = await runCommand("git", [
39
+ "init",
40
+ "--initial-branch=main"
41
+ ], {
42
+ cwd: repoRoot
43
+ });
44
+ if (preferred.code === 0) {
45
+ return;
46
+ }
47
+ const fallback = await runCommand("git", [
48
+ "init"
49
+ ], {
50
+ cwd: repoRoot
51
+ });
52
+ if (fallback.code !== 0) {
53
+ throw new Error(fallback.stderr.trim() || preferred.stderr.trim() || `Unable to initialize git in ${repoRoot}.`);
54
+ }
55
+ }
56
+ export async function ensureGitRepository(cwd) {
57
+ const existing = await findRepoRoot(cwd);
58
+ if (existing) {
59
+ return {
60
+ repoRoot: existing,
61
+ createdRepository: false
62
+ };
63
+ }
64
+ await initializeRepository(cwd);
65
+ return {
66
+ repoRoot: await detectRepoRoot(cwd),
67
+ createdRepository: true
68
+ };
69
+ }
70
+ export async function hasHeadCommit(repoRoot) {
71
+ const result = await runCommand("git", [
72
+ "rev-parse",
73
+ "--verify",
74
+ "HEAD"
75
+ ], {
76
+ cwd: repoRoot
77
+ });
78
+ return result.code === 0;
79
+ }
80
+ export async function ensureBootstrapCommit(repoRoot, message = "kavi: bootstrap project") {
81
+ if (await hasHeadCommit(repoRoot)) {
82
+ return {
83
+ createdCommit: false,
84
+ commit: await getHeadCommit(repoRoot),
85
+ stagedPaths: []
86
+ };
87
+ }
88
+ const add = await runCommand("git", [
89
+ "add",
90
+ "-A"
91
+ ], {
92
+ cwd: repoRoot
93
+ });
94
+ if (add.code !== 0) {
95
+ throw new Error(add.stderr.trim() || `Unable to stage bootstrap files in ${repoRoot}.`);
96
+ }
97
+ const staged = await runCommand("git", [
98
+ "diff",
99
+ "--cached",
100
+ "--name-only",
101
+ "--diff-filter=ACMRTUXB"
102
+ ], {
103
+ cwd: repoRoot
104
+ });
105
+ if (staged.code !== 0) {
106
+ throw new Error(staged.stderr.trim() || `Unable to inspect staged bootstrap files in ${repoRoot}.`);
107
+ }
108
+ const commit = await runCommand("git", [
109
+ "-c",
110
+ "user.name=Kavi",
111
+ "-c",
112
+ "user.email=kavi@local.invalid",
113
+ "commit",
114
+ "--allow-empty",
115
+ "-m",
116
+ message
117
+ ], {
118
+ cwd: repoRoot
119
+ });
120
+ if (commit.code !== 0) {
121
+ throw new Error(commit.stderr.trim() || `Unable to create bootstrap commit in ${repoRoot}.`);
122
+ }
123
+ return {
124
+ createdCommit: true,
125
+ commit: await getHeadCommit(repoRoot),
126
+ stagedPaths: parsePathList(staged.stdout)
127
+ };
128
+ }
30
129
  export async function getCurrentBranch(repoRoot) {
31
130
  const result = await runCommand("git", [
32
131
  "branch",
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",
@@ -189,6 +247,7 @@ async function ensureStartupReady(repoRoot, paths) {
189
247
  "node",
190
248
  "codex",
191
249
  "claude",
250
+ "claude-auth",
192
251
  "git-worktree",
193
252
  "codex-app-server",
194
253
  "codex-auth-file"
@@ -239,8 +298,8 @@ async function commandResume(cwd) {
239
298
  }
240
299
  async function commandStart(cwd, args) {
241
300
  const goal = getGoal(args);
242
- const repoRoot = await detectRepoRoot(cwd);
243
301
  const socketPath = await startOrAttachSession(cwd, goal);
302
+ const repoRoot = await detectRepoRoot(cwd);
244
303
  const paths = resolveAppPaths(repoRoot);
245
304
  const session = await loadSessionRecord(paths);
246
305
  console.log(`Started Kavi session ${session.id}`);
@@ -283,6 +342,10 @@ async function commandStatus(cwd, args) {
283
342
  decisionCounts: {
284
343
  total: session.decisions.length
285
344
  },
345
+ reviewCounts: {
346
+ open: session.reviewNotes.filter((note)=>note.status === "open").length,
347
+ total: session.reviewNotes.length
348
+ },
286
349
  pathClaimCounts: {
287
350
  active: session.pathClaims.filter((claim)=>claim.status === "active").length
288
351
  },
@@ -302,6 +365,7 @@ async function commandStatus(cwd, args) {
302
365
  console.log(`Runtime: node=${payload.runtime.nodeExecutable} codex=${payload.runtime.codexExecutable} claude=${payload.runtime.claudeExecutable}`);
303
366
  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
367
  console.log(`Approvals: pending=${payload.approvalCounts.pending}`);
368
+ console.log(`Reviews: open=${payload.reviewCounts.open} total=${payload.reviewCounts.total}`);
305
369
  console.log(`Decisions: total=${payload.decisionCounts.total}`);
306
370
  console.log(`Path claims: active=${payload.pathClaimCounts.active}`);
307
371
  for (const worktree of payload.worktrees){
@@ -309,7 +373,7 @@ async function commandStatus(cwd, args) {
309
373
  }
310
374
  }
311
375
  async function commandPaths(cwd, args) {
312
- const repoRoot = await detectRepoRoot(cwd);
376
+ const repoRoot = await findRepoRoot(cwd) ?? cwd;
313
377
  const paths = resolveAppPaths(repoRoot);
314
378
  const runtime = await resolveSessionRuntime(paths);
315
379
  const payload = {
@@ -474,6 +538,21 @@ async function commandTaskOutput(cwd, args) {
474
538
  }
475
539
  console.log("Envelope:");
476
540
  console.log(JSON.stringify(artifact.envelope, null, 2));
541
+ console.log("Review Notes:");
542
+ if (artifact.reviewNotes.length === 0) {
543
+ console.log("-");
544
+ } else {
545
+ for (const note of artifact.reviewNotes){
546
+ console.log(`${note.createdAt} | ${note.disposition} | ${note.status} | ${note.filePath}${note.hunkIndex === null ? "" : ` | hunk ${note.hunkIndex + 1}`}`);
547
+ console.log(` assignee: ${note.assignee ?? "-"}`);
548
+ console.log(` comments: ${note.comments.length}`);
549
+ for (const [index, comment] of note.comments.entries()){
550
+ console.log(` ${index === 0 ? "root" : `reply-${index}`}: ${comment.body}`);
551
+ }
552
+ console.log(` landed: ${note.landedAt ?? "-"}`);
553
+ console.log(` follow-ups: ${note.followUpTaskIds.join(", ") || "-"}`);
554
+ }
555
+ }
477
556
  console.log("Raw Output:");
478
557
  console.log(artifact.rawOutput ?? "");
479
558
  }
@@ -520,6 +599,32 @@ async function commandClaims(cwd, args) {
520
599
  console.log(` note: ${claim.note ?? "-"}`);
521
600
  }
522
601
  }
602
+ async function commandReviews(cwd, args) {
603
+ const { paths } = await requireSession(cwd);
604
+ const rpcSnapshot = await tryRpcSnapshot(paths);
605
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
606
+ const notes = args.includes("--all") ? [
607
+ ...session.reviewNotes
608
+ ] : session.reviewNotes.filter((note)=>note.status === "open");
609
+ if (args.includes("--json")) {
610
+ console.log(JSON.stringify(notes, null, 2));
611
+ return;
612
+ }
613
+ if (notes.length === 0) {
614
+ console.log("No review notes recorded.");
615
+ return;
616
+ }
617
+ for (const note of notes){
618
+ console.log(`${note.id} | ${note.agent} | ${note.status} | ${note.disposition} | ${note.filePath}${note.hunkIndex === null ? "" : ` | hunk ${note.hunkIndex + 1}`}`);
619
+ console.log(` task: ${note.taskId ?? "-"}`);
620
+ console.log(` assignee: ${note.assignee ?? "-"}`);
621
+ console.log(` updated: ${note.updatedAt}`);
622
+ console.log(` comments: ${note.comments.length}`);
623
+ console.log(` landed: ${note.landedAt ?? "-"}`);
624
+ console.log(` follow-ups: ${note.followUpTaskIds.join(", ") || "-"}`);
625
+ console.log(` body: ${note.body}`);
626
+ }
627
+ }
523
628
  async function commandApprovals(cwd, args) {
524
629
  const { paths } = await requireSession(cwd);
525
630
  const rpcSnapshot = await tryRpcSnapshot(paths);
@@ -683,6 +788,46 @@ async function commandLand(cwd) {
683
788
  snapshotCommits: result.snapshotCommits,
684
789
  commands: result.commandsRun
685
790
  });
791
+ const landedReviewNotes = markReviewNotesLandedForTasks(session, session.tasks.filter((task)=>task.status === "completed").map((task)=>task.id));
792
+ for (const note of landedReviewNotes){
793
+ addDecisionRecord(session, {
794
+ kind: "review",
795
+ agent: note.agent,
796
+ taskId: note.taskId,
797
+ summary: `Marked review note ${note.id} as landed`,
798
+ detail: `Follow-up work for ${note.filePath} is now part of ${targetBranch}.`,
799
+ metadata: {
800
+ reviewNoteId: note.id,
801
+ filePath: note.filePath,
802
+ landedAt: note.landedAt,
803
+ targetBranch
804
+ }
805
+ });
806
+ await recordEvent(paths, session.id, "review.note_landed", {
807
+ reviewNoteId: note.id,
808
+ taskId: note.taskId,
809
+ followUpTaskIds: note.followUpTaskIds,
810
+ agent: note.agent,
811
+ filePath: note.filePath,
812
+ landedAt: note.landedAt,
813
+ targetBranch
814
+ });
815
+ }
816
+ await saveSessionRecord(paths, session);
817
+ const artifactTaskIds = [
818
+ ...new Set(landedReviewNotes.flatMap((note)=>note.taskId ? [
819
+ note.taskId
820
+ ] : []))
821
+ ];
822
+ for (const taskId of artifactTaskIds){
823
+ const artifact = await loadTaskArtifact(paths, taskId);
824
+ if (!artifact) {
825
+ continue;
826
+ }
827
+ artifact.reviewNotes = session.reviewNotes.filter((note)=>note.taskId === taskId);
828
+ await saveTaskArtifact(paths, artifact);
829
+ }
830
+ await notifyOperatorSurface(paths, "land.completed");
686
831
  console.log(`Landed branches into ${targetBranch}`);
687
832
  console.log(`Integration branch: ${result.integrationBranch}`);
688
833
  console.log(`Integration worktree: ${result.integrationPath}`);
@@ -845,6 +990,9 @@ async function main() {
845
990
  case "claims":
846
991
  await commandClaims(cwd, args);
847
992
  break;
993
+ case "reviews":
994
+ await commandReviews(cwd, args);
995
+ break;
848
996
  case "approvals":
849
997
  await commandApprovals(cwd, args);
850
998
  break;
@@ -0,0 +1,188 @@
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 = reviewDispositionSummaryLabel(disposition);
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 reviewDispositionSummaryLabel(disposition) {
14
+ switch(disposition){
15
+ case "accepted_risk":
16
+ return "Accepted Risk";
17
+ case "wont_fix":
18
+ return "Won't Fix";
19
+ default:
20
+ return disposition[0].toUpperCase() + disposition.slice(1);
21
+ }
22
+ }
23
+ function createReviewComment(body) {
24
+ const timestamp = nowIso();
25
+ return {
26
+ id: randomUUID(),
27
+ body: trimBody(body),
28
+ createdAt: timestamp,
29
+ updatedAt: timestamp
30
+ };
31
+ }
32
+ export function addReviewNote(session, input) {
33
+ const timestamp = nowIso();
34
+ const note = {
35
+ id: randomUUID(),
36
+ agent: input.agent,
37
+ assignee: input.assignee ?? input.agent,
38
+ taskId: input.taskId ?? null,
39
+ filePath: input.filePath,
40
+ hunkIndex: input.hunkIndex ?? null,
41
+ hunkHeader: input.hunkHeader ?? null,
42
+ disposition: input.disposition,
43
+ status: "open",
44
+ summary: summarizeReviewNote(input.disposition, input.filePath, input.hunkHeader ?? null, input.body),
45
+ body: trimBody(input.body),
46
+ comments: [
47
+ createReviewComment(input.body)
48
+ ],
49
+ resolvedAt: null,
50
+ landedAt: null,
51
+ followUpTaskIds: [],
52
+ createdAt: timestamp,
53
+ updatedAt: timestamp
54
+ };
55
+ session.reviewNotes = [
56
+ ...session.reviewNotes,
57
+ note
58
+ ].slice(-MAX_REVIEW_NOTES);
59
+ return note;
60
+ }
61
+ export function reviewNotesForTask(session, taskId) {
62
+ return session.reviewNotes.filter((note)=>note.taskId === taskId);
63
+ }
64
+ export function reviewNotesForPath(session, agent, filePath, hunkIndex) {
65
+ return session.reviewNotes.filter((note)=>{
66
+ if (note.agent !== agent || note.filePath !== filePath) {
67
+ return false;
68
+ }
69
+ if (hunkIndex === undefined) {
70
+ return true;
71
+ }
72
+ return note.hunkIndex === hunkIndex;
73
+ });
74
+ }
75
+ export function updateReviewNote(session, noteId, input) {
76
+ const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
77
+ if (!note) {
78
+ return null;
79
+ }
80
+ const nextBody = typeof input.body === "string" ? trimBody(input.body) : note.body;
81
+ const nextDisposition = input.disposition ?? note.disposition;
82
+ const nextAssignee = input.assignee === undefined ? note.assignee : input.assignee;
83
+ note.body = nextBody;
84
+ note.disposition = nextDisposition;
85
+ note.assignee = nextAssignee;
86
+ if (note.comments.length === 0) {
87
+ note.comments.push(createReviewComment(nextBody));
88
+ } else if (typeof input.body === "string") {
89
+ note.comments[0] = {
90
+ ...note.comments[0],
91
+ body: nextBody,
92
+ updatedAt: nowIso()
93
+ };
94
+ }
95
+ note.summary = summarizeReviewNote(nextDisposition, note.filePath, note.hunkHeader, nextBody);
96
+ note.updatedAt = nowIso();
97
+ return note;
98
+ }
99
+ export function setReviewNoteStatus(session, noteId, status) {
100
+ const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
101
+ if (!note) {
102
+ return null;
103
+ }
104
+ note.status = status;
105
+ note.resolvedAt = status === "resolved" ? nowIso() : null;
106
+ if (status === "open") {
107
+ note.landedAt = null;
108
+ }
109
+ note.updatedAt = nowIso();
110
+ return note;
111
+ }
112
+ export function linkReviewFollowUpTask(session, noteId, taskId, assignee) {
113
+ const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
114
+ if (!note) {
115
+ return null;
116
+ }
117
+ note.followUpTaskIds = [
118
+ ...new Set([
119
+ ...note.followUpTaskIds,
120
+ taskId
121
+ ])
122
+ ];
123
+ if (assignee !== undefined) {
124
+ note.assignee = assignee;
125
+ }
126
+ note.updatedAt = nowIso();
127
+ return note;
128
+ }
129
+ export function addReviewReply(session, noteId, body) {
130
+ const note = session.reviewNotes.find((item)=>item.id === noteId) ?? null;
131
+ if (!note) {
132
+ return null;
133
+ }
134
+ note.comments.push(createReviewComment(body));
135
+ note.status = "open";
136
+ note.resolvedAt = null;
137
+ note.landedAt = null;
138
+ note.updatedAt = nowIso();
139
+ return note;
140
+ }
141
+ export function cycleReviewAssignee(current, noteAgent) {
142
+ const sequence = [
143
+ noteAgent,
144
+ noteAgent === "codex" ? "claude" : "codex",
145
+ "operator",
146
+ null
147
+ ];
148
+ const index = sequence.findIndex((item)=>item === current);
149
+ if (index === -1) {
150
+ return noteAgent;
151
+ }
152
+ return sequence[(index + 1) % sequence.length] ?? null;
153
+ }
154
+ export function autoResolveReviewNotesForCompletedTask(session, taskId) {
155
+ const resolved = [];
156
+ for (const note of session.reviewNotes){
157
+ if (note.status !== "open" || !note.followUpTaskIds.includes(taskId)) {
158
+ continue;
159
+ }
160
+ note.status = "resolved";
161
+ note.resolvedAt = nowIso();
162
+ note.updatedAt = nowIso();
163
+ resolved.push(note);
164
+ }
165
+ return resolved;
166
+ }
167
+ export function markReviewNotesLandedForTasks(session, taskIds) {
168
+ if (taskIds.length === 0) {
169
+ return [];
170
+ }
171
+ const landedTaskIds = new Set(taskIds);
172
+ const landed = [];
173
+ for (const note of session.reviewNotes){
174
+ if (note.status !== "resolved" || note.landedAt !== null) {
175
+ continue;
176
+ }
177
+ if (!note.followUpTaskIds.some((taskId)=>landedTaskIds.has(taskId))) {
178
+ continue;
179
+ }
180
+ note.landedAt = nowIso();
181
+ note.updatedAt = nowIso();
182
+ landed.push(note);
183
+ }
184
+ return landed;
185
+ }
186
+
187
+
188
+ //# sourceURL=reviews.ts