@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/README.md CHANGED
@@ -3,11 +3,12 @@
3
3
  Kavi is a local terminal control plane for managed Codex and Claude collaboration.
4
4
 
5
5
  Current capabilities:
6
- - `kavi init`: create repo-local `.kavi` config, prompt files, and ignore rules.
6
+ - `kavi init`: create repo-local `.kavi` config, prompt files, ignore rules, and bootstrap git if the folder is not already a repository.
7
7
  - `kavi init --home`: also scaffold the user-local config file used for binary overrides.
8
+ - `kavi init --no-commit`: skip the bootstrap commit and let `kavi open` or `kavi start` create the first base commit later.
8
9
  - `kavi doctor`: verify Node, Codex, Claude, git worktree support, and local readiness.
9
10
  - `kavi start`: start a managed session without attaching the TUI.
10
- - `kavi open`: create a managed session with separate Codex and Claude worktrees and open the full-screen operator console.
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.
11
12
  - `kavi resume`: reopen the operator console for the current repo session.
12
13
  - `kavi status`: inspect session health and task counts from any terminal.
13
14
  - `kavi paths`: inspect resolved repo-local, user-local, worktree, and runtime paths.
@@ -16,6 +17,7 @@ Current capabilities:
16
17
  - `kavi task-output`: inspect the normalized envelope and raw output for a completed task.
17
18
  - `kavi decisions`: inspect the persisted routing, approval, task, and integration decisions.
18
19
  - `kavi claims`: inspect active or historical path claims.
20
+ - `kavi reviews`: inspect persisted operator review threads and linked follow-up tasks.
19
21
  - `kavi approvals`: inspect the approval inbox.
20
22
  - `kavi approve` and `kavi deny`: resolve a pending approval request, optionally with `--remember`.
21
23
  - `kavi events`: inspect recent daemon and task events.
@@ -29,68 +31,19 @@ Runtime model:
29
31
  - User-local runtime overrides live in `~/.config/kavi/config.toml` and can point Kavi at custom `node`, `codex`, and `claude` binaries.
30
32
  - The operator surface talks to the daemon over a local control socket under the machine-local state root.
31
33
  - SQLite event mirroring is opt-in with `KAVI_ENABLE_SQLITE_HISTORY=1`; the default event log is JSONL under `.kavi/state/events.jsonl`.
32
- - The operator console exposes a task board, dual agent lanes, a live inspector pane, approval actions, inline task composition, and worktree diff review with file and hunk navigation.
33
-
34
- Development:
35
-
36
- ```bash
37
- node bin/kavi.js init --home
38
- node bin/kavi.js doctor --json
39
- node bin/kavi.js paths
40
- node bin/kavi.js start --goal "Build the auth backend"
41
- node bin/kavi.js status
42
- node bin/kavi.js task --agent auto "Design the dashboard shell"
43
- node bin/kavi.js tasks
44
- node bin/kavi.js task-output latest
45
- node bin/kavi.js decisions
46
- node bin/kavi.js claims
47
- node bin/kavi.js approvals
48
- node bin/kavi.js open
49
- npm test
50
- ```
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
+ - 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.
51
36
 
52
37
  Notes:
