@tarcisiopgs/lisa 1.31.0 → 1.32.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-NHC3JG2C.js";
33
34
  import {
34
35
  divider,
35
36
  error,
@@ -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,7 +4,7 @@ import {
4
4
  readContext,
5
5
  resolveModels,
6
6
  runWithFallback
7
- } from "./chunk-RQTH257A.js";
7
+ } from "./chunk-NHC3JG2C.js";
8
8
  import {
9
9
  error,
10
10
  log,
@@ -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,
@@ -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";
@@ -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-K2ILRYXS.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-AAWXKEV7.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-NHC3JG2C.js";
52
52
  import {
53
53
  banner,
54
54
  error,
@@ -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-PXUNTCQS.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-F27DQLKJ.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-WKKIHWJT.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-F27DQLKJ.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();
@@ -130,9 +130,12 @@ function Card({
130
130
  card.column === "in_progress" ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", marginTop: 0, children: [
131
131
  isPausedInProgress ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u23F8" }) : /* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
132
132
  /* @__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)
133
+ ] }) : card.column === "done" && card.startedAt !== void 0 && card.finishedAt !== void 0 ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [
134
+ /* @__PURE__ */ jsxs(Text, { color: card.merged ? "magenta" : "green", children: [
135
+ "\u2714 ",
136
+ formatElapsed(card.finishedAt - card.startedAt)
137
+ ] }),
138
+ card.prUrls.length > 0 && /* @__PURE__ */ jsx(Text, { color: card.merged ? "magenta" : "yellow", dimColor: true, children: card.merged ? "PR\u2714" : "PR" })
136
139
  ] }) : 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
140
  // Empty row for backlog and done-without-timing — maintains CARD_HEIGHT
138
141
  /* @__PURE__ */ jsx(Text, { children: " ".repeat(cardWidth) })
@@ -390,7 +393,7 @@ function logLineColor(line) {
390
393
  }
391
394
 
392
395
  // src/ui/detail.tsx
393
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
396
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
394
397
  function openUrl(url) {
395
398
  const platform = process.platform;
396
399
  let command;
@@ -443,7 +446,7 @@ function statusLabel(column, hasError, killed, skipped, merged) {
443
446
  if (column === "done") return { text: "DONE", color: "green" };
444
447
  return { text: "QUEUED", color: "white" };
445
448
  }
446
- function IssueDetail({ card, onBack }) {
449
+ function IssueDetail({ card, onBack, reviewers, assignees }) {
447
450
  const [now, setNow] = useState3(Date.now());
448
451
  const [logScrollOffset, setLogScrollOffset] = useState3(0);
449
452
  const [userScrolled, setUserScrolled] = useState3(false);
@@ -488,7 +491,9 @@ function IssueDetail({ card, onBack }) {
488
491
  const maxLineWidth = Math.max(1, terminalCols - SIDEBAR_TOTAL_WIDTH4 - 4);
489
492
  const prCount = card.prUrls.length > 0 ? card.prUrls.length : 0;
490
493
  const logFileRow = card.logFile ? 1 : 0;
491
- const headerOverhead = 6 + prCount + logFileRow;
494
+ const hasPrMeta = card.column === "done" && (reviewers?.length || assignees?.length);
495
+ const prMetaRow = hasPrMeta ? 1 : 0;
496
+ const headerOverhead = 6 + prCount + logFileRow + prMetaRow;
492
497
  const bodyRows = Math.max(1, terminalRows - headerOverhead);
493
498
  const lines = useMemo(() => processOutputLines(card.outputLog), [card.outputLog]);
494
499
  const startLine = Math.max(0, lines.length - bodyRows - logScrollOffset);
@@ -548,6 +553,17 @@ function IssueDetail({ card, onBack }) {
548
553
  /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: card.prUrls.length === 1 ? "PR: " : `PR ${i + 1}: ` }),
549
554
  /* @__PURE__ */ jsx4(Text4, { color: "yellow", wrap: "truncate", children: hyperlink(url, truncateLine(url, maxLineWidth - 5)) })
550
555
  ] }, url)),
556
+ hasPrMeta && /* @__PURE__ */ jsxs4(Box4, { marginTop: 0, flexDirection: "row", children: [
557
+ reviewers?.length ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
558
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", dimColor: true, children: "REVIEWERS: " }),
559
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: reviewers.join(", ") })
560
+ ] }) : null,
561
+ reviewers?.length && assignees?.length ? /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: " \u2502 " }) : null,
562
+ assignees?.length ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
563
+ /* @__PURE__ */ jsx4(Text4, { color: "green", dimColor: true, children: "ASSIGNEES: " }),
564
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: assignees.map((a) => a === "self" ? "you" : a).join(", ") })
565
+ ] }) : null
566
+ ] }),
551
567
  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
568
  /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: separator }) }),
553
569
  /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", justifyContent: "space-between", children: [
