@tarcisiopgs/lisa 1.12.3 → 1.13.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.
@@ -229,8 +229,10 @@ var GitHubIssuesSource = class {
229
229
  name = "github-issues";
230
230
  async fetchNextIssue(config) {
231
231
  const { owner, repo } = parseOwnerRepo(config.team);
232
- const labels = Array.isArray(config.label) ? config.label : [config.label];
233
- const label = labels.map((l) => encodeURIComponent(l)).join(",");
232
+ const validStates = ["open", "closed", "all"];
233
+ const isOrphanDetection = !!config.pick_from && !validStates.includes(config.pick_from);
234
+ const filterLabels = isOrphanDetection ? [config.pick_from] : Array.isArray(config.label) ? config.label : [config.label];
235
+ const label = filterLabels.map((l) => encodeURIComponent(l)).join(",");
234
236
  const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
235
237
  const issues = await githubGet(path);
236
238
  if (issues.length === 0) return null;
@@ -310,8 +312,36 @@ var GitHubIssuesSource = class {
310
312
  return null;
311
313
  }
312
314
  }
313
- async updateStatus(issueId, labelToAdd) {
315
+ async updateStatus(issueId, labelToAdd, config) {
314
316
  const ref = parseGitHubIssueNumber(issueId);
317
+ if (config && config.in_progress !== config.pick_from) {
318
+ const filterLabels = Array.isArray(config.label) ? config.label : [config.label];
319
+ const isMovingToInProgress = labelToAdd === config.in_progress;
320
+ if (isMovingToInProgress) {
321
+ await githubPost(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
322
+ labels: [labelToAdd]
323
+ });
324
+ for (const label of filterLabels) {
325
+ try {
326
+ await githubDelete(
327
+ `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(label)}`
328
+ );
329
+ } catch {
330
+ }
331
+ }
332
+ return;
333
+ }
334
+ await githubPost(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
335
+ labels: filterLabels
336
+ });
337
+ try {
338
+ await githubDelete(
339
+ `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(config.in_progress)}`
340
+ );
341
+ } catch {
342
+ }
343
+ return;
344
+ }
315
345
  await githubPost(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
316
346
  labels: [labelToAdd]
317
347
  });
@@ -322,7 +352,7 @@ var GitHubIssuesSource = class {
322
352
  body: `Pull request: ${prUrl}`
323
353
  });
324
354
  }