38
+ - `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.
53
39
  - Codex runs through `codex app-server` in managed mode, so Codex-side approvals now land in the same Kavi inbox as Claude hook approvals.
54
40
  - Claude still runs through the installed `claude` CLI with Kavi-managed hooks and approval decisions.
41
+ - `kavi doctor` now checks Claude auth readiness with `claude auth status`, and startup blocks if Claude is installed but not authenticated.
55
42
  - 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.
56
43
  - The socket is machine-local rather than repo-local to avoid Unix socket path-length issues in deep repos and temp directories.
57
- - 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, `y/n` resolve approvals, and `c` opens the inline task composer.
58
-
59
- Local install options:
60
-
61
- ```bash
62
- # Run directly from the repo
63
- node bin/kavi.js help
64
-
65
- # Symlink into ~/.local/bin
66
- ./scripts/install-local.sh
67
-
68
- # Or use npm link from this repo
69
- npm link
70
- ```
71
-
72
- Publishing for testers:
73
-
74
- ```bash
75
- # interactive publish flow: prompts for version, defaults to the next patch,
76
- # runs release checks, then publishes the beta tag
77
- npm run publish
78
-
79
- # authenticate once
80
- npm login
81
-
82
- # verify the package before publish
83
- npm run release:check
84
-
85
- # prompt for version and publish beta explicitly
86
- npm run publish:beta
87
-
88
- # prompt for version and publish stable
89
- npm run publish:latest
90
-
91
- # test the publish flow without sending anything to npm
92
- npm run publish -- --dry-run
93
- ```
44
+ - 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.
45
+ - 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
+ - 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.
94
47
 
95
48
  Install commands for testers:
96
49
 
@@ -102,13 +55,6 @@ npm install -g @mandipadk7/kavi@beta
102
55
  npx @mandipadk7/kavi@beta help
103
56
  ```
104
57
 
105
- Notes on publish:
106
- - The package name is scoped as `@mandipadk7/kavi` to match the npm user `mandipadk7`.
107
- - The publish flow now builds a compiled `dist/` directory first, and the installed CLI prefers `dist/main.js`. Source mode remains available for local development.
108
- - Hook commands now invoke the compiled entrypoint directly when `dist/` is present, and only use `--experimental-strip-types` in source mode.
109
- - The interactive publish script strips `repository`, `homepage`, and `bugs` from the packaged `package.json`, so the npm page uses the bundled `README.md` instead of private GitHub links.
110
- - `prepublishOnly` runs the release checks automatically during publish.
111
-
112
58
  User-local config example:
113
59
 