@@ -875,7 +891,7 @@ function PlanReview({ goal, issues, selectedIndex }) {
875
891
  import { existsSync } from "fs";
876
892
  import { basename, join } from "path";
877
893
  import { Box as Box8, Text as Text8 } from "ink";
878
- import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
894
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
879
895
  function Sidebar({
880
896
  provider,
881
897
  model,
@@ -890,7 +906,9 @@ function Sidebar({
890
906
  mergeConfirm = null,
891
907
  merging = null,
892
908
  updateInfo = null,
893
- workComplete = null
909
+ workComplete = null,
910
+ reviewers,
911
+ assignees
894
912
  }) {
895
913
  const dir = basename(cwd).toUpperCase();
896
914
  const cwdLabel = existsSync(join(cwd, ".git")) ? "REPOSITORY" : "WORKSPACE";
@@ -916,10 +934,10 @@ function Sidebar({
916
934
  ] }),
917
935
  /* @__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
936
  ] }),
919
- /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: activeView === "idle" || activeView === "empty" ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
937
+ /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: activeView === "idle" || activeView === "empty" ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
920
938
  /* @__PURE__ */ jsx8(Text8, { color: "gray", children: "\u25C7 " }),
921
939
  /* @__PURE__ */ jsx8(Text8, { color: "gray", bold: true, children: "IDLE" })
922
- ] }) : /* @__PURE__ */ jsxs8(Fragment2, { children: [
940
+ ] }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
923
941
  /* @__PURE__ */ jsx8(Text8, { color: paused ? "yellow" : "green", children: paused ? "\u23F8 " : "\u25B6 " }),
924
942
  /* @__PURE__ */ jsx8(Text8, { color: paused ? "yellow" : "green", bold: true, children: paused ? "PAUSED" : "RUNNING" })
925
943
  ] }) }),
@@ -989,7 +1007,24 @@ function Sidebar({
989
1007
  canMerge && !merging && !mergeConfirm && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "[m] merge" }),
990
1008
  merging && /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u23F3 merging..." }),
991
1009
  mergeConfirm && /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u26A0 CI not passed\n merge? [y/n]" }),
992
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "[Esc] back" })
1010
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "[Esc] back" }),
1011
+ hasPrUrl && (reviewers?.length || assignees?.length) ? /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
1012
+ /* @__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" }),
1013
+ reviewers?.length ? /* @__PURE__ */ jsx8(
1014
+ Text8,
1015
+ {
1016
+ dimColor: true,
1017
+ children: `\u{1F440} ${reviewers.length} reviewer${reviewers.length > 1 ? "s" : ""}`
1018
+ }
1019
+ ) : null,
1020
+ assignees?.length ? /* @__PURE__ */ jsx8(
1021
+ Text8,
1022
+ {
1023
+ dimColor: true,
1024
+ children: `\u{1F464} ${assignees.map((a) => a === "self" ? "you" : a).join(", ")}`
1025
+ }
1026
+ ) : null
1027
+ ] }) : null
993
1028
  ] }),
994
1029
  activeView === "watching" && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "[q] quit" }) }),
995
1030
  activeView === "watch-prompt" && /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
@@ -1402,7 +1437,9 @@ Merge failed: ${result.error}
1402
1437
  mergeConfirm,
1403
1438
  merging,
1404
1439
  updateInfo,
1405
- workComplete
1440
+ workComplete,
1441
+ reviewers: config.pr?.reviewers,
1442
+ assignees: config.pr?.assignees
1406
1443
  }
1407
1444
  ),
1408
1445
  activeView === "plan-chat" ? /* @__PURE__ */ jsx9(
@@ -1473,7 +1510,9 @@ Merge failed: ${result.error}
1473
1510
  onBack: () => {
1474
1511
  setActiveView("board");
1475
1512
  setSelectedCardId(null);
1476
- }
1513
+ },
1514
+ reviewers: config.pr?.reviewers,
1515
+ assignees: config.pr?.assignees
1477
1516
  }
1478
1517
  )
1479
1518
  ] });
@@ -3,15 +3,15 @@ import {
3
3
  checkoutBaseBranches,
4
4
  runDemoLoop,
5
5
  runLoop
6
- } from "./chunk-45ZNECZ5.js";
6
+ } from "./chunk-AAWXKEV7.js";
7
7
  import {
8
8
  WATCH_POLL_INTERVAL_MS
9
- } from "./chunk-RQTH257A.js";
9
+ } from "./chunk-NHC3JG2C.js";
10
10
  import "./chunk-66TB6NMR.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 {
@@ -6,12 +6,12 @@ import {
6
6
  markdownToIssue,
7
7
  parseStructuredOutput,
8
8
  savePlan
9
- } from "./chunk-YTHUJQKB.js";
9
+ } from "./chunk-K2ILRYXS.js";
10
10
  import {
11
11
  createSource,
12
12
  resolveModels,
13
13
  runWithFallback
14
- } from "./chunk-RQTH257A.js";
14
+ } from "./chunk-NHC3JG2C.js";
15
15
  import {
16
16
  error,
17
17
  kanbanEmitter,
@@ -22,7 +22,7 @@ import {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "description": "Autonomous issue resolver",
5
5
  "keywords": [
6
6
  "loop",