@tarcisiopgs/lisa 1.36.0 → 1.37.1

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
@@ -53,7 +53,7 @@ If something fails — pre-push hooks, quota limits, stuck processes — Lisa ha
53
53
  - **Smart activity detection** — reads agent session logs to prevent false stuck kills during analysis phases
54
54
  - **Progress comments** — posts real-time status updates on issues as Lisa works through stages
55
55
  - **Context enrichment** — greps for issue-related files and surfaces them in the agent prompt
56
- - **PR reviewers & assignees** — auto-request reviews and assign PRs via config; `self` keyword resolves to the authenticated user
56
+ - **PR reviewers & assignees** — auto-request reviews and assign PRs via config; `self` keyword resolves to the authenticated user. Manage reviewers interactively from the TUI detail view (`r`)
57
57
  - **Self-healing** — orphan recovery on startup, push failure retry, stuck process detection
58
58
  - **Guardrails** — past failures are injected into future prompts to avoid repeating mistakes
59
59
  - **Lineage context** — plan-decomposed issues get sibling task awareness, preventing duplicate work in concurrent mode
@@ -360,6 +360,7 @@ The real-time Kanban board shows issue progress, streams provider output, and de
360
360
  |-----|--------|
361
361
  | `↑` `↓` | Scroll output log |
362
362
  | `o` | Open PR in browser |
363
+ | `r` | Toggle reviewer picker (add/remove reviewers on the PR) |
363
364
  | `m` | Merge PR (warns if CI not passed) |
364
365
  | `Esc` | Back to board |
