@tarcisiopgs/lisa 1.31.0 → 1.33.0

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
@@ -49,6 +49,7 @@ If something fails — pre-push hooks, quota limits, stuck processes — Lisa ha
49
49
  - **CI monitoring** — polls CI after PR creation, re-invokes the agent to fix failures automatically
50
50
  - **Progress comments** — posts real-time status updates on issues as Lisa works through stages
51
51
  - **Context enrichment** — greps for issue-related files and surfaces them in the agent prompt
52
+ - **PR reviewers & assignees** — auto-request reviews and assign PRs via config; `self` keyword resolves to the authenticated user
52
53
  - **Self-healing** — orphan recovery on startup, push failure retry, stuck process detection
53
54
  - **Guardrails** — past failures are injected into future prompts to avoid repeating mistakes
54
55
  - **Project context** — auto-generates `.lisa/context.md` with your stack, conventions, and constraints
@@ -254,6 +255,13 @@ ci_monitor:
254
255
  progress_comments:
255
256
  enabled: true # post real-time status on issues
256
257
 
258
+ pr:
259
+ reviewers: # auto-request reviews on every PR
260
+ - octocat
261
+ - hubot
262
+ assignees: # auto-assign PRs ("self" = authenticated user)
263
+ - self
264
+
257
265
  hooks:
258
266
  before_run: "./scripts/setup.sh"
259
267
  after_run: "./scripts/cleanup.sh"
@@ -5,6 +5,7 @@ import {
5
5
  appendPlatformAttribution,
6
6
  appendPlatformProofOfWork,
7
7
  appendPlatformSpecCompliance,
8
+ applyPrReviewersAndAssignees,
8
9
  buildCompliancePrompt,
9
10
  buildComplianceRecoveryPrompt,
10
11
  buildContinuationPrompt,
@@ -29,7 +30,7 @@ import {
29
30
  resolveModels,
30
31
  runValidationCommands,
31
32
  runWithFallback
32
- } from "./chunk-RQTH257A.js";
33
+ } from "./chunk-SC222P5D.js";
33
34
  import {
34
35
  divider,
35
36
  error,
@@ -38,7 +39,7 @@ import {
38
39
  log,
39
40
  ok,
40
41
  warn
41
- } from "./chunk-66TB6NMR.js";
42
+ } from "./chunk-XLAY4P45.js";
42
43
  import {
43
44
  appendRawEntry,
44
45
  migrateGuardrails
@@ -192,6 +193,23 @@ function findConfigDir(startDir = process.cwd()) {
192
193
  dir = parent;
193
194
  }
194
195
  }