114
60
  ```toml
package/dist/config.js CHANGED
@@ -10,6 +10,8 @@ message_limit = 6
10
10
  [routing]
11
11
  frontend_keywords = ["frontend", "ui", "ux", "design", "copy", "react", "css", "html"]
12
12
  backend_keywords = ["backend", "api", "server", "db", "schema", "migration", "auth", "test"]
13
+ codex_paths = []
14
+ claude_paths = []
13
15
 
14
16
  [agents.codex]
15
17
  role = "planning-backend"
@@ -77,7 +79,9 @@ export function defaultConfig() {
77
79
  "migration",
78
80
  "auth",
79
81
  "test"
80
- ]
82
+ ],
83
+ codexPaths: [],
84
+ claudePaths: []
81
85
  },
82
86
  agents: {
83
87
  codex: {
@@ -140,7 +144,9 @@ export async function loadConfig(paths) {
140
144
  messageLimit: asNumber(parsed.message_limit, 6),
141
145
  routing: {
142
146
  frontendKeywords: asStringArray(routing.frontend_keywords, defaultConfig().routing.frontendKeywords),
143
- backendKeywords: asStringArray(routing.backend_keywords, defaultConfig().routing.backendKeywords)
147
+ backendKeywords: asStringArray(routing.backend_keywords, defaultConfig().routing.backendKeywords),
148
+ codexPaths: asStringArray(routing.codex_paths, defaultConfig().routing.codexPaths),
149
+ claudePaths: asStringArray(routing.claude_paths, defaultConfig().routing.claudePaths)
144
150
  },
145
151
  agents: {
146
152
  codex: {
package/dist/daemon.js CHANGED
@@ -9,9 +9,22 @@ import { listApprovalRequests, resolveApprovalRequest } from "./approvals.js";
9
9
  import { addDecisionRecord, upsertPathClaim } from "./decision-ledger.js";
10
10
  import { getWorktreeDiffReview, listWorktreeChangedPaths } from "./git.js";
11
11
  import { nowIso } from "./paths.js";
12
+ import { addReviewReply, addReviewNote, autoResolveReviewNotesForCompletedTask, linkReviewFollowUpTask, reviewNotesForTask, setReviewNoteStatus, updateReviewNote } from "./reviews.js";
12
13
  import { buildAdHocTask, buildKickoffTasks } from "./router.js";
13
14
  import { loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord } from "./session.js";
14
15
  import { loadTaskArtifact, saveTaskArtifact } from "./task-artifacts.js";
16
+ function normalizeReviewDisposition(value) {
17
+ if (value === "approve" || value === "concern" || value === "question" || value === "accepted_risk" || value === "wont_fix") {
18
+ return value;
19
+ }
20
+ return "note";
21
+ }
22
+ function normalizeReviewAssignee(value) {
23
+ if (value === "codex" || value === "claude" || value === "operator") {
24
+ return value;
25
+ }
26
+ return null;
27
+ }
15
28
  export class KaviDaemon {
16
29
  paths;
17
30
  session;
@@ -191,6 +204,41 @@ export class KaviDaemon {
191
204
  ok: true
192
205
  }
193
206
  };
207
+ case "addReviewNote":
208
+ await this.addReviewNoteFromRpc(params);
209
+ return {
210
+ result: {
211
+ ok: true
212
+ }
213
+ };
214
+ case "updateReviewNote":
215
+ await this.updateReviewNoteFromRpc(params);
216
+ return {
217
+ result: {
218
+ ok: true
219
+ }
220
+ };
221
+ case "addReviewReply":
222
+ await this.addReviewReplyFromRpc(params);
223
+ return {
224
+ result: {
225
+ ok: true
226
+ }
227
+ };
228
+ case "setReviewNoteStatus":
229
+ await this.setReviewNoteStatusFromRpc(params);
230
+ return {
231
+ result: {
232
+ ok: true
233
+ }
234
+ };
235
+ case "enqueueReviewFollowUp":
236
+ await this.enqueueReviewFollowUpFromRpc(params);
237
+ return {
238
+ result: {
239
+ ok: true
240
+ }
241
+ };
194
242
  default:
195
243
  throw new Error(`Unknown RPC method: ${method}`);
196
244
  }
@@ -351,6 +399,287 @@ export class KaviDaemon {
351
399
  }
352
400
  return await getWorktreeDiffReview(agent, worktree.path, this.session.baseCommit, filePath);
353
401
  }
402
+ async addReviewNoteFromRpc(params) {
403
+ await this.runMutation(async ()=>{
404
+ const agent = params.agent === "claude" ? "claude" : "codex";
405
+ const filePath = typeof params.filePath === "string" ? params.filePath.trim() : "";
406
+ const disposition = normalizeReviewDisposition(params.disposition);
407
+ const assignee = normalizeReviewAssignee(params.assignee) ?? agent;
408
+ const body = typeof params.body === "string" ? params.body.trim() : "";
409
+ const taskId = typeof params.taskId === "string" ? params.taskId : null;
410
+ const hunkIndex = typeof params.hunkIndex === "number" ? params.hunkIndex : null;
411
+ const hunkHeader = typeof params.hunkHeader === "string" ? params.hunkHeader : null;
412
+ if (!filePath) {
413
+ throw new Error("addReviewNote requires a filePath.");
414
+ }
415
+ if (!body) {
416
+ throw new Error("addReviewNote requires a note body.");
417
+ }
418
+ const note = addReviewNote(this.session, {
419
+ agent,
420
+ assignee,
421
+ taskId,
422
+ filePath,
423
+ hunkIndex,
424
+ hunkHeader,
425
+ disposition,
426
+ body
427
+ });
428
+ addDecisionRecord(this.session, {
429
+ kind: "review",
430
+ agent,
431
+ taskId,
432
+ summary: note.summary,
433
+ detail: note.body,
434
+ metadata: {
435
+ filePath: note.filePath,
436
+ hunkIndex: note.hunkIndex,
437
+ hunkHeader: note.hunkHeader,
438
+ disposition: note.disposition,
439
+ assignee: note.assignee,
440
+ reviewNoteId: note.id
441
+ }
442
+ });
443
+ await saveSessionRecord(this.paths, this.session);
444
+ if (taskId) {
445
+ await this.refreshTaskArtifactReviewNotes(taskId);
446
+ }
447
+ await recordEvent(this.paths, this.session.id, "review.note_added", {
448
+ reviewNoteId: note.id,
449
+ agent: note.agent,
450
+ taskId: note.taskId,
451
+ filePath: note.filePath,
452
+ hunkIndex: note.hunkIndex,
453
+ disposition: note.disposition,
454
+ assignee: note.assignee
455
+ });
456
+ await this.publishSnapshot("review.note_added");
457
+ });
458
+ }
459
+ async updateReviewNoteFromRpc(params) {
460
+ await this.runMutation(async ()=>{
461
+ const noteId = typeof params.noteId === "string" ? params.noteId : "";
462
+ const body = typeof params.body === "string" ? params.body.trim() : undefined;
463
+ const disposition = params.disposition === undefined ? undefined : normalizeReviewDisposition(params.disposition);
464
+ const assignee = params.assignee === undefined ? undefined : normalizeReviewAssignee(params.assignee);
465
+ if (!noteId) {
466
+ throw new Error("updateReviewNote requires a noteId.");
467
+ }
468
+ if ((body === undefined || body.length === 0) && disposition === undefined && assignee === undefined) {
469
+ throw new Error("updateReviewNote requires at least one body, disposition, or assignee change.");
470
+ }
471
+ if (body !== undefined && body.length === 0) {
472
+ throw new Error("updateReviewNote requires a non-empty note body.");
473
+ }
474
+ const note = updateReviewNote(this.session, noteId, {
475
+ ...body !== undefined ? {
476
+ body
477
+ } : {},
478
+ ...disposition !== undefined ? {
479
+ disposition
480
+ } : {},
481
+ ...assignee !== undefined ? {
482
+ assignee
483
+ } : {}
484
+ });
485
+ if (!note) {
486
+ throw new Error(`Review note ${noteId} was not found.`);
487
+ }
488
+ addDecisionRecord(this.session, {
489
+ kind: "review",
490
+ agent: note.agent,
491
+ taskId: note.taskId,
492
+ summary: `Edited review note ${note.id}`,
493
+ detail: note.body,
494
+ metadata: {
495
+ reviewNoteId: note.id,
496
+ filePath: note.filePath,
497
+ hunkIndex: note.hunkIndex,
498
+ disposition: note.disposition,
499
+ assignee: note.assignee
500
+ }
501
+ });
502
+ await saveSessionRecord(this.paths, this.session);
503
+ if (note.taskId) {
504
+ await this.refreshTaskArtifactReviewNotes(note.taskId);
505
+ }
506
+ await recordEvent(this.paths, this.session.id, "review.note_updated", {
507
+ reviewNoteId: note.id,
508
+ taskId: note.taskId,
509
+ agent: note.agent,
510
+ filePath: note.filePath,
511
+ disposition: note.disposition,
512
+ assignee: note.assignee
513
+ });
514
+ await this.publishSnapshot("review.note_updated");
515
+ });
516
+ }
517
+ async addReviewReplyFromRpc(params) {
518
+ await this.runMutation(async ()=>{
519
+ const noteId = typeof params.noteId === "string" ? params.noteId : "";
520
+ const body = typeof params.body === "string" ? params.body.trim() : "";
521
+ if (!noteId) {
522
+ throw new Error("addReviewReply requires a noteId.");
523
+ }
524
+ if (!body) {
525
+ throw new Error("addReviewReply requires a reply body.");
526
+ }
527
+ const note = addReviewReply(this.session, noteId, body);
528
+ if (!note) {
529
+ throw new Error(`Review note ${noteId} was not found.`);
530
+ }
531
+ addDecisionRecord(this.session, {
532
+ kind: "review",
533
+ agent: note.agent,
534
+ taskId: note.taskId,
535
+ summary: `Replied to review note ${note.id}`,
536
+ detail: body,
537
+ metadata: {
538
+ reviewNoteId: note.id,
539
+ filePath: note.filePath,
540
+ hunkIndex: note.hunkIndex,
541
+ replyCount: note.comments.length
542
+ }
543
+ });
544
+ await saveSessionRecord(this.paths, this.session);
545
+ if (note.taskId) {
546
+ await this.refreshTaskArtifactReviewNotes(note.taskId);
547
+ }
548
+ await recordEvent(this.paths, this.session.id, "review.reply_added", {
549
+ reviewNoteId: note.id,
550
+ taskId: note.taskId,
551
+ agent: note.agent,
552
+ filePath: note.filePath,
553
+ replyCount: note.comments.length
554
+ });
555
+ await this.publishSnapshot("review.reply_added");
556
+ });
557
+ }
558
+ async setReviewNoteStatusFromRpc(params) {
559
+ await this.runMutation(async ()=>{
560
+ const noteId = typeof params.noteId === "string" ? params.noteId : "";
561
+ const status = params.status === "resolved" ? "resolved" : "open";
562
+ if (!noteId) {
563
+ throw new Error("setReviewNoteStatus requires a noteId.");
564
+ }
565
+ const note = setReviewNoteStatus(this.session, noteId, status);
566
+ if (!note) {
567
+ throw new Error(`Review note ${noteId} was not found.`);
568
+ }
569
+ addDecisionRecord(this.session, {
570
+ kind: "review",
571
+ agent: note.agent,
572
+ taskId: note.taskId,
573
+ summary: `${status === "resolved" ? "Resolved" : "Reopened"} review note ${note.id}`,
574
+ detail: note.summary,
575
+ metadata: {
576
+ reviewNoteId: note.id,
577
+ status,
578
+ filePath: note.filePath,
579
+ hunkIndex: note.hunkIndex
580
+ }
581
+ });
582
+ await saveSessionRecord(this.paths, this.session);
583
+ if (note.taskId) {
584
+ await this.refreshTaskArtifactReviewNotes(note.taskId);
585
+ }
586
+ await recordEvent(this.paths, this.session.id, "review.note_status_changed", {
587
+ reviewNoteId: note.id,
588
+ taskId: note.taskId,
589
+ agent: note.agent,
590
+ filePath: note.filePath,
591
+ status: note.status
592
+ });
593
+ await this.publishSnapshot("review.note_status_changed");
594
+ });
595
+ }
596
+ async enqueueReviewFollowUpFromRpc(params) {
597
+ await this.runMutation(async ()=>{
598
+ const noteId = typeof params.noteId === "string" ? params.noteId : "";
599
+ const owner = params.owner === "claude" ? "claude" : "codex";
600
+ const mode = params.mode === "handoff" ? "handoff" : "fix";
601
+ if (!noteId) {
602
+ throw new Error("enqueueReviewFollowUp requires a noteId.");
603
+ }
604
+ const note = this.session.reviewNotes.find((item)=>item.id === noteId) ?? null;
605
+ if (!note) {
606
+ throw new Error(`Review note ${noteId} was not found.`);
607
+ }
608
+ const taskId = `task-review-${Date.now()}`;
609
+ const scope = note.hunkHeader ? `${note.filePath} ${note.hunkHeader}` : note.filePath;
610
+ const promptLines = [
611
+ `${mode === "handoff" ? "Handle a review handoff" : "Address a review note"} for ${scope}.`,
612
+ `Disposition: ${note.disposition}.`,
613
+ `Review note: ${note.body}`
614
+ ];
615
+ if (note.taskId) {
616
+ promptLines.push(`Originating task: ${note.taskId}.`);
617
+ }
618
+ if (mode === "handoff") {
619
+ promptLines.push(`This was handed off from ${note.agent} work to ${owner}.`);
620
+ }
621
+ promptLines.push(`Focus the change in ${note.filePath} and update the managed worktree accordingly.`);
622
+ const task = buildAdHocTask(owner, promptLines.join(" "), taskId, {
623
+ routeReason: mode === "handoff" ? `Operator handed off review note ${note.id} to ${owner}.` : `Operator created a follow-up task from review note ${note.id}.`,
624
+ claimedPaths: [
625
+ note.filePath
626
+ ]
627
+ });
628
+ this.session.tasks.push(task);
629
+ linkReviewFollowUpTask(this.session, note.id, taskId, owner);
630
+ upsertPathClaim(this.session, {
631
+ taskId,
632
+ agent: owner,
633
+ source: "route",
634
+ paths: task.claimedPaths,
635
+ note: task.routeReason
636
+ });
637
+ addDecisionRecord(this.session, {
638
+ kind: "review",
639
+ agent: owner,
640
+ taskId,
641
+ summary: `Queued ${mode} follow-up from review note ${note.id}`,
642
+ detail: note.body,
643
+ metadata: {
644
+ reviewNoteId: note.id,
645
+ owner,
646
+ mode,
647
+ filePath: note.filePath,
648
+ sourceAgent: note.agent,
649
+ assignee: owner
650
+ }
651
+ });
652
+ await saveSessionRecord(this.paths, this.session);
653
+ if (note.taskId) {
654
+ await this.refreshTaskArtifactReviewNotes(note.taskId);
655
+ }
656
+ await recordEvent(this.paths, this.session.id, "review.followup_queued", {
657
+ reviewNoteId: note.id,
658
+ followUpTaskId: taskId,
659
+ owner,
660
+ mode,
661
+ filePath: note.filePath,
662
+ assignee: owner
663
+ });
664
+ await this.publishSnapshot("review.followup_queued");
665
+ });
666
+ }
667
+ async refreshTaskArtifactReviewNotes(taskId) {
668
+ const artifact = await loadTaskArtifact(this.paths, taskId);
669
+ if (!artifact) {
670
+ return;
671
+ }
672
+ artifact.reviewNotes = reviewNotesForTask(this.session, taskId);
673
+ await saveTaskArtifact(this.paths, artifact);
674
+ }
675
+ async refreshReviewArtifactsForNotes(notes) {
676
+ const taskIds = [
677
+ ...new Set(notes.map((note)=>note.taskId).filter((value)=>Boolean(value)))
678
+ ];
679
+ for (const taskId of taskIds){
680
+ await this.refreshTaskArtifactReviewNotes(taskId);
681
+ }
682
+ }
354
683
  async publishSnapshot(reason) {
355
684
  if (this.subscribers.size === 0) {
356
685
  return;
@@ -545,6 +874,22 @@ export class KaviDaemon {
545
874
  claimedPaths: task.claimedPaths
546
875
  }
547
876
  });
877
+ const autoResolvedNotes = autoResolveReviewNotesForCompletedTask(this.session, task.id);
878
+ for (const note of autoResolvedNotes){
879
+ addDecisionRecord(this.session, {
880
+ kind: "review",
881
+ agent: note.agent,
882
+ taskId: note.taskId,
883
+ summary: `Auto-resolved review note ${note.id}`,
884
+ detail: `Closed because linked follow-up task ${task.id} completed successfully.`,
885
+ metadata: {
886
+ reviewNoteId: note.id,
887
+ filePath: note.filePath,
888
+ followUpTaskId: task.id,
889
+ reason: "follow-up-task-completed"
890
+ }
891
+ });
892
+ }
548
893
  this.session.peerMessages.push(...peerMessages);
549
894
  await saveSessionRecord(this.paths, this.session);
550
895
  const decisionReplay = buildDecisionReplay(this.session, task, task.owner === "claude" ? "claude" : "codex");
@@ -561,15 +906,26 @@ export class KaviDaemon {
561
906
  rawOutput,
562
907
  error: null,
563
908
  envelope,
909
+ reviewNotes: reviewNotesForTask(this.session, task.id),
564
910
  startedAt,
565
911
  finishedAt: task.updatedAt
566
912
  });
913
+ await this.refreshReviewArtifactsForNotes(autoResolvedNotes);
567
914
  await recordEvent(this.paths, this.session.id, "task.completed", {
568
915
  taskId: task.id,
569
916
  owner: task.owner,
570
917
  status: task.status,
571
918
  peerMessages: peerMessages.length
572
919
  });
920
+ for (const note of autoResolvedNotes){
921
+ await recordEvent(this.paths, this.session.id, "review.note_auto_resolved", {
922
+ reviewNoteId: note.id,
923
+ taskId: note.taskId,
924
+ followUpTaskId: task.id,
925
+ agent: note.agent,
926
+ filePath: note.filePath
927
+ });
928
+ }
573
929
  await this.publishSnapshot("task.completed");
574
930
  } catch (error) {
575
931
  task.status = "failed";
@@ -617,6 +973,7 @@ export class KaviDaemon {
617
973
  rawOutput: null,
618
974
  error: task.summary,
619
975
  envelope: null,
976
+ reviewNotes: reviewNotesForTask(this.session, task.id),
620
977
  startedAt,
621
978
  finishedAt: task.updatedAt
622
979
  });
package/dist/doctor.js CHANGED
@@ -1,6 +1,24 @@
1
1
  import { fileExists } from "./fs.js";
2
2
  import { runCommand } from "./process.js";
3
3
  import { hasSupportedNode, minimumNodeMajor, resolveSessionRuntime } from "./runtime.js";
4
+ export function parseClaudeAuthStatus(output) {
5
+ try {
6
+ const parsed = JSON.parse(output);
7
+ const loggedIn = parsed.loggedIn === true;
8
+ const authMethod = typeof parsed.authMethod === "string" ? parsed.authMethod : "unknown";
9
+ const apiProvider = typeof parsed.apiProvider === "string" ? parsed.apiProvider : "unknown";
10
+ return {
11
+ loggedIn,
12
+ detail: loggedIn ? `logged in via ${authMethod} (${apiProvider})` : `not logged in (${authMethod}, ${apiProvider})`
13
+ };
14
+ } catch {
15
+ const trimmed = output.trim();
16
+ return {
17
+ loggedIn: false,
18
+ detail: trimmed || "unable to parse claude auth status"
19
+ };
20
+ }
21
+ }
4
22
  function normalizeVersion(output) {
5
23
  return output.trim().split(/\s+/).slice(-1)[0] ?? output.trim();
6
24
  }
@@ -37,6 +55,21 @@ export async function runDoctor(repoRoot, paths) {
37
55
  ok: claudeVersion.code === 0,
38
56
  detail: claudeVersion.code === 0 ? `${claudeVersion.stdout.trim()} via ${runtime.claudeExecutable}` : claudeVersion.stderr.trim()
39
57
  });
58
+ const claudeAuth = await runCommand(runtime.claudeExecutable, [
59
+ "auth",
60
+ "status"
61
+ ], {
62
+ cwd: repoRoot
63
+ });
64
+ const claudeAuthStatus = claudeAuth.code === 0 ? parseClaudeAuthStatus(claudeAuth.stdout) : {
65
+ loggedIn: false,
66
+ detail: claudeAuth.stderr.trim() || claudeAuth.stdout.trim() || "claude auth status failed"
67
+ };
68
+ checks.push({
69
+ name: "claude-auth",
70
+ ok: claudeAuth.code === 0 && claudeAuthStatus.loggedIn,
71
+ detail: claudeAuth.code === 0 ? claudeAuthStatus.detail : claudeAuthStatus.detail
72
+ });
40
73
  const worktreeCheck = await runCommand("git", [
41
74
  "worktree",
42
75
  "list"