365
366
 
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/errors.ts
4
+ function formatError(err) {
5
+ if (err instanceof Error) {
6
+ const cause = err.cause ? ` (caused by: ${formatError(err.cause)})` : "";
7
+ return `${err.message}${cause}`;
8
+ }
9
+ return String(err);
10
+ }
11
+ var LisaError = class extends Error {
12
+ constructor(message, options) {
13
+ super(message, options);
14
+ this.name = "LisaError";
15
+ }
16
+ };
17
+ var SourceError = class extends LisaError {
18
+ constructor(message, source, statusCode, options) {
19
+ super(message, options);
20
+ this.source = source;
21
+ this.statusCode = statusCode;
22
+ this.name = "SourceError";
23
+ }
24
+ source;
25
+ statusCode;
26
+ };
27
+
28
+ export {
29
+ formatError,
30
+ LisaError,
31
+ SourceError
32
+ };
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/output/logger.ts
4
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
5
+ import { dirname } from "path";
6
+ import pc from "picocolors";
7
+ var logFilePath = null;
8
+ var outputMode = "default";
9
+ var logLevel = "default";
10
+ function setOutputMode(mode) {
11
+ outputMode = mode;
12
+ }
13
+ function getOutputMode() {
14
+ return outputMode;
15
+ }
16
+ function setLogLevel(level) {
17
+ logLevel = level;
18
+ }
19
+ function shouldPrintToConsole() {
20
+ return outputMode !== "tui" && logLevel !== "quiet";
21
+ }
22
+ function initLogFile(path) {
23
+ const dir = dirname(path);
24
+ if (!existsSync(dir)) {
25
+ mkdirSync(dir, { recursive: true });
26
+ }
27
+ writeFileSync(path, `[${timestamp()}] Log started
28
+ `);
29
+ logFilePath = path;
30
+ }
31
+ function timestamp() {
32
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
33
+ }
34
+ function writeToFile(level, message) {
35
+ if (logFilePath) {
36
+ appendFileSync(logFilePath, `[${timestamp()}] [${level}] ${message}
37
+ `);
38
+ }
39
+ }
40
+ function log(message) {
41
+ if (shouldPrintToConsole()) {
42
+ console.error(`${pc.cyan("[lisa]")} ${pc.dim(timestamp())} ${message}`);
43
+ }
44
+ writeToFile("info", message);
45
+ }
46
+ function warn(message) {
47
+ if (shouldPrintToConsole()) {
48
+ console.error(`${pc.yellow("[lisa]")} ${pc.dim(timestamp())} ${message}`);
49
+ }
50
+ writeToFile("warn", message);
51
+ }
52
+ function error(message) {
53
+ if (shouldPrintToConsole()) {
54
+ console.error(`${pc.red("[lisa]")} ${pc.dim(timestamp())} ${message}`);
55
+ }
56
+ writeToFile("error", message);
57
+ }
58
+ function ok(message) {
59
+ if (shouldPrintToConsole()) {
60
+ console.error(`${pc.green("[lisa]")} ${pc.dim(timestamp())} ${message}`);
61
+ }
62
+ writeToFile("ok", message);
63
+ }
64
+ function verbose(message) {
65
+ if (logLevel !== "verbose") return;
66
+ if (shouldPrintToConsole()) {
67
+ console.error(`${pc.dim("[lisa]")} ${pc.dim(timestamp())} ${pc.dim(message)}`);
68
+ }
69
+ writeToFile("verbose", message);
70
+ }
71
+ function divider(session) {
72
+ log(`${"\u2501".repeat(3)} Session ${session} ${"\u2501".repeat(3)}`);
73
+ }
74
+ function banner() {
75
+ if (outputMode !== "default" || logLevel === "quiet") return;
76
+ const title = " lisa \u266A autonomous issue resolver ";
77
+ const border = "\u2500".repeat(title.length);
78
+ console.error(pc.yellow(`
79
+ \u250C${border}\u2510`));
80
+ console.error(pc.yellow(` \u2502`) + pc.bold(pc.white(title)) + pc.yellow("\u2502"));
81
+ console.error(pc.yellow(` \u2514${border}\u2518
82
+ `));
83
+ }
84
+ function updateNotice(update) {
85
+ if (outputMode !== "default" || logLevel === "quiet") return;
86
+ const msg = `Update available ${pc.dim(update.currentVersion)} \u2192 ${pc.green(pc.bold(update.latestVersion))}`;
87
+ const cmd = `Run ${pc.cyan("npm i -g @tarcisiopgs/lisa")} to update`;
88
+ const lines = [msg, cmd];
89
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
90
+ const maxLen = Math.max(...lines.map((l) => strip(l).length));
91
+ const pad = (s) => s + " ".repeat(maxLen - strip(s).length);
92
+ console.error(pc.yellow(` \u250C${"\u2500".repeat(maxLen + 2)}\u2510`));
93
+ for (const line of lines) {
94
+ console.error(pc.yellow(" \u2502 ") + pad(line) + pc.yellow(" \u2502"));
95
+ }
96
+ console.error(pc.yellow(` \u2514${"\u2500".repeat(maxLen + 2)}\u2518
97
+ `));
98
+ }
99
+
100
+ export {
101
+ setOutputMode,
102
+ getOutputMode,
103
+ setLogLevel,
104
+ initLogFile,
105
+ log,
106
+ warn,
107
+ error,
108
+ ok,
109
+ verbose,
110
+ divider,
111
+ banner,
112
+ updateNotice
113
+ };
@@ -5,17 +5,22 @@ import {
5
5
  resolveModels,
6
6
  runWithFallback,
7
7
  saveLineage
8
- } from "./chunk-6VIN5PMW.js";
8
+ } from "./chunk-YRKJONH5.js";
9
+ import {
10
+ normalizeLabels
11
+ } from "./chunk-LR2GREZS.js";
9
12
  import {
10
13
  error,
11
14
  log,
12
- normalizeLabels,
13
15
  ok,
14
16
  warn
15
- } from "./chunk-V44FTYWZ.js";
17
+ } from "./chunk-HPWL5JRW.js";
18
+ import {
19
+ LisaError
20
+ } from "./chunk-4MZ2565Y.js";
16
21
 
17
22
  // src/cli/error.ts