325
- async completeIssue(issueId, _status, labelToRemove) {
355
+ async completeIssue(issueId, _status, labelToRemove, config) {
326
356
  const ref = parseGitHubIssueNumber(issueId);
327
357
  await githubPatch(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`, {
328
358
  state: "closed"
@@ -330,6 +360,14 @@ var GitHubIssuesSource = class {
330
360
  if (labelToRemove) {
331
361
  await this.removeLabel(issueId, labelToRemove);
332
362
  }
363
+ if (config && config.in_progress !== config.pick_from) {
364
+ try {
365
+ await githubDelete(
366
+ `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(config.in_progress)}`
367
+ );
368
+ } catch {
369
+ }
370
+ }
333
371
  }
334
372
  async listIssues(config) {
335
373
  const { owner, repo } = parseOwnerRepo(config.team);
@@ -436,8 +474,10 @@ var GitLabIssuesSource = class {
436
474
  name = "gitlab-issues";
437
475
  async fetchNextIssue(config) {
438
476
  const project = parseGitLabProject(config.team);
439
- const labelsArr = Array.isArray(config.label) ? config.label : [config.label];
440
- const label = labelsArr.map((l) => encodeURIComponent(l)).join(",");
477
+ const validStates = ["opened", "closed", "all"];
478
+ const isOrphanDetection = !!config.pick_from && !validStates.includes(config.pick_from);
479
+ const filterLabels = isOrphanDetection ? [config.pick_from] : Array.isArray(config.label) ? config.label : [config.label];
480
+ const label = filterLabels.map((l) => encodeURIComponent(l)).join(",");
441
481
  const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
442
482
  const issues = await gitlabGet(path);
443
483
  if (issues.length === 0) return null;
@@ -500,10 +540,30 @@ var GitLabIssuesSource = class {
500
540
  return null;
501
541
  }
502
542
  }
503
- async updateStatus(issueId, labelToAdd) {
543
+ async updateStatus(issueId, labelToAdd, config) {
504
544
  const { project, iid } = splitIssueId(issueId);
505
545
  const encodedProject = parseGitLabProject(project);
506
546
  const issue = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
547
+ if (config && config.in_progress !== config.pick_from) {
548
+ const filterLabels = Array.isArray(config.label) ? config.label : [config.label];
549
+ const isMovingToInProgress = labelToAdd === config.in_progress;
550
+ if (isMovingToInProgress) {
551
+ const updated2 = [.../* @__PURE__ */ new Set([...issue.labels, labelToAdd])].filter(
552
+ (l) => !filterLabels.includes(l)
553
+ );
554
+ await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
555
+ labels: updated2.join(",")
556
+ });
557
+ return;
558
+ }
559
+ const updated = [.../* @__PURE__ */ new Set([...issue.labels, ...filterLabels])].filter(
560
+ (l) => l !== config.in_progress
561
+ );
562
+ await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
563
+ labels: updated.join(",")
564
+ });
565
+ return;
566
+ }
507
567
  const labels = [.../* @__PURE__ */ new Set([...issue.labels, labelToAdd])];
508
568
  await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, { labels: labels.join(",") });
509
569
  }
@@ -514,11 +574,14 @@ var GitLabIssuesSource = class {
514
574
  body: `Pull request: ${prUrl}`
515
575
  });
516
576
  }
517
- async completeIssue(issueId, _status, labelToRemove) {
577
+ async completeIssue(issueId, _status, labelToRemove, config) {
518
578
  const { project, iid } = splitIssueId(issueId);
519
579
  const encodedProject = parseGitLabProject(project);
520
580
  const issue = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
521
- const labels = labelToRemove ? issue.labels.filter((l) => l.toLowerCase() !== labelToRemove.toLowerCase()) : issue.labels;
581
+ let labels = labelToRemove ? issue.labels.filter((l) => l.toLowerCase() !== labelToRemove.toLowerCase()) : issue.labels;
582
+ if (config && config.in_progress !== config.pick_from) {
583
+ labels = labels.filter((l) => l !== config.in_progress);
584
+ }
522
585
  await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
523
586
  state_event: "close",
524
587
  labels: labels.join(",")
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  startSpinner,
38
38
  stopSpinner,
39
39
  warn
40
- } from "./chunk-UKRCQYHM.js";
40
+ } from "./chunk-JSRHJYX2.js";
41
41
 
42
42
  // src/telemetry.ts
43
43
  import { readFileSync } from "fs";
@@ -404,7 +404,8 @@ import { execa as execa2 } from "execa";
404
404
  var WORKTREES_DIR = ".worktrees";
405
405
  function generateBranchName(issueId, title) {
406
406
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").substring(0, 40).replace(/^-|-$/g, "");
407
- return `feat/${issueId.toLowerCase()}-${slug}`;
407
+ const safeId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
408
+ return `feat/${safeId}-${slug}`;
408
409
  }
409
410
  async function cleanupOrphanedWorktree(repoRoot, branchName) {
410
411
  const { stdout: branchList } = await execa2("git", ["branch", "--list", branchName], {
@@ -3652,7 +3653,7 @@ var PlaneSource = class {
3652
3653
  const data = await planeGet(
3653
3654
  `/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${stateId}&per_page=100`
3654
3655
  );
3655
- const matching = data.results.filter((i) => labelIds.every((lid) => i.labels.includes(lid)));
3656
+ const matching = data.results.filter((i) => i.state === stateId).filter((i) => labelIds.every((lid) => i.labels.includes(lid)));
3656
3657
  if (matching.length === 0) return null;
3657
3658
  const allStates = await fetchAll(
3658
3659
  `/workspaces/${workspaceSlug}/projects/${projectId}/states/`
@@ -3756,7 +3757,7 @@ var PlaneSource = class {
3756
3757
  const data = await planeGet(
3757
3758
  `/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${stateId}&per_page=100`
3758
3759
  );
3759
- return data.results.filter((i) => labelIds.every((lid) => i.labels.includes(lid))).map((i) => {
3760
+ return data.results.filter((i) => i.state === stateId).filter((i) => labelIds.every((lid) => i.labels.includes(lid))).map((i) => {
3760
3761
  const webUrl = `${getAppUrl()}/${workspaceSlug}/projects/${projectId}/issues/${i.id}`;
3761
3762
  return {
3762
3763
  id: makeIssueId(workspaceSlug, projectId, i.id),
@@ -3814,6 +3815,10 @@ async function shortcutPost(path, body) {
3814
3815
  async function shortcutPut(path, body) {
3815
3816
  return shortcutFetch("PUT", path, body);
3816
3817
  }
3818
+ function extractStories(result) {
3819
+ if (Array.isArray(result)) return result;
3820
+ return result.data ?? [];
3821
+ }
3817
3822
  async function resolveWorkflowStateId(stateName) {
3818
3823
  const workflows = await shortcutGet("/api/v3/workflows");
3819
3824
  for (const workflow of workflows) {
@@ -3875,12 +3880,15 @@ var ShortcutSource = class {
3875
3880
  const seen = /* @__PURE__ */ new Set();
3876
3881
  const allStories = [];
3877
3882
  for (const stateId of stateIds) {
3878
- const searchResult = await shortcutPost("/api/v3/stories/search", {
3879
- workflow_state_id: stateId,
3880
- label_name: primaryLabel,
3881
- archived: false
3882
- });
3883
- for (const story2 of searchResult.data ?? []) {
3883
+ const searchResult = await shortcutPost(
3884
+ "/api/v3/stories/search",
3885
+ {
3886
+ workflow_state_id: stateId,
3887
+ label_name: primaryLabel,
3888
+ archived: false
3889
+ }
3890
+ );
3891
+ for (const story2 of extractStories(searchResult)) {
3884
3892
  if (!seen.has(story2.id)) {
3885
3893
  seen.add(story2.id);
3886
3894
  allStories.push(story2);
@@ -3976,12 +3984,15 @@ var ShortcutSource = class {
3976
3984
  const seen = /* @__PURE__ */ new Set();
3977
3985
  const allStories = [];
3978
3986
  for (const stateId of stateIds) {
3979
- const searchResult = await shortcutPost("/api/v3/stories/search", {
3980
- workflow_state_id: stateId,
3981
- label_name: primaryLabel,
3982
- archived: false
3983
- });
3984
- for (const story of searchResult.data ?? []) {
3987
+ const searchResult = await shortcutPost(
3988
+ "/api/v3/stories/search",
3989
+ {
3990
+ workflow_state_id: stateId,
3991
+ label_name: primaryLabel,
3992
+ archived: false
3993
+ }
3994
+ );
3995
+ for (const story of extractStories(searchResult)) {
3985
3996
  if (!seen.has(story.id)) {
3986
3997
  seen.add(story.id);
3987
3998
  allStories.push(story);
@@ -4410,10 +4421,10 @@ function installSignalHandlers() {
4410
4421
  }
4411
4422
  }
4412
4423
  const revertPromises = [...activeCleanups.entries()].map(
4413
- async ([issueId, { previousStatus, source }]) => {
4424
+ async ([issueId, { previousStatus, source, sourceConfig }]) => {
4414
4425
  try {
4415
4426
  await Promise.race([
4416
- source.updateStatus(issueId, previousStatus),
4427
+ source.updateStatus(issueId, previousStatus, sourceConfig),
4417
4428
  new Promise(
4418
4429
  (_, reject) => setTimeout(() => reject(new Error("Revert timed out")), 5e3)
4419
4430
  )
@@ -4483,7 +4494,7 @@ async function recoverOrphanIssues(source, config2) {
4483
4494
  `Found orphan issue ${orphan.id} stuck in "${config2.source_config.in_progress}". Reverting to "${config2.source_config.pick_from}".`
4484
4495
  );
4485
4496
  try {
4486
- await source.updateStatus(orphan.id, config2.source_config.pick_from);
4497
+ await source.updateStatus(orphan.id, config2.source_config.pick_from, config2.source_config);
4487
4498
  ok(`Recovered orphan ${orphan.id}`);
4488
4499
  } catch (err) {
4489
4500
  error(
@@ -4643,12 +4654,12 @@ async function runSequentialLoop(config2, source, models, workspace, opts) {
4643
4654
  try {
4644
4655
  const inProgress = config2.source_config.in_progress;
4645
4656
  kanbanEmitter.emit("issue:started", issue2.id);
4646
- await source.updateStatus(issue2.id, inProgress);
4657
+ await source.updateStatus(issue2.id, inProgress, config2.source_config);
4647
4658
  ok(`Moved ${issue2.id} to "${inProgress}"`);
4648
4659
  } catch (err) {
4649
4660
  warn(`Failed to update status: ${err instanceof Error ? err.message : String(err)}`);
4650
4661
  }
4651
- activeCleanups.set(issue2.id, { previousStatus, source });
4662
+ activeCleanups.set(issue2.id, { previousStatus, source, sourceConfig: config2.source_config });
4652
4663
  let sessionResult;
4653
4664
  try {
4654
4665
  sessionResult = config2.workflow === "worktree" ? await runWorktreeSession(config2, issue2, logFile, session, models) : await runBranchSession(config2, issue2, logFile, session, models);
@@ -4658,7 +4669,7 @@ async function runSequentialLoop(config2, source, models, workspace, opts) {
4658
4669
  `Unhandled error in session for ${issue2.id}: ${err instanceof Error ? err.message : String(err)}`
4659
4670
  );
4660
4671
  try {
4661
- await source.updateStatus(issue2.id, previousStatus);
4672
+ await source.updateStatus(issue2.id, previousStatus, config2.source_config);
4662
4673
  ok(`Reverted ${issue2.id} to "${previousStatus}"`);
4663
4674
  } catch (revertErr) {
4664
4675
  error(
@@ -4753,12 +4764,12 @@ async function runConcurrentLoop(config2, source, models, workspace, opts) {
4753
4764
  const previousStatus = config2.source_config.pick_from;
4754
4765
  try {
4755
4766
  kanbanEmitter.emit("issue:started", issue2.id);
4756
- await source.updateStatus(issue2.id, config2.source_config.in_progress);
4767
+ await source.updateStatus(issue2.id, config2.source_config.in_progress, config2.source_config);
4757
4768
  ok(`Moved ${issue2.id} to "${config2.source_config.in_progress}"`);
4758
4769
  } catch (err) {
4759
4770
  warn(`Failed to update status: ${err instanceof Error ? err.message : String(err)}`);
4760
4771
  }
4761
- activeCleanups.set(issue2.id, { previousStatus, source });
4772
+ activeCleanups.set(issue2.id, { previousStatus, source, sourceConfig: config2.source_config });
4762
4773
  let sessionResult;
4763
4774
  try {
4764
4775
  sessionResult = await runWorktreeSession(config2, issue2, logFile, session, models);
@@ -4767,7 +4778,7 @@ async function runConcurrentLoop(config2, source, models, workspace, opts) {
4767
4778
  `Unhandled error in session for ${issue2.id}: ${err instanceof Error ? err.message : String(err)}`
4768
4779
  );
4769
4780
  try {
4770
- await source.updateStatus(issue2.id, previousStatus);
4781
+ await source.updateStatus(issue2.id, previousStatus, config2.source_config);
4771
4782
  ok(`Reverted ${issue2.id} to "${previousStatus}"`);
4772
4783
  } catch (revertErr) {
4773
4784
  error(
@@ -4882,7 +4893,7 @@ async function handleSessionResult(sessionResult, issue2, previousStatus, source
4882
4893
  providerPausedSet.delete(issue2.id);
4883
4894
  warn(`Issue ${issue2.id} killed by user.`);
4884
4895
  try {
4885
- await source.updateStatus(issue2.id, previousStatus);
4896
+ await source.updateStatus(issue2.id, previousStatus, config2.source_config);
4886
4897
  ok(`Reverted ${issue2.id} to "${previousStatus}"`);
4887
4898
  } catch (err) {
4888
4899
  error(
@@ -4898,7 +4909,7 @@ async function handleSessionResult(sessionResult, issue2, previousStatus, source
4898
4909
  providerPausedSet.delete(issue2.id);
4899
4910
  warn(`Issue ${issue2.id} skipped by user.`);
4900
4911
  try {
4901
- await source.updateStatus(issue2.id, previousStatus);
4912
+ await source.updateStatus(issue2.id, previousStatus, config2.source_config);
4902
4913
  ok(`Reverted ${issue2.id} to "${previousStatus}"`);
4903
4914
  } catch (err) {
4904
4915
  error(
@@ -4913,7 +4924,7 @@ async function handleSessionResult(sessionResult, issue2, previousStatus, source
4913
4924
  error(`All models failed for ${issue2.id}. Reverting to "${previousStatus}".`);
4914
4925
  logAttemptHistory(sessionResult);
4915
4926
  try {
4916
- await source.updateStatus(issue2.id, previousStatus);
4927
+ await source.updateStatus(issue2.id, previousStatus, config2.source_config);
4917
4928
  ok(`Reverted ${issue2.id} to "${previousStatus}"`);
4918
4929
  kanbanEmitter.emit("issue:reverted", issue2.id);
4919
4930
  } catch (err) {
@@ -4928,7 +4939,7 @@ async function handleSessionResult(sessionResult, issue2, previousStatus, source
4928
4939
  `Session succeeded but no PRs created for ${issue2.id}. Reverting to "${previousStatus}".`
4929
4940
  );
4930
4941
  try {
4931
- await source.updateStatus(issue2.id, previousStatus);
4942
+ await source.updateStatus(issue2.id, previousStatus, config2.source_config);
4932
4943
  ok(`Reverted ${issue2.id} to "${previousStatus}"`);
4933
4944
  kanbanEmitter.emit("issue:reverted", issue2.id);
4934
4945
  } catch (err) {
@@ -4956,7 +4967,7 @@ async function handleSessionResult(sessionResult, issue2, previousStatus, source
4956
4967
  try {
4957
4968
  const doneStatus = config2.source_config.done;
4958
4969
  const labelToRemove = opts.issueId ? void 0 : getRemoveLabel(config2.source_config);
4959
- await source.completeIssue(issue2.id, doneStatus, labelToRemove);
4970
+ await source.completeIssue(issue2.id, doneStatus, labelToRemove, config2.source_config);
4960
4971
  ok(`Updated ${issue2.id} status to "${doneStatus}"`);
4961
4972
  if (labelToRemove) {
4962
4973
  ok(`Removed label "${labelToRemove}" from ${issue2.id}`);
@@ -5760,7 +5771,7 @@ Add them to your ${shell} and run: source ${shell}`));
5760
5771
  if (isTTY) {
5761
5772
  const { render } = await import("ink");
5762
5773
  const { createElement } = await import("react");
5763
- const { KanbanApp } = await import("./kanban-THY72UON.js");
5774
+ const { KanbanApp } = await import("./kanban-MJCCS3ZP.js");
5764
5775
  render(createElement(KanbanApp, { config: merged }), { exitOnCtrlC: false });
5765
5776
  }
5766
5777
  await runLoop(merged, {
@@ -5,7 +5,7 @@ import {
5
5
  startSpinner,
6
6
  stopSpinner,
7
7
  useKanbanState
8
- } from "./chunk-UKRCQYHM.js";
8
+ } from "./chunk-JSRHJYX2.js";
9
9
 
10
10
  // src/ui/kanban.tsx
11
11
  import { Box as Box7, useApp, useInput as useInput2 } from "ink";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.12.3",
3
+ "version": "1.13.0",
4
4
  "description": "Autonomous issue resolver",
5
5
  "keywords": [
6
6
  "loop",