196
+ var USERNAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,38}$/;
197
+ function isValidPrUsername(s) {
198
+ return s === "self" || USERNAME_RE.test(s);
199
+ }
200
+ function isStringArray(v) {
201
+ return Array.isArray(v) && v.every((item) => typeof item === "string");
202
+ }
203
+ function parsePrConfig(raw) {
204
+ if (!raw) return void 0;
205
+ const reviewers = isStringArray(raw.reviewers) ? raw.reviewers.filter(isValidPrUsername) : void 0;
206
+ const assignees = isStringArray(raw.assignees) ? raw.assignees.filter(isValidPrUsername) : void 0;
207
+ if (!reviewers?.length && !assignees?.length) return void 0;
208
+ return {
209
+ reviewers: reviewers?.length ? reviewers : void 0,
210
+ assignees: assignees?.length ? assignees : void 0
211
+ };
212
+ }
195
213
  function loadConfig(cwd = process.cwd()) {
196
214
  const configPath = getConfigPath(cwd);
197
215
  if (!existsSync(configPath)) {
@@ -234,6 +252,7 @@ function loadConfig(cwd = process.cwd()) {
234
252
  const rawCiMonitor = parsed.ci_monitor;
235
253
  const rawSpecCompliance = parsed.spec_compliance;
236
254
  const rawProgress = parsed.progress_comments;
255
+ const rawPr = parsed.pr;
237
256
  const config = {
238
257
  ...DEFAULT_CONFIG,
239
258
  ...parsedWithoutLogs,
@@ -279,6 +298,7 @@ function loadConfig(cwd = process.cwd()) {
279
298
  block_on_failure: rawSpecCompliance.block_on_failure
280
299
  } : void 0,
281
300
  progress_comments: rawProgress ? { enabled: rawProgress.enabled ?? false } : void 0,
301
+ pr: parsePrConfig(rawPr),
282
302
  provider_options: {
283
303
  ...DEFAULT_CONFIG.provider_options || {},
284
304
  ...parsed.provider_options ?? {}
@@ -2577,6 +2597,7 @@ ${contResult.output}
2577
2597
  await executeHook("before_remove", config.hooks, worktreePath, hookEnv);
2578
2598
  await cleanupWorktree(repoPath, worktreePath);
2579
2599
  await appendPlatformAttribution(prUrl, result.providerUsed, config.platform);
2600
+ await applyPrReviewersAndAssignees(prUrl, config.pr, config.platform);
2580
2601
  ok(`Step ${stepNum} complete: ${repoPath} \u2014 PR: ${prUrl}`);
2581
2602
  return {
2582
2603
  success: true,
@@ -2746,6 +2767,7 @@ async function runNativeWorktreeSession(config, issue, logFile, session, models,
2746
2767
  }
2747
2768
  ok(`PR created by provider: ${prUrl}`);
2748
2769
  await appendPlatformAttribution(prUrl, result.providerUsed, config.platform);
2770
+ await applyPrReviewersAndAssignees(prUrl, config.pr, config.platform);
2749
2771
  if (isCiMonitorEnabled(config.ci_monitor)) {
2750
2772
  const manifestBranch = manifest?.branch;
2751
2773
  if (manifestBranch) {
@@ -2967,6 +2989,7 @@ async function runManualWorktreeSession(config, issue, logFile, session, models,
2967
2989
  }
2968
2990
  ok(`PR created by provider: ${prUrl}`);
2969
2991
  await appendPlatformAttribution(prUrl, result.providerUsed, config.platform);
2992
+ await applyPrReviewersAndAssignees(prUrl, config.pr, config.platform);
2970
2993
  if (validationResults) {
2971
2994
  await appendPlatformProofOfWork(prUrl, validationResults, config.platform);
2972
2995
  }
@@ -3470,6 +3493,7 @@ async function runBranchSession(config, issue, logFile, session, models, source,
3470
3493
  }
3471
3494
  ok(`PR created by provider: ${prUrl}`);
3472
3495
  await appendPlatformAttribution(prUrl, result.providerUsed, config.platform);
3496
+ await applyPrReviewersAndAssignees(prUrl, config.pr, config.platform);
3473
3497
  if (validationResults) {
3474
3498
  await appendPlatformProofOfWork(prUrl, validationResults, config.platform);
3475
3499
  }
@@ -4,14 +4,14 @@ import {
4
4
  readContext,
5
5
  resolveModels,
6
6
  runWithFallback
7
- } from "./chunk-RQTH257A.js";
7
+ } from "./chunk-SC222P5D.js";
8
8
  import {
9
9
  error,
10
10
  log,
11
11
  normalizeLabels,
12
12
  ok,
13
13
  warn
14
- } from "./chunk-66TB6NMR.js";
14
+ } from "./chunk-XLAY4P45.js";
15
15
 
16
16
  // src/cli/error.ts
17
17
  var CliError = class extends Error {
@@ -153,6 +153,16 @@ function extractCleanText(text2) {
153
153
  }
154
154
 
155
155
  // src/plan/create.ts
156
+ function ensureAcceptanceCriteria(description, criteria) {
157
+ if (!criteria.length) return description;
158
+ if (/- \[ \]/.test(description)) return description;
159
+ const checklist = criteria.map((c) => `- [ ] ${c}`).join("\n");
160
+ return `${description}
161
+
162
+ ## Acceptance Criteria
163
+
164
+ ${checklist}`;
165
+ }
156
166
  async function createPlanIssues(source, config, plan) {
157
167
  if (!source.createIssue) {
158
168
  throw new Error(`Source "${source.name}" does not support createIssue`);
@@ -163,7 +173,7 @@ async function createPlanIssues(source, config, plan) {
163
173
  const createdIds = [];
164
174
  const orderToId = /* @__PURE__ */ new Map();
165
175
  for (const issue of sorted) {
166
- let description = issue.description;
176
+ let description = ensureAcceptanceCriteria(issue.description, issue.acceptanceCriteria);
167
177
  if (issue.dependsOn.length > 0 && !source.linkDependency) {
168
178
  const depRefs = issue.dependsOn.map((depOrder) => {
169
179
  const depId = orderToId.get(depOrder);
@@ -174,33 +184,39 @@ async function createPlanIssues(source, config, plan) {
174
184
  ---
175
185
  _Depends on: ${depRefs}_`;
176
186
  }
177
- const id = await source.createIssue(
178
- {
179
- title: issue.title,
180
- description,
181
- status: config.pick_from,
182
- label: primaryLabel,
183
- order: issue.order,
184
- parentId: plan.sourceIssueId
185
- },
186
- config
187
- );
188
- createdIds.push(id);
189
- orderToId.set(issue.order, id);
190
- ok(`${id}: ${issue.title}`);
191
- if (source.linkDependency && issue.dependsOn.length > 0) {
192
- for (const depOrder of issue.dependsOn) {
193
- const depId = orderToId.get(depOrder);
194
- if (depId) {
195
- try {
196
- await source.linkDependency(id, depId);
197
- } catch (err) {
198
- warn(
199
- `Could not link dependency ${id} \u2192 ${depId}: ${err instanceof Error ? err.message : String(err)}`
200
- );
187
+ try {
188
+ const id = await source.createIssue(
189
+ {
190
+ title: issue.title,
191
+ description,
192
+ status: config.pick_from,
193
+ label: primaryLabel,
194
+ order: issue.order,
195
+ parentId: plan.sourceIssueId
196
+ },
197
+ config
198
+ );
199
+ createdIds.push(id);
200
+ orderToId.set(issue.order, id);
201
+ ok(`${id}: ${issue.title}`);
202
+ if (source.linkDependency && issue.dependsOn.length > 0) {
203
+ for (const depOrder of issue.dependsOn) {
204
+ const depId = orderToId.get(depOrder);
205
+ if (depId) {
206
+ try {
207
+ await source.linkDependency(id, depId);
208
+ } catch (err) {
209
+ warn(
210
+ `Could not link dependency ${id} \u2192 ${depId}: ${err instanceof Error ? err.message : String(err)}`
211
+ );
212
+ }
201
213
  }
202
214
  }
203
215
  }
216
+ } catch (err) {
217
+ warn(
218
+ `Failed to create issue "${issue.title}": ${err instanceof Error ? err.message : String(err)}`
219
+ );
204
220
  }
205
221
  }
206
222
  return createdIds;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  isGhCliAvailable
4
- } from "./chunk-72DVXSHO.js";
4
+ } from "./chunk-YMV4CBQE.js";
5
5
  import {
6
6
  formatError
7
7
  } from "./chunk-7JT7DTSS.js";
@@ -11,7 +11,7 @@ import {
11
11
  normalizeLabels,
12
12
  ok,
13
13
  warn
14
- } from "./chunk-66TB6NMR.js";
14
+ } from "./chunk-XLAY4P45.js";
15
15
  import {
16
16
  appendEntry,
17
17
  buildGuardrailsSection,
@@ -23,10 +23,15 @@ import {
23
23
  getPlanPath
24
24
  } from "./chunk-7OCDGYDM.js";
25
25
  import {
26
+ addAssignees,
27
+ addReviewers,
26
28
  appendPrAttribution,
27
29
  appendPrBody,
30
+ getAuthenticatedUser,
31
+ parseBitbucketPrUrl,
32
+ parseGitLabMrUrl,
28
33
  stripProviderAttribution
29
- } from "./chunk-72DVXSHO.js";
34
+ } from "./chunk-YMV4CBQE.js";
30
35
  import {
31
36
  formatError
32
37
  } from "./chunk-7JT7DTSS.js";
@@ -3348,6 +3353,69 @@ async function appendPrBody2(prUrl, content) {
3348
3353
  } catch {
3349
3354
  }
3350
3355
  }
3356
+ var accountIdCache = /* @__PURE__ */ new Map();
3357
+ async function resolveAccountIds(usernames) {
3358
+ const result = /* @__PURE__ */ new Map();
3359
+ const uncached = usernames.filter((u) => {
3360
+ const cached = accountIdCache.get(u);
3361
+ if (cached) {
3362
+ result.set(u, cached);
3363
+ return false;
3364
+ }
3365
+ return true;
3366
+ });
3367
+ if (uncached.length === 0) return result;
3368
+ const resolutions = await Promise.allSettled(
3369
+ uncached.map(async (username) => {
3370
+ const res = await fetch(`${API_URL3}/users/${encodeURIComponent(username)}`, {
3371
+ headers: { Authorization: getAuthHeader() },
3372
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
3373
+ });
3374
+ if (!res.ok) return;
3375
+ const data = await res.json();
3376
+ if (data.account_id) {
3377
+ accountIdCache.set(username, data.account_id);
3378
+ result.set(username, data.account_id);
3379
+ }
3380
+ })
3381
+ );
3382
+ return result;
3383
+ }
3384
+ async function addPrReviewers(prUrl, reviewers) {
3385
+ if (!reviewers.length) return;
3386
+ const parsed = parseBitbucketPrUrl(prUrl);
3387
+ if (!parsed) return;
3388
+ const authHeader = getAuthHeader();
3389
+ const accountIds = await resolveAccountIds(reviewers);
3390
+ const getRes = await fetch(
3391
+ `${API_URL3}/repositories/${parsed.workspace}/${parsed.repoSlug}/pullrequests/${parsed.id}`,
3392
+ {
3393
+ headers: { Authorization: authHeader },
3394
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
3395
+ }
3396
+ );
3397
+ if (!getRes.ok) throw new Error(`Bitbucket API error (${getRes.status})`);
3398
+ const prData = await getRes.json();
3399
+ const existingIds = new Set((prData.reviewers ?? []).map((r) => r.account_id));
3400
+ for (const [, accountId] of accountIds) {
3401
+ existingIds.add(accountId);
3402
+ }
3403
+ const authorId = prData.author?.account_id;
3404
+ if (authorId) existingIds.delete(authorId);
3405
+ const mergedReviewers = [...existingIds].map((account_id) => ({ account_id }));
3406
+ await fetch(
3407
+ `${API_URL3}/repositories/${parsed.workspace}/${parsed.repoSlug}/pullrequests/${parsed.id}`,
3408
+ {
3409
+ method: "PUT",
3410
+ headers: {
3411
+ Authorization: authHeader,
3412
+ "Content-Type": "application/json"
3413
+ },
3414
+ body: JSON.stringify({ title: prData.title, reviewers: mergedReviewers }),
3415
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
3416
+ }
3417
+ );
3418
+ }
3351
3419
 
3352
3420
  // src/git/gitlab.ts
3353
3421
  import { execa as execa3 } from "execa";
@@ -3366,6 +3434,11 @@ function formatProviderName2(providerUsed) {
3366
3434
  const providerKey = providerUsed.split("/")[0] ?? providerUsed;
3367
3435
  return PROVIDER_DISPLAY_NAMES2[providerKey] ?? providerKey;
3368
3436
  }
3437
+ function getToken() {
3438
+ const token = process.env.GITLAB_TOKEN;
3439
+ if (!token) throw new Error("GITLAB_TOKEN is not set");
3440
+ return token;
3441
+ }
3369
3442
  function buildApiBase(host) {
3370
3443
  return `https://${host}/api/v4`;
3371
3444
  }
@@ -3432,6 +3505,105 @@ async function appendMrBody(mrUrl, content) {
3432
3505
  } catch {
3433
3506
  }
3434
3507
  }
3508
+ var authenticatedUserCache = null;
3509
+ async function getGitLabAuthenticatedUser() {
3510
+ if (authenticatedUserCache) return authenticatedUserCache;
3511
+ const token = getToken();
3512
+ const res = await fetch("https://gitlab.com/api/v4/user", {
3513
+ headers: { "PRIVATE-TOKEN": token },
3514
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
3515
+ });
3516
+ if (!res.ok) throw new Error(`GitLab API error (${res.status})`);
3517
+ const data = await res.json();
3518
+ authenticatedUserCache = data.username;
3519
+ return authenticatedUserCache;
3520
+ }
3521
+ var memberIdCache = /* @__PURE__ */ new Map();
3522
+ async function resolveUserIds(usernames, host, projectPath) {
3523
+ const result = /* @__PURE__ */ new Map();
3524
+ const uncached = usernames.filter((u) => {
3525
+ const cached = memberIdCache.get(u);
3526
+ if (cached !== void 0) {
3527
+ result.set(u, cached);
3528
+ return false;
3529
+ }
3530
+ return true;
3531
+ });
3532
+ if (uncached.length === 0) return result;
3533
+ const token = getToken();
3534
+ const apiBase = buildApiBase(host);
3535
+ const encodedPath = encodeURIComponent(projectPath);
3536
+ try {
3537
+ const membersRes = await fetch(`${apiBase}/projects/${encodedPath}/members/all?per_page=100`, {
3538
+ headers: { "PRIVATE-TOKEN": token },
3539
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
3540
+ });
3541
+ if (membersRes.ok) {
3542
+ const members = await membersRes.json();
3543
+ for (const m of members) {
3544
+ memberIdCache.set(m.username, m.id);
3545
+ if (uncached.includes(m.username)) {
3546
+ result.set(m.username, m.id);
3547
+ }
3548
+ }
3549
+ }
3550
+ } catch {
3551
+ }
3552
+ const stillMissing = uncached.filter((u) => !result.has(u));
3553
+ const resolutions = await Promise.allSettled(
3554
+ stillMissing.map(async (username) => {
3555
+ const res = await fetch(`${apiBase}/users?username=${encodeURIComponent(username)}`, {
3556
+ headers: { "PRIVATE-TOKEN": token },
3557
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
3558
+ });
3559
+ if (!res.ok) return;
3560
+ const users = await res.json();
3561
+ if (users[0]) {
3562
+ memberIdCache.set(users[0].username, users[0].id);
3563
+ result.set(username, users[0].id);
3564
+ }
3565
+ })
3566
+ );
3567
+ return result;
3568
+ }
3569
+ async function addMrReviewersAndAssignees(mrUrl, reviewerUsernames, assigneeUsernames) {
3570
+ if (!reviewerUsernames.length && !assigneeUsernames.length) return;
3571
+ const parsed = parseGitLabMrUrl(mrUrl);
3572
+ if (!parsed) return;
3573
+ const token = getToken();
3574
+ const apiBase = buildApiBase(parsed.host);
3575
+ const encodedPath = encodeURIComponent(parsed.projectPath);
3576
+ const allUsernames = [.../* @__PURE__ */ new Set([...reviewerUsernames, ...assigneeUsernames])];
3577
+ const idMap = await resolveUserIds(allUsernames, parsed.host, parsed.projectPath);
3578
+ const getRes = await fetch(`${apiBase}/projects/${encodedPath}/merge_requests/${parsed.iid}`, {
3579
+ headers: { "PRIVATE-TOKEN": token },
3580
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
3581
+ });
3582
+ if (!getRes.ok) throw new Error(`GitLab API error (${getRes.status})`);
3583
+ const mrData = await getRes.json();
3584
+ const existingReviewerIds = new Set((mrData.reviewers ?? []).map((r) => r.id));
3585
+ const existingAssigneeIds = new Set((mrData.assignees ?? []).map((a) => a.id));
3586
+ for (const username of reviewerUsernames) {
3587
+ const id = idMap.get(username);
3588
+ if (id) existingReviewerIds.add(id);
3589
+ }
3590
+ for (const username of assigneeUsernames) {
3591
+ const id = idMap.get(username);
3592
+ if (id) existingAssigneeIds.add(id);
3593
+ }
3594
+ const body = {};
3595
+ if (reviewerUsernames.length) body.reviewer_ids = [...existingReviewerIds];
3596
+ if (assigneeUsernames.length) body.assignee_ids = [...existingAssigneeIds];
3597
+ await fetch(`${apiBase}/projects/${encodedPath}/merge_requests/${parsed.iid}`, {
3598
+ method: "PUT",
3599
+ headers: {
3600
+ "PRIVATE-TOKEN": token,
3601
+ "Content-Type": "application/json"
3602
+ },
3603
+ body: JSON.stringify(body),
3604
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
3605
+ });
3606
+ }
3435
3607
 
3436
3608
  // src/git/platform.ts
3437
3609
  async function appendPlatformAttribution(prUrl, providerUsed, platform2) {
@@ -3508,6 +3680,52 @@ function buildPrCreateInstruction(platform2, targetBranch) {
3508
3680
  \`gh pr create --title "<conventional-commit-title>" --body "<markdown-summary>"${base}\`
3509
3681
  Capture the PR URL from the output.`;
3510
3682
  }
3683
+ async function resolveAuthenticatedUser(platform2) {
3684
+ try {
3685
+ if (platform2 === "gitlab") {
3686
+ return await getGitLabAuthenticatedUser();
3687
+ }
3688
+ if (platform2 === "bitbucket") {
3689
+ return null;
3690
+ }
3691
+ return await getAuthenticatedUser(platform2);
3692
+ } catch {
3693
+ return null;
3694
+ }
3695
+ }
3696
+ async function applyPrReviewersAndAssignees(prUrl, prConfig, platform2) {
3697
+ if (!prConfig) return;
3698
+ const rawReviewers = prConfig.reviewers ?? [];
3699
+ const rawAssignees = prConfig.assignees ?? [];
3700
+ if (!rawReviewers.length && !rawAssignees.length) return;
3701
+ try {
3702
+ const hasSelf = rawReviewers.includes("self") || rawAssignees.includes("self");
3703
+ let selfUsername = null;
3704
+ if (hasSelf) {
3705
+ selfUsername = await resolveAuthenticatedUser(platform2);
3706
+ }
3707
+ const reviewers = rawReviewers.filter((r) => r !== "self").filter((r) => r !== selfUsername);
3708
+ const assignees = rawAssignees.map((a) => {
3709
+ if (a === "self") return selfUsername;
3710
+ return a;
3711
+ }).filter((a) => a !== null);
3712
+ if (selfUsername && rawReviewers.includes("self")) {
3713
+ warn("Filtered 'self' from reviewers \u2014 cannot request review from yourself");
3714
+ }
3715
+ if (platform2 === "gitlab") {
3716
+ await addMrReviewersAndAssignees(prUrl, reviewers, assignees);
3717
+ } else if (platform2 === "bitbucket") {
3718
+ if (reviewers.length) await addPrReviewers(prUrl, reviewers);
3719
+ } else {
3720
+ const tasks = [];
3721
+ if (reviewers.length) tasks.push(addReviewers(prUrl, reviewers));
3722
+ if (assignees.length) tasks.push(addAssignees(prUrl, assignees));
3723
+ await Promise.allSettled(tasks);
3724
+ }
3725
+ } catch (err) {
3726
+ warn(`Failed to add reviewers/assignees: ${formatError(err)}`);
3727
+ }
3728
+ }
3511
3729
 
3512
3730
  // src/prompt.ts
3513
3731
  function detectPackageManager(cwd) {
@@ -4109,6 +4327,7 @@ export {
4109
4327
  appendPlatformAttribution,
4110
4328
  appendPlatformProofOfWork,
4111
4329
  appendPlatformSpecCompliance,
4330
+ applyPrReviewersAndAssignees,
4112
4331
  detectPackageManager,
4113
4332
  detectTestRunner,
4114
4333
  buildImplementPrompt,
@@ -772,6 +772,7 @@ function registerBellListeners(bellEnabled) {
772
772
  function useKanbanState(bellEnabled, initialCards = []) {
773
773
  const [cards, setCards] = useState(initialCards);
774
774
  const [isEmpty, setIsEmpty] = useState(false);
775
+ const [isFetching, setIsFetching] = useState(initialCards.length === 0);
775
776
  const [isWatching, setIsWatching] = useState(false);
776
777
  const [isWatchPrompt, setIsWatchPrompt] = useState(false);
777
778
  const [workComplete, setWorkComplete] = useState(
@@ -780,6 +781,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
780
781
  const [modelInUse, setModelInUse] = useState(null);
781
782
  useEffect(() => {
782
783
  const onQueued = (issue) => {
784
+ setIsFetching(false);
783
785
  setCards((prev) => {
784
786
  if (prev.some((c) => c.id === issue.id)) return prev;
785
787
  const maxOrder = prev.reduce((max, c) => Math.max(max, c.queueOrder ?? 0), 0);
@@ -933,10 +935,16 @@ function useKanbanState(bellEnabled, initialCards = []) {
933
935
  kanbanEmitter.on("issue:output", onOutput);
934
936
  const onModelChanged = (model) => setModelInUse(model);
935
937
  kanbanEmitter.on("provider:model-changed", onModelChanged);
936
- const onEmpty = () => setIsEmpty(true);
938
+ const onEmpty = () => {
939
+ setIsEmpty(true);
940
+ setIsFetching(false);
941
+ };
937
942
  const onResumed = () => setIsEmpty(false);
938
943
  const onComplete = (data) => setWorkComplete(data);
939
- const onWatching = () => setIsWatching(true);
944
+ const onWatching = () => {
945
+ setIsWatching(true);
946
+ setIsFetching(false);
947
+ };
940
948
  const onWatchResume = () => setIsWatching(false);
941
949
  const onWatchPrompt = () => {
942
950
  setIsWatchPrompt(true);
@@ -992,7 +1000,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
992
1000
  }
993
1001
  }
994
1002
  }, []);
995
- return { cards, isEmpty, isWatching, isWatchPrompt, workComplete, modelInUse };
1003
+ return { cards, isEmpty, isFetching, isWatching, isWatchPrompt, workComplete, modelInUse };
996
1004
  }
997
1005
 
998
1006
  export {
@@ -25,7 +25,32 @@ function stripProviderAttribution(body) {
25
25
  return result.trimEnd();
26
26
  }
27
27
 
28
+ // src/git/url-parser.ts
29
+ function parseGitHubPrUrl(prUrl) {
30
+ const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
31
+ if (!match) return null;
32
+ const num = Number.parseInt(match[3] ?? "0", 10);
33
+ if (num <= 0) return null;
34
+ return { owner: match[1] ?? "", repo: match[2] ?? "", number: num };
35
+ }
36
+ function parseGitLabMrUrl(mrUrl) {
37
+ const match = mrUrl.match(/https?:\/\/([^/]+)\/(.+?)\/-\/merge_requests\/(\d+)/);
38
+ if (!match) return null;
39
+ const iid = Number.parseInt(match[3] ?? "0", 10);
40
+ if (iid <= 0) return null;
41
+ return { host: match[1] ?? "", projectPath: match[2] ?? "", iid };
42
+ }
43
+ function parseBitbucketPrUrl(prUrl) {
44
+ const match = prUrl.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)/);
45
+ if (!match) return null;
46
+ const id = Number.parseInt(match[3] ?? "0", 10);
47
+ if (id <= 0) return null;
48
+ return { workspace: match[1] ?? "", repoSlug: match[2] ?? "", id };
49
+ }
50
+
28
51
  // src/git/github.ts
52
+ var API_URL = "https://api.github.com";
53
+ var REQUEST_TIMEOUT_MS = 3e4;
29
54
  async function isGhCliAvailable() {
30
55
  try {
31
56
  await execa("gh", ["auth", "status"]);
@@ -34,6 +59,11 @@ async function isGhCliAvailable() {
34
59
  return false;
35
60
  }
36
61
  }
62
+ function getToken() {
63
+ const token = process.env.GITHUB_TOKEN;
64
+ if (!token) throw new Error("GITHUB_TOKEN is not set");
65
+ return token;
66
+ }
37
67
  var PROVIDER_DISPLAY_NAMES = {
38
68
  claude: "Claude Code",
39
69
  gemini: "Gemini CLI",
@@ -107,10 +137,73 @@ async function appendPrBody(prUrl, content) {
107
137
  } catch {
108
138
  }
109
139
  }
140
+ var authenticatedUserCache = null;
141
+ async function getAuthenticatedUser(method = "cli") {
142
+ if (authenticatedUserCache) return authenticatedUserCache;
143
+ if (method === "cli" || method === "token") {
144
+ if (method === "cli") {
145
+ const { stdout } = await execa("gh", ["api", "/user", "--jq", ".login"]);
146
+ authenticatedUserCache = stdout.trim();
147
+ } else {
148
+ const res = await fetch(`${API_URL}/user`, {
149
+ headers: { Authorization: `Bearer ${getToken()}`, Accept: "application/vnd.github+json" },
150
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
151
+ });
152
+ if (!res.ok) throw new Error(`GitHub API error (${res.status})`);
153
+ const data = await res.json();
154
+ authenticatedUserCache = data.login;
155
+ }
156
+ }
157
+ if (!authenticatedUserCache) throw new Error("Could not resolve authenticated GitHub user");
158
+ return authenticatedUserCache;
159
+ }
160
+ async function addReviewers(prUrl, reviewers) {
161
+ if (!reviewers.length) return;
162
+ const parsed = parseGitHubPrUrl(prUrl);
163
+ if (!parsed) return;
164
+ await execa(
165
+ "gh",
166
+ [
167
+ "api",
168
+ "--method",
169
+ "POST",
170
+ `/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}/requested_reviewers`,
171
+ "--input",
172
+ "-"
173
+ ],
174
+ {
175
+ input: JSON.stringify({ reviewers })
176
+ }
177
+ );
178
+ }
179
+ async function addAssignees(prUrl, assignees) {
180
+ if (!assignees.length) return;
181
+ const parsed = parseGitHubPrUrl(prUrl);
182
+ if (!parsed) return;
183
+ await execa(
184
+ "gh",
185
+ [
186
+ "api",
187
+ "--method",
188
+ "POST",
189
+ `/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}/assignees`,
190
+ "--input",
191
+ "-"
192
+ ],
193
+ {
194
+ input: JSON.stringify({ assignees })
195
+ }
196
+ );
197
+ }
110
198
 
111
199
  export {
112
200
  stripProviderAttribution,
201
+ parseGitLabMrUrl,
202
+ parseBitbucketPrUrl,
113
203
  isGhCliAvailable,
114
204
  appendPrAttribution,
115
- appendPrBody
205
+ appendPrBody,
206
+ getAuthenticatedUser,
207
+ addReviewers,
208
+ addAssignees
116
209
  };
@@ -12,8 +12,8 @@ import {
12
12
  getVersion,
13
13
  isCursorFreePlan,
14
14
  verifyPlatformCredential
15
- } from "./chunk-IQDRQXFK.js";
16
- import "./chunk-72DVXSHO.js";
15
+ } from "./chunk-R6D5VH65.js";
16
+ import "./chunk-YMV4CBQE.js";
17
17
  import "./chunk-7JT7DTSS.js";
18
18
  export {
19
19
  detectDefaultBranch,
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  parseStructuredOutput,
8
8
  runPlanWizard,
9
9
  savePlan
10
- } from "./chunk-YTHUJQKB.js";
10
+ } from "./chunk-6XC7GQUI.js";
11
11
  import {
12
12
  detectDefaultBranch,
13
13
  detectGitRepos,
@@ -18,7 +18,7 @@ import {
18
18
  getMissingEnvVars,
19
19
  getVersion,
20
20
  isCursorFreePlan
21
- } from "./chunk-IQDRQXFK.js";
21
+ } from "./chunk-R6D5VH65.js";
22
22
  import {
23
23
  getCachedUpdateInfo
24
24
  } from "./chunk-7CIXBENY.js";
@@ -36,7 +36,7 @@ import {
36
36
  runLoop,
37
37
  saveConfig,
38
38
  validateConfig
39
- } from "./chunk-45ZNECZ5.js";
39
+ } from "./chunk-23Z5BBRT.js";
40
40
  import {
41
41
  buildContextMdBlock,
42
42
  createProvider,
@@ -48,7 +48,7 @@ import {
48
48
  readContext,
49
49
  resolveModels,
50
50
  runWithFallback
51
- } from "./chunk-RQTH257A.js";
51
+ } from "./chunk-SC222P5D.js";
52
52
  import {
53
53
  banner,
54
54
  error,
@@ -58,7 +58,7 @@ import {
58
58
  setLogLevel,
59
59
  setOutputMode,
60
60
  updateNotice
61
- } from "./chunk-66TB6NMR.js";
61
+ } from "./chunk-XLAY4P45.js";
62
62
  import "./chunk-3EOEDL3T.js";
63
63
  import {
64
64
  getKanbanStatePath,
@@ -67,7 +67,7 @@ import {
67
67
  import "./chunk-72CYGBT4.js";
68
68
  import {
69
69
  isGhCliAvailable
70
- } from "./chunk-72DVXSHO.js";
70
+ } from "./chunk-YMV4CBQE.js";
71
71
  import {
72
72
  formatError
73
73
  } from "./chunk-7JT7DTSS.js";
@@ -1001,6 +1001,23 @@ function runAdvancedChecks(config2) {
1001
1001
  suggestion: existsSync(contextPath) ? void 0 : 'Run "lisa context refresh" to generate .lisa/context.md for better planning.',
1002
1002
  category: "advanced"
1003
1003
  });
1004
+ if (config2.pr) {
1005
+ if (config2.platform === "bitbucket" && config2.pr.assignees?.length) {
1006
+ results.push({
1007
+ passed: false,
1008
+ label: "PR assignees compatible with platform",
1009
+ suggestion: "Bitbucket does not support PR assignees. Remove pr.assignees or switch platform.",
1010
+ category: "advanced"
1011
+ });
1012
+ }
1013
+ if (config2.pr.reviewers?.length || config2.pr.assignees?.length) {
1014
+ results.push({
1015
+ passed: true,
1016
+ label: `PR config: ${config2.pr.reviewers?.length ?? 0} reviewer(s), ${config2.pr.assignees?.length ?? 0} assignee(s)`,
1017
+ category: "advanced"
1018
+ });
1019
+ }
1020
+ }
1004
1021
  return results;
1005
1022
  }
1006
1023
  var doctor = defineCommand3({
@@ -1527,7 +1544,7 @@ async function reviewAndCreate(plan2, planPath, opts) {
1527
1544
  log("Run `lisa run` when ready.");
1528
1545
  return;
1529
1546
  }
1530
- const { runLoop: runLoop2 } = await import("./loop-INGME5Y3.js");
1547
+ const { runLoop: runLoop2 } = await import("./loop-XEJHKZHO.js");
1531
1548
  await runLoop2(config2, {
1532
1549
  once: false,
1533
1550
  watch: false,
@@ -1891,7 +1908,7 @@ async function executeRun(args) {
1891
1908
  if (isTTY) {
1892
1909
  const { render } = await import("ink");
1893
1910
  const { createElement } = await import("react");
1894
- const { KanbanApp } = await import("./kanban-TXO3LZON.js");
1911
+ const { KanbanApp } = await import("./kanban-X4OR4ZFI.js");
1895
1912
  const demoConfig = {
1896
1913
  provider: "claude",
1897
1914
  source: "linear",
@@ -1991,7 +2008,7 @@ Add them to your ${shell} and run: source ${shell}`));
1991
2008
  const initialCards = persistence.load();
1992
2009
  persistedCards = initialCards;
1993
2010
  persistence.start();
1994
- const { registerPlanBridge } = await import("./tui-bridge-KSE7MJDX.js");
2011
+ const { registerPlanBridge } = await import("./tui-bridge-G6BBFZFH.js");
1995
2012
  const cleanupPlan = registerPlanBridge(merged);
1996
2013
  onBeforeExit = () => {
1997
2014
  persistence.stop();
@@ -1999,7 +2016,7 @@ Add them to your ${shell} and run: source ${shell}`));
1999
2016
  };
2000
2017
  const { render } = await import("ink");
2001
2018
  const { createElement } = await import("react");
2002
- const { KanbanApp } = await import("./kanban-TXO3LZON.js");
2019
+ const { KanbanApp } = await import("./kanban-X4OR4ZFI.js");
2003
2020
  render(createElement(KanbanApp, { config: merged, initialCards }), { exitOnCtrlC: false });
2004
2021
  }
2005
2022
  await runLoop(merged, {
@@ -2047,6 +2064,7 @@ var status = defineCommand9({
2047
2064
  label: formatLabels(config2.source_config),
2048
2065
  scope: config2.source_config.scope,
2049
2066
  platform: config2.platform,
2067
+ pr: config2.pr ?? null,
2050
2068
  logsDir,
2051
2069
  sessionCount
2052
2070
  },
@@ -2070,6 +2088,17 @@ var status = defineCommand9({
2070
2088
  console.log(` Pick from: ${pc7.bold(config2.source_config.pick_from)}`);
2071
2089
  console.log(` In progress: ${pc7.bold(config2.source_config.in_progress)}`);
2072
2090
  console.log(` Done: ${pc7.bold(config2.source_config.done)}`);
2091
+ if (config2.pr) {
2092
+ const reviewers = config2.pr.reviewers?.join(", ") ?? pc7.dim("none");
2093
+ const assignees = config2.pr.assignees?.join(", ") ?? pc7.dim("none");
2094
+ console.log(` Reviewers: ${pc7.bold(reviewers)}`);
2095
+ console.log(` Assignees: ${pc7.bold(assignees)}`);
2096
+ if (config2.platform === "bitbucket" && config2.pr.assignees?.length) {
2097
+ console.log(
2098
+ ` ${pc7.yellow("!")} Bitbucket does not support assignees \u2014 only reviewers will be added`
2099
+ );
2100
+ }
2101
+ }
2073
2102
  console.log(` Logs: ${pc7.dim(logsDir)}`);
2074
2103
  if (sessionCount > 0) {
2075
2104
  console.log(`
@@ -2111,7 +2140,7 @@ process.on("unhandledRejection", (err) => {
2111
2140
  }
2112
2141
  process.exit(1);
2113
2142
  });
2114
- import("./detection-PC7EMUHY.js").then(
2143
+ import("./detection-H5QJR5XI.js").then(
2115
2144
  ({ getVersion: getVersion2 }) => import("./version-D2YFKS7Q.js").then(({ checkForUpdate }) => checkForUpdate(getVersion2()))
2116
2145
  );
2117
2146
  runCli();
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  kanbanEmitter,
7
7
  useKanbanState
8
- } from "./chunk-66TB6NMR.js";
8
+ } from "./chunk-XLAY4P45.js";
9
9
  import {
10
10
  resetTitle,
11
11
  startSpinner,
@@ -18,6 +18,7 @@ import { useEffect as useEffect5, useState as useState6 } from "react";
18
18
 
19
19
  // src/ui/board.tsx
20
20
  import { Box as Box3, Text as Text3 } from "ink";
21
+ import Spinner2 from "ink-spinner";
21
22
 
22
23
  // src/ui/column.tsx
23
24
  import { Box as Box2, Text as Text2 } from "ink";
@@ -130,9 +131,12 @@ function Card({
130
131
  card.column === "in_progress" ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", marginTop: 0, children: [
131
132
  isPausedInProgress ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u23F8" }) : /* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
132
133
  /* @__PURE__ */ jsx(Text, { color: isPausedInProgress ? "gray" : "yellow", dimColor: isPausedInProgress, children: elapsedMs !== null ? ` ${formatElapsed(elapsedMs)}` : "" })
133
- ] }) : card.column === "done" && card.startedAt !== void 0 && card.finishedAt !== void 0 ? /* @__PURE__ */ jsxs(Text, { color: card.merged ? "magenta" : "green", children: [
134
- "\u2714 ",
135
- formatElapsed(card.finishedAt - card.startedAt)
134
+ ] }) : card.column === "done" && card.startedAt !== void 0 && card.finishedAt !== void 0 ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [
135
+ /* @__PURE__ */ jsxs(Text, { color: card.merged ? "magenta" : "green", children: [
136
+ "\u2714 ",
137
+ formatElapsed(card.finishedAt - card.startedAt)
138
+ ] }),
139
+ card.prUrls.length > 0 && /* @__PURE__ */ jsx(Text, { color: card.merged ? "magenta" : "yellow", dimColor: true, children: card.merged ? "PR\u2714" : "PR" })
136
140
  ] }) : card.killed ? /* @__PURE__ */ jsx(Text, { color: "red", children: "KILLED" }) : card.skipped ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "SKIPPED" }) : card.hasError && !card.killed && !card.skipped ? /* @__PURE__ */ jsx(Text, { color: "red", children: "FAILED" }) : (
137
141
  // Empty row for backlog and done-without-timing — maintains CARD_HEIGHT
138
142
  /* @__PURE__ */ jsx(Text, { children: " ".repeat(cardWidth) })
@@ -254,6 +258,7 @@ function Board({
254
258
  columns,
255
259
  labels,
256
260
  isEmpty,
261
+ isFetching = false,
257
262
  isWatching = false,
258
263
  isWatchPrompt = false,
259
264
  workComplete,
@@ -262,6 +267,28 @@ function Board({
262
267
  paused = false
263
268
  }) {
264
269
  const { backlog, inProgress, done } = columns;
270
+ if (isFetching) {
271
+ return /* @__PURE__ */ jsx3(Box3, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs3(
272
+ Box3,
273
+ {
274
+ flexDirection: "column",
275
+ borderStyle: "single",
276
+ borderColor: "yellow",
277
+ paddingX: 3,
278
+ paddingY: 1,
279
+ children: [
280
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "row", children: [
281
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: /* @__PURE__ */ jsx3(Spinner2, { type: "dots" }) }),
282
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", bold: true, children: " FETCHING ISSUES..." })
283
+ ] }),
284
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
285
+ /* @__PURE__ */ jsx3(Text3, { color: "white", dimColor: true, children: "Querying the issue tracker for matching items." }),
286
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
287
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Press [q] to quit" })
288
+ ]
289
+ }
290
+ ) });
291
+ }
265
292
  if (isWatching) {
266
293
  return /* @__PURE__ */ jsx3(Box3, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs3(
267
294
  Box3,
@@ -375,7 +402,7 @@ function Board({
375
402
  // src/ui/detail.tsx
376
403
  import { exec } from "child_process";
377
404
  import { Box as Box4, Text as Text4, useInput } from "ink";
378
- import Spinner2 from "ink-spinner";
405
+ import Spinner3 from "ink-spinner";
379
406
  import { useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
380
407
 
381
408
  // src/output/line-color.ts
@@ -390,7 +417,7 @@ function logLineColor(line) {
390
417
  }
391
418
 
392
419
  // src/ui/detail.tsx
393
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
420
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
394
421
  function openUrl(url) {
395
422
  const platform = process.platform;
396
423
  let command;
@@ -443,7 +470,7 @@ function statusLabel(column, hasError, killed, skipped, merged) {
443
470
  if (column === "done") return { text: "DONE", color: "green" };
444
471
  return { text: "QUEUED", color: "white" };
445
472
  }
446
- function IssueDetail({ card, onBack }) {
473
+ function IssueDetail({ card, onBack, reviewers, assignees }) {
447
474
  const [now, setNow] = useState3(Date.now());
448
475
  const [logScrollOffset, setLogScrollOffset] = useState3(0);
449
476
  const [userScrolled, setUserScrolled] = useState3(false);
@@ -488,7 +515,9 @@ function IssueDetail({ card, onBack }) {
488
515
  const maxLineWidth = Math.max(1, terminalCols - SIDEBAR_TOTAL_WIDTH4 - 4);
489
516
  const prCount = card.prUrls.length > 0 ? card.prUrls.length : 0;
490
517
  const logFileRow = card.logFile ? 1 : 0;
491
- const headerOverhead = 6 + prCount + logFileRow;
518
+ const hasPrMeta = card.column === "done" && (reviewers?.length || assignees?.length);
519
+ const prMetaRow = hasPrMeta ? 1 : 0;
520
+ const headerOverhead = 6 + prCount + logFileRow + prMetaRow;
492
521
  const bodyRows = Math.max(1, terminalRows - headerOverhead);
493
522
  const lines = useMemo(() => processOutputLines(card.outputLog), [card.outputLog]);
494
523
  const startLine = Math.max(0, lines.length - bodyRows - logScrollOffset);
@@ -530,7 +559,7 @@ function IssueDetail({ card, onBack }) {
530
559
  ] }),
531
560
  /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", children: [
532
561
  isRunning && elapsedDisplay && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", marginRight: 2, children: [
533
- /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
562
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: /* @__PURE__ */ jsx4(Spinner3, { type: "dots" }) }),
534
563
  /* @__PURE__ */ jsx4(Text4, { color: "yellow", bold: true, children: ` ${elapsedDisplay}` })
535
564
  ] }),
536
565
  isPausedInProgress && elapsedDisplay && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", marginRight: 2, children: [
@@ -548,6 +577,17 @@ function IssueDetail({ card, onBack }) {
548
577
  /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: card.prUrls.length === 1 ? "PR: " : `PR ${i + 1}: ` }),
549
578
  /* @__PURE__ */ jsx4(Text4, { color: "yellow", wrap: "truncate", children: hyperlink(url, truncateLine(url, maxLineWidth - 5)) })
550
579
  ] }, url)),
580
+ hasPrMeta && /* @__PURE__ */ jsxs4(Box4, { marginTop: 0, flexDirection: "row", children: [
581
+ reviewers?.length ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
582
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", dimColor: true, children: "REVIEWERS: " }),
583
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: reviewers.join(", ") })
584
+ ] }) : null,
585
+ reviewers?.length && assignees?.length ? /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: " \u2502 " }) : null,
586
+ assignees?.length ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
587
+ /* @__PURE__ */ jsx4(Text4, { color: "green", dimColor: true, children: "ASSIGNEES: " }),
588
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: assignees.map((a) => a === "self" ? "you" : a).join(", ") })
589
+ ] }) : null
590
+ ] }),
551
591
  card.logFile && /* @__PURE__ */ jsx4(Box4, { marginTop: 0, children: /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, wrap: "truncate", children: `LOG: ${truncateLine(card.logFile, maxLineWidth - 5)}` }) }),
552
592
  /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: separator }) }),
553
593
  /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", justifyContent: "space-between", children: [
@@ -559,7 +599,7 @@ function IssueDetail({ card, onBack }) {
559
599
  !userScrolled && totalLines > bodyRows && /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "live" })
560
600
  ] }),
561
601
  /* @__PURE__ */ jsx4(Box4, { height: bodyRows, flexDirection: "column", overflow: "hidden", children: card.outputLog.length === 0 ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", marginTop: 1, children: [
562
- /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
602
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: /* @__PURE__ */ jsx4(Spinner3, { type: "dots" }) }),
563
603
  /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: " Waiting for provider output..." })
564
604
  ] }) : visibleLines.map((line, i) => {
565
605
  const color = logLineColor(line);
@@ -575,7 +615,7 @@ function IssueDetail({ card, onBack }) {
575
615
 
576
616
  // src/ui/plan-chat.tsx
577
617
  import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
578
- import Spinner3 from "ink-spinner";
618
+ import Spinner4 from "ink-spinner";
579
619
  import { useEffect as useEffect4, useState as useState4 } from "react";
580
620
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
581
621
  var SIDEBAR_TOTAL_WIDTH = 30;
@@ -702,7 +742,7 @@ function PlanChat({ messages, isThinking, onSend, onCancel }) {
702
742
  );
703
743
  }),
704
744
  isThinking && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "row", marginTop: 0, children: [
705
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: /* @__PURE__ */ jsx5(Spinner3, { type: "dots" }) }),
745
+ /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: /* @__PURE__ */ jsx5(Spinner4, { type: "dots" }) }),
706
746
  /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: " Analyzing..." })
707
747
  ] })
708
748
  ] }),
@@ -875,7 +915,7 @@ function PlanReview({ goal, issues, selectedIndex }) {
875
915
  import { existsSync } from "fs";
876
916
  import { basename, join } from "path";
877
917
  import { Box as Box8, Text as Text8 } from "ink";
878
- import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
918
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
879
919
  function Sidebar({
880
920
  provider,
881
921
  model,
@@ -890,7 +930,9 @@ function Sidebar({
890
930
  mergeConfirm = null,
891
931
  merging = null,
892
932
  updateInfo = null,
893
- workComplete = null
933
+ workComplete = null,
934
+ reviewers,
935
+ assignees
894
936
  }) {
895
937
  const dir = basename(cwd).toUpperCase();
896
938
  const cwdLabel = existsSync(join(cwd, ".git")) ? "REPOSITORY" : "WORKSPACE";
@@ -916,10 +958,10 @@ function Sidebar({
916
958
  ] }),
917
959
  /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D" })
918
960
  ] }),
919
- /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: activeView === "idle" || activeView === "empty" ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
961
+ /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: activeView === "idle" || activeView === "empty" ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
920
962
  /* @__PURE__ */ jsx8(Text8, { color: "gray", children: "\u25C7 " }),
921
963
  /* @__PURE__ */ jsx8(Text8, { color: "gray", bold: true, children: "IDLE" })
922
- ] }) : /* @__PURE__ */ jsxs8(Fragment2, { children: [
964
+ ] }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
923
965
  /* @__PURE__ */ jsx8(Text8, { color: paused ? "yellow" : "green", children: paused ? "\u23F8 " : "\u25B6 " }),
924
966
  /* @__PURE__ */ jsx8(Text8, { color: paused ? "yellow" : "green", bold: true, children: paused ? "PAUSED" : "RUNNING" })
925
967
  ] }) }),
@@ -989,7 +1031,24 @@ function Sidebar({
989
1031
  canMerge && !merging && !mergeConfirm && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "[m] merge" }),
990
1032
  merging && /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u23F3 merging..." }),
991
1033
  mergeConfirm && /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u26A0 CI not passed\n merge? [y/n]" }),
992
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "[Esc] back" })
1034
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "[Esc] back" }),
1035
+ hasPrUrl && (reviewers?.length || assignees?.length) ? /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
1036
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
1037
+ reviewers?.length ? /* @__PURE__ */ jsx8(
1038
+ Text8,
1039
+ {
1040
+ dimColor: true,
1041
+ children: `\u{1F440} ${reviewers.length} reviewer${reviewers.length > 1 ? "s" : ""}`
1042
+ }
1043
+ ) : null,
1044
+ assignees?.length ? /* @__PURE__ */ jsx8(
1045
+ Text8,
1046
+ {
1047
+ dimColor: true,
1048
+ children: `\u{1F464} ${assignees.map((a) => a === "self" ? "you" : a).join(", ")}`
1049
+ }
1050
+ ) : null
1051
+ ] }) : null
993
1052
  ] }),
994
1053
  activeView === "watching" && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "[q] quit" }) }),
995
1054
  activeView === "watch-prompt" && /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
@@ -1027,10 +1086,7 @@ function Sidebar({
1027
1086
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1028
1087
  function KanbanApp({ config, initialCards = [] }) {
1029
1088
  const { exit } = useApp();
1030
- const { cards, isEmpty, isWatching, isWatchPrompt, workComplete, modelInUse } = useKanbanState(
1031
- config.bell ?? true,
1032
- initialCards
1033
- );
1089
+ const { cards, isEmpty, isFetching, isWatching, isWatchPrompt, workComplete, modelInUse } = useKanbanState(config.bell ?? true, initialCards);
1034
1090
  const { rows } = useTerminalSize();
1035
1091
  const [activeView, setActiveView] = useState6("board");
1036
1092
  const [activeColIndex, setActiveColIndex] = useState6(0);
@@ -1150,7 +1206,7 @@ function KanbanApp({ config, initialCards = [] }) {
1150
1206
  }, [cards, selectedCardId, activeView]);
1151
1207
  const selectedCard = activeView === "detail" && selectedCardId ? cards.find((c) => c.id === selectedCardId) ?? null : null;
1152
1208
  async function handleMergeRequest(card) {
1153
- const { checkPrCiStatus } = await import("./merge-LRFZXFV2.js");
1209
+ const { checkPrCiStatus } = await import("./merge-CFQO7VU4.js");
1154
1210
  const prUrl = card.prUrls[0];
1155
1211
  const ciStatus = await checkPrCiStatus(prUrl);
1156
1212
  if (ciStatus === "passing" || ciStatus === "unknown") {
@@ -1163,7 +1219,7 @@ function KanbanApp({ config, initialCards = [] }) {
1163
1219
  const card = cards.find((c) => c.id === issueId);
1164
1220
  if (!card || card.prUrls.length === 0) return;
1165
1221
  setMerging(issueId);
1166
- const { mergePr } = await import("./merge-LRFZXFV2.js");
1222
+ const { mergePr } = await import("./merge-CFQO7VU4.js");
1167
1223
  let allSuccess = true;
1168
1224
  for (const prUrl of card.prUrls) {
1169
1225
  const result = await mergePr(prUrl);
@@ -1378,6 +1434,7 @@ Merge failed: ${result.error}
1378
1434
  let sidebarMode = "board";
1379
1435
  if (isWatchPrompt) sidebarMode = "watch-prompt";
1380
1436
  else if (isWatching) sidebarMode = "watching";
1437
+ else if (isFetching && activeView === "board") sidebarMode = "empty";
1381
1438
  else if (isEmpty && activeView === "board" && cards.length === 0) sidebarMode = "empty";
1382
1439
  else if (isEmpty && activeView === "board" && cards.length > 0) sidebarMode = "idle";
1383
1440
  else if (activeView === "plan-chat") sidebarMode = "plan-chat";
@@ -1402,7 +1459,9 @@ Merge failed: ${result.error}
1402
1459
  mergeConfirm,
1403
1460
  merging,
1404
1461
  updateInfo,
1405
- workComplete
1462
+ workComplete,
1463
+ reviewers: config.pr?.reviewers,
1464
+ assignees: config.pr?.assignees
1406
1465
  }
1407
1466
  ),
1408
1467
  activeView === "plan-chat" ? /* @__PURE__ */ jsx9(
@@ -1459,6 +1518,7 @@ Merge failed: ${result.error}
1459
1518
  columns: { backlog, inProgress, done },
1460
1519
  labels,
1461
1520
  isEmpty,
1521
+ isFetching,
1462
1522
  isWatching,
1463
1523
  isWatchPrompt,
1464
1524
  workComplete,
@@ -1473,7 +1533,9 @@ Merge failed: ${result.error}
1473
1533
  onBack: () => {
1474
1534
  setActiveView("board");
1475
1535
  setSelectedCardId(null);
1476
- }
1536
+ },
1537
+ reviewers: config.pr?.reviewers,
1538
+ assignees: config.pr?.assignees
1477
1539
  }
1478
1540
  )
1479
1541
  ] });
@@ -3,15 +3,15 @@ import {
3
3
  checkoutBaseBranches,
4
4
  runDemoLoop,
5
5
  runLoop
6
- } from "./chunk-45ZNECZ5.js";
6
+ } from "./chunk-23Z5BBRT.js";
7
7
  import {
8
8
  WATCH_POLL_INTERVAL_MS
9
- } from "./chunk-RQTH257A.js";
10
- import "./chunk-66TB6NMR.js";
9
+ } from "./chunk-SC222P5D.js";
10
+ import "./chunk-XLAY4P45.js";
11
11
  import "./chunk-3EOEDL3T.js";
12
12
  import "./chunk-7OCDGYDM.js";
13
13
  import "./chunk-72CYGBT4.js";
14
- import "./chunk-72DVXSHO.js";
14
+ import "./chunk-YMV4CBQE.js";
15
15
  import "./chunk-7JT7DTSS.js";
16
16
  import "./chunk-DGL33SDZ.js";
17
17
  export {
@@ -71,7 +71,9 @@ async function mergePr(prUrl) {
71
71
  }
72
72
  async function mergeGitHubPr(prUrl) {
73
73
  try {
74
- await execa("gh", ["pr", "merge", prUrl, "--delete-branch"], { timeout: 3e4 });
74
+ await execa("gh", ["pr", "merge", prUrl, "--squash", "--delete-branch"], {
75
+ timeout: 3e4
76
+ });
75
77
  return { success: true };
76
78
  } catch (err) {
77
79
  return { success: false, error: formatError(err) };
@@ -6,23 +6,23 @@ import {
6
6
  markdownToIssue,
7
7
  parseStructuredOutput,
8
8
  savePlan
9
- } from "./chunk-YTHUJQKB.js";
9
+ } from "./chunk-6XC7GQUI.js";
10
10
  import {
11
11
  createSource,
12
12
  resolveModels,
13
13
  runWithFallback
14
- } from "./chunk-RQTH257A.js";
14
+ } from "./chunk-SC222P5D.js";
15
15
  import {
16
16
  error,
17
17
  kanbanEmitter,
18
18
  log,
19
19
  ok,
20
20
  warn
21
- } from "./chunk-66TB6NMR.js";
21
+ } from "./chunk-XLAY4P45.js";
22
22
  import "./chunk-3EOEDL3T.js";
23
23
  import "./chunk-7OCDGYDM.js";
24
24
  import "./chunk-72CYGBT4.js";
25
- import "./chunk-72DVXSHO.js";
25
+ import "./chunk-YMV4CBQE.js";
26
26
  import "./chunk-7JT7DTSS.js";
27
27
 
28
28
  // src/plan/tui-bridge.ts
@@ -133,6 +133,9 @@ async function handleApproval(config, issues, goal) {
133
133
  } catch (err) {
134
134
  warn(`Could not refresh kanban: ${err instanceof Error ? err.message : String(err)}`);
135
135
  }
136
+ if (createdIds.length > 0) {
137
+ kanbanEmitter.emit("loop:run");
138
+ }
136
139
  }
137
140
  function buildChatPrompt(goal, config, chatHistory) {
138
141
  const basePrompt = buildPlanningPrompt(goal, config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.31.0",
3
+ "version": "1.33.0",
4
4
  "description": "Autonomous issue resolver",
5
5
  "keywords": [
6
6
  "loop",