18
- var CliError = class extends Error {
23
+ var CliError = class extends LisaError {
19
24
  exitCode;
20
25
  constructor(message, exitCode = 1) {
21
26
  super(message);
@@ -2,96 +2,12 @@
2
2
  import {
3
3
  notify
4
4
  } from "./chunk-72CYGBT4.js";
5
-
6
- // src/output/logger.ts
7
- import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
8
- import { dirname } from "path";
9
- import pc from "picocolors";
10
- var logFilePath = null;
11
- var outputMode = "default";
12
- var logLevel = "default";
13
- function setOutputMode(mode) {
14
- outputMode = mode;
15
- }
16
- function getOutputMode() {
17
- return outputMode;
18
- }
19
- function setLogLevel(level) {
20
- logLevel = level;
21
- }
22
- function shouldPrintToConsole() {
23
- return outputMode !== "tui" && logLevel !== "quiet";
24
- }
25
- function initLogFile(path) {
26
- const dir = dirname(path);
27
- if (!existsSync(dir)) {
28
- mkdirSync(dir, { recursive: true });
29
- }
30
- writeFileSync(path, `[${timestamp()}] Log started
31
- `);
32
- logFilePath = path;
33
- }
34
- function timestamp() {
35
- return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
36
- }
37
- function writeToFile(level, message) {
38
- if (logFilePath) {
39
- appendFileSync(logFilePath, `[${timestamp()}] [${level}] ${message}
40
- `);
41
- }
42
- }
43
- function log(message) {
44
- if (shouldPrintToConsole()) {
45
- console.error(`${pc.cyan("[lisa]")} ${pc.dim(timestamp())} ${message}`);
46
- }
47
- writeToFile("info", message);
48
- }
49
- function warn(message) {
50
- if (shouldPrintToConsole()) {
51
- console.error(`${pc.yellow("[lisa]")} ${pc.dim(timestamp())} ${message}`);
52
- }
53
- writeToFile("warn", message);
54
- }
55
- function error(message) {
56
- if (shouldPrintToConsole()) {
57
- console.error(`${pc.red("[lisa]")} ${pc.dim(timestamp())} ${message}`);
58
- }
59
- writeToFile("error", message);
60
- }
61
- function ok(message) {
62
- if (shouldPrintToConsole()) {
63
- console.error(`${pc.green("[lisa]")} ${pc.dim(timestamp())} ${message}`);
64
- }
65
- writeToFile("ok", message);
66
- }
67
- function divider(session) {
68
- log(`${"\u2501".repeat(3)} Session ${session} ${"\u2501".repeat(3)}`);
69
- }
70
- function banner() {
71
- if (outputMode !== "default" || logLevel === "quiet") return;
72
- const title = " lisa \u266A autonomous issue resolver ";
73
- const border = "\u2500".repeat(title.length);
74
- console.error(pc.yellow(`
75
- \u250C${border}\u2510`));
76
- console.error(pc.yellow(` \u2502`) + pc.bold(pc.white(title)) + pc.yellow("\u2502"));
77
- console.error(pc.yellow(` \u2514${border}\u2518
78
- `));
79
- }
80
- function updateNotice(update) {
81
- if (outputMode !== "default" || logLevel === "quiet") return;
82
- const msg = `Update available ${pc.dim(update.currentVersion)} \u2192 ${pc.green(pc.bold(update.latestVersion))}`;
83
- const cmd = `Run ${pc.cyan("npm i -g @tarcisiopgs/lisa")} to update`;
84
- const lines = [msg, cmd];
85
- const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
86
- const maxLen = Math.max(...lines.map((l) => strip(l).length));
87
- const pad = (s) => s + " ".repeat(maxLen - strip(s).length);
88
- console.error(pc.yellow(` \u250C${"\u2500".repeat(maxLen + 2)}\u2510`));
89
- for (const line of lines) {
90
- console.error(pc.yellow(" \u2502 ") + pad(line) + pc.yellow(" \u2502"));
91
- }
92
- console.error(pc.yellow(` \u2514${"\u2500".repeat(maxLen + 2)}\u2518
93
- `));
94
- }
5
+ import {
6
+ warn
7
+ } from "./chunk-HPWL5JRW.js";
8
+ import {
9
+ SourceError
10
+ } from "./chunk-4MZ2565Y.js";
95
11
 
96
12
  // src/ui/state.ts
97
13
  import { EventEmitter } from "events";
@@ -102,51 +18,101 @@ import { execa } from "execa";
102
18
 
103
19
  // src/sources/base.ts
104
20
  var REQUEST_TIMEOUT_MS = 3e4;
21
+ var MAX_ATTEMPTS = 3;
22
+ var BASE_BACKOFF_MS = 1e3;
105
23
  function normalizeLabels(config) {
106
24
  return Array.isArray(config.label) ? config.label : config.label ? [config.label] : [];
107
25
  }
108
26
  function createApiClient(baseUrl, getHeaders, name) {
109
- async function request(method, path, body) {
110
- const url = `${baseUrl}${path}`;
111
- const headers = {
112
- ...await getHeaders(),
113
- "Content-Type": "application/json"
114
- };
115
- const res = await fetch(url, {
116
- method,
117
- headers,
118
- body: body !== void 0 ? JSON.stringify(body) : void 0,
119
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
120
- });
121
- if (!res.ok) {
122
- const text = await res.text();
123
- throw new Error(`${name} API error (${res.status}): ${text}`);
27
+ function isRetryable(error) {
28
+ if (error instanceof Error) {
29
+ if (error.name === "AbortError") return true;
30
+ if (error instanceof TypeError) return true;
31
+ if (error instanceof SourceError && error.statusCode) {
32
+ return error.statusCode >= 500;
33
+ }
34
+ const match = error.message.match(/API error \((\d+)\)/);
35
+ if (match?.[1]) {
36
+ const status = Number.parseInt(match[1], 10);
37
+ return status >= 500;
38
+ }
124
39
  }
125
- if (method === "DELETE" || res.status === 204) return void 0;
126
- return await res.json();
40
+ return false;
127
41
  }
128
- return {
129
- get: (path) => request("GET", path),
130
- post: (path, body) => request("POST", path, body),
131
- put: (path, body) => request("PUT", path, body),
132
- patch: (path, body) => request("PATCH", path, body),
133
- delete: (path) => request("DELETE", path),
134
- /** Raw request for non-JSON bodies (e.g. Trello form-encoded). */
135
- raw: async (method, path, init) => {
42
+ async function retryableRequest(operation) {
43
+ let lastError;
44
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
45
+ try {
46
+ return await operation();
47
+ } catch (error) {
48
+ lastError = error;
49
+ if (!isRetryable(error) || attempt === MAX_ATTEMPTS - 1) {
50
+ throw error;
51
+ }
52
+ const delay = BASE_BACKOFF_MS * 2 ** attempt;
53
+ warn(
54
+ `${name} API request failed (attempt ${attempt + 1}/${MAX_ATTEMPTS}), retrying in ${delay}ms...`
55
+ );
56
+ await new Promise((resolve) => setTimeout(resolve, delay));
57
+ }
58
+ }
59
+ throw lastError;
60
+ }
61
+ async function request(method, path, body, schema) {
62
+ return retryableRequest(async () => {
136
63
  const url = `${baseUrl}${path}`;
137
- const headers = await getHeaders();
64
+ const headers = {
65
+ ...await getHeaders(),
66
+ "Content-Type": "application/json"
67
+ };
138
68
  const res = await fetch(url, {
139
69
  method,
140
- headers: { ...headers, ...init?.headers },
141
- body: init?.body,
70
+ headers,
71
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
142
72
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
143
73
  });
144
74
  if (!res.ok) {
145
75
  const text = await res.text();
146
- throw new Error(`${name} API error (${res.status}): ${text}`);
76
+ throw new SourceError(`${name} API error (${res.status}): ${text}`, name, res.status);
147
77
  }
148
78
  if (method === "DELETE" || res.status === 204) return void 0;
149
- return await res.json();
79
+ const data = await res.json();
80
+ if (schema) {
81
+ const result = schema.safeParse(data);
82
+ if (!result.success) {
83
+ throw new Error(
84
+ `${name} API response validation failed for ${method} ${path}: ${result.error.message}`
85
+ );
86
+ }
87
+ return result.data;
88
+ }
89
+ return data;
90
+ });
91
+ }
92
+ return {
93
+ get: (path, schema) => request("GET", path, void 0, schema),
94
+ post: (path, body, schema) => request("POST", path, body, schema),
95
+ put: (path, body, schema) => request("PUT", path, body, schema),
96
+ patch: (path, body, schema) => request("PATCH", path, body, schema),
97
+ delete: (path) => request("DELETE", path),
98
+ /** Raw request for non-JSON bodies (e.g. Trello form-encoded). */
99
+ raw: async (method, path, init) => {
100
+ return retryableRequest(async () => {
101
+ const url = `${baseUrl}${path}`;
102
+ const headers = await getHeaders();
103
+ const res = await fetch(url, {
104
+ method,
105
+ headers: { ...headers, ...init?.headers },
106
+ body: init?.body,
107
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
108
+ });
109
+ if (!res.ok) {
110
+ const text = await res.text();
111
+ throw new SourceError(`${name} API error (${res.status}): ${text}`, name, res.status);
112
+ }
113
+ if (method === "DELETE" || res.status === 204) return void 0;
114
+ return await res.json();
115
+ });
150
116
  }
151
117
  };
152
118
  }
@@ -161,7 +127,10 @@ async function getToken() {
161
127
  if (stdout.trim()) return stdout.trim();
162
128
  } catch {
163
129
  }
164
- throw new Error("GitHub authentication required: set GITHUB_TOKEN or run `gh auth login`");
130
+ throw new SourceError(
131
+ "GitHub authentication required: set GITHUB_TOKEN or run `gh auth login`",
132
+ "github-issues"
133
+ );
165
134
  }
166
135
  async function getAuthHeaders() {
167
136
  const token = await getToken();
@@ -247,8 +216,15 @@ var GitHubIssuesSource = class {
247
216
  const isOrphanDetection = !!config.pick_from && !validStates.includes(config.pick_from);
248
217
  const filterLabels = isOrphanDetection ? [config.pick_from] : normalizeLabels(config);
249
218
  const label = filterLabels.map((l) => encodeURIComponent(l)).join(",");
250
- const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
251
- const issues = (await api().get(path)).filter((i) => !i.pull_request);
219
+ const issues = [];
220
+ let page = 1;
221
+ while (true) {
222
+ const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100&page=${page}`;
223
+ const batch = (await api().get(path)).filter((i) => !i.pull_request);
224
+ issues.push(...batch);
225
+ if (batch.length < 100) break;
226
+ page++;
227
+ }
252
228
  if (issues.length === 0) return null;
253
229
  const unblocked = [];
254
230
  const blocked = [];
@@ -388,9 +364,16 @@ var GitHubIssuesSource = class {
388
364
  const { owner, repo } = parseOwnerRepo(config.scope);
389
365
  const labels = normalizeLabels(config);
390
366
  const label = labels.map((l) => encodeURIComponent(l)).join(",");
391
- const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
392
- const issues = (await api().get(path)).filter((i) => !i.pull_request);
393
- return issues.map((issue) => ({
367
+ const allIssues = [];
368
+ let page = 1;
369
+ while (true) {
370
+ const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100&page=${page}`;
371
+ const batch = (await api().get(path)).filter((i) => !i.pull_request);
372
+ allIssues.push(...batch);
373
+ if (batch.length < 100) break;
374
+ page++;
375
+ }
376
+ return allIssues.map((issue) => ({
394
377
  id: makeIssueId(owner, repo, issue.number),
395
378
  title: issue.title,
396
379
  description: issue.body ?? "",
@@ -457,7 +440,7 @@ function getBaseUrl() {
457
440
  }
458
441
  function getAuthHeaders2() {
459
442
  const token = process.env.GITLAB_TOKEN;
460
- if (!token) throw new Error("GITLAB_TOKEN must be set");
443
+ if (!token) throw new SourceError("GITLAB_TOKEN must be set", "gitlab-issues");
461
444
  return { "PRIVATE-TOKEN": token };
462
445
  }
463
446
  var _api2;
@@ -512,8 +495,15 @@ var GitLabIssuesSource = class {
512
495
  const isOrphanDetection = !!config.pick_from && !validStates.includes(config.pick_from);
513
496
  const filterLabels = isOrphanDetection ? [config.pick_from] : normalizeLabels(config);
514
497
  const label = filterLabels.map((l) => encodeURIComponent(l)).join(",");
515
- const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
516
- const issues = await api2().get(path);
498
+ const issues = [];
499
+ let page = 1;
500
+ while (true) {
501
+ const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100&page=${page}`;
502
+ const batch = await api2().get(path);
503
+ issues.push(...batch);
504
+ if (batch.length < 100) break;
505
+ page++;
506
+ }
517
507
  if (issues.length === 0) return null;
518
508
  const unblocked = [];
519
509
  const blocked = [];
@@ -626,9 +616,16 @@ var GitLabIssuesSource = class {
626
616
  const project = parseGitLabProject(config.scope);
627
617
  const labelsArr = normalizeLabels(config);
628
618
  const label = labelsArr.map((l) => encodeURIComponent(l)).join(",");
629
- const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
630
- const issues = await api2().get(path);
631
- return issues.map((issue) => ({
619
+ const allIssues = [];
620
+ let page = 1;
621
+ while (true) {
622
+ const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100&page=${page}`;
623
+ const batch = await api2().get(path);
624
+ allIssues.push(...batch);
625
+ if (batch.length < 100) break;
626
+ page++;
627
+ }
628
+ return allIssues.map((issue) => ({
632
629
  id: makeIssueId2(config.scope, issue.iid),
633
630
  title: issue.title,
634
631
  description: issue.description ?? "",
@@ -739,17 +736,18 @@ function stopMergePolling(key) {
739
736
  }
740
737
  }
741
738
  function startMergePolling(issueId, prUrl) {
742
- if (activePolls.has(prUrl)) return;
739
+ const key = `${issueId}:${prUrl}`;
740
+ if (activePolls.has(key)) return;
743
741
  const intervalId = setInterval(() => {
744
742
  checkPrMergedByUrl(prUrl).then((merged) => {
745
743
  if (merged) {
746
- stopMergePolling(prUrl);
744
+ stopMergePolling(key);
747
745
  kanbanEmitter.emit("issue:merged", issueId);
748
746
  }
749
747
  }).catch(() => {
750
748
  });
751
749
  }, MERGE_POLL_INTERVAL_MS);
752
- activePolls.set(prUrl, intervalId);
750
+ activePolls.set(key, intervalId);
753
751
  }
754
752
  var KanbanEmitter = class extends EventEmitter {
755
753
  };
@@ -893,6 +891,14 @@ function useKanbanState(bellEnabled, initialCards = []) {
893
891
  const onSubstatus = (issueId, substatus) => {
894
892
  setCards((prev) => prev.map((c) => c.id === issueId ? { ...c, substatus } : c));
895
893
  };
894
+ const onReviewersUpdated = (issueId, reviewers) => {
895
+ setCards((prev) => prev.map((c) => c.id === issueId ? { ...c, reviewers } : c));
896
+ };
897
+ const onAvailableReviewers = (issueId, available) => {
898
+ setCards(
899
+ (prev) => prev.map((c) => c.id === issueId ? { ...c, availableReviewers: available } : c)
900
+ );
901
+ };
896
902
  const MAX_OUTPUT_SIZE = 2e5;
897
903
  const outputBuffer = /* @__PURE__ */ new Map();
898
904
  let flushTimer = null;
@@ -941,6 +947,8 @@ function useKanbanState(bellEnabled, initialCards = []) {
941
947
  kanbanEmitter.on("provider:resumed", onProviderResumed);
942
948
  kanbanEmitter.on("issue:log-file", onLogFile);
943
949
  kanbanEmitter.on("issue:substatus", onSubstatus);
950
+ kanbanEmitter.on("issue:reviewers-updated", onReviewersUpdated);
951
+ kanbanEmitter.on("issue:available-reviewers", onAvailableReviewers);
944
952
  kanbanEmitter.on("issue:output", onOutput);
945
953
  const onModelChanged = (model) => setModelInUse(model);
946
954
  kanbanEmitter.on("provider:model-changed", onModelChanged);
@@ -987,6 +995,8 @@ function useKanbanState(bellEnabled, initialCards = []) {
987
995
  kanbanEmitter.off("provider:resumed", onProviderResumed);
988
996
  kanbanEmitter.off("issue:log-file", onLogFile);
989
997
  kanbanEmitter.off("issue:substatus", onSubstatus);
998
+ kanbanEmitter.off("issue:reviewers-updated", onReviewersUpdated);
999
+ kanbanEmitter.off("issue:available-reviewers", onAvailableReviewers);
990
1000
  kanbanEmitter.off("issue:output", onOutput);
991
1001
  kanbanEmitter.off("provider:model-changed", onModelChanged);
992
1002
  kanbanEmitter.off("work:empty", onEmpty);
@@ -1015,17 +1025,6 @@ function useKanbanState(bellEnabled, initialCards = []) {
1015
1025
  }
1016
1026
 
1017
1027
  export {
1018
- setOutputMode,
1019
- getOutputMode,
1020
- setLogLevel,
1021
- initLogFile,
1022
- log,
1023
- warn,
1024
- error,
1025
- ok,
1026
- divider,
1027
- banner,
1028
- updateNotice,
1029
1028
  REQUEST_TIMEOUT_MS,
1030
1029
  normalizeLabels,
1031
1030
  createApiClient,