@tarcisiopgs/lisa 1.14.2 → 1.16.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
@@ -112,7 +112,7 @@ Set the tokens for your chosen source and PR platform:
112
112
  # PR platform
113
113
  GITHUB_TOKEN # GitHub (platform: cli or token)
114
114
  GITLAB_TOKEN # GitLab (platform: gitlab)
115
- BITBUCKET_TOKEN # Bitbucket (platform: bitbucket)
115
+ BITBUCKET_TOKEN # Bitbucket app password (platform: bitbucket)
116
116
  BITBUCKET_USERNAME # Bitbucket username
117
117
 
118
118
  # Issue sources
@@ -125,9 +125,14 @@ PLANE_BASE_URL # optional, defaults to https://api.plane.so
125
125
  GITLAB_TOKEN # source: gitlab-issues
126
126
  GITLAB_BASE_URL # optional, defaults to https://gitlab.com
127
127
  GITHUB_TOKEN # source: github-issues
128
- JIRA_BASE_URL # source: jira
128
+ JIRA_BASE_URL # source: jira (e.g. https://yourorg.atlassian.net)
129
129
  JIRA_EMAIL
130
- JIRA_API_TOKEN
130
+ JIRA_API_TOKEN # generate at id.atlassian.com — expires, regenerate if 401
131
+
132
+ # Aider provider (one of)
133
+ GEMINI_API_KEY
134
+ OPENAI_API_KEY
135
+ ANTHROPIC_API_KEY
131
136
  ```
132
137
 
133
138
  ---
@@ -196,6 +201,22 @@ validation:
196
201
  require_acceptance_criteria: true
197
202
  ```
198
203
 
204
+ ### Source-specific notes
205
+
206
+ **GitHub Issues / GitLab Issues** — `pick_from`, `in_progress`, and `done` are **labels**, not statuses. Make sure `in_progress` differs from `pick_from`; using the same value causes Lisa to re-pick issues that are already being worked on.
207
+
208
+ **Trello** — `team`, `pick_from`, `in_progress`, and `done` are list **names** (not IDs).
209
+
210
+ **Jira** — `team` is your project **key** (e.g. `ENG`). `JIRA_API_TOKEN` is generated at [id.atlassian.com](https://id.atlassian.com) and expires — regenerate if you get 401 errors.
211
+
212
+ **Goose** — `lisa init` asks which backend to use (gemini-cli, anthropic, openai, etc.) and saves it to config. No env vars needed. You can also set `GOOSE_PROVIDER` manually — it takes precedence over the config value.
213
+
214
+ **Aider** — requires a direct LLM API key (`GEMINI_API_KEY`, `OPENAI_API_KEY`, or `ANTHROPIC_API_KEY`). Does not support OAuth or cached credentials.
215
+
216
+ **OpenCode** — if `~/.config/opencode/config.json` contains MCP entries, remove them or set the file to `{}` to prevent OpenCode from hanging on startup.
217
+
218
+ ---
219
+
199
220
  ### Workflow Modes
200
221
 
201
222
  **Branch** — The agent creates a branch in your current checkout. Simple setup, works everywhere.
@@ -3,7 +3,7 @@
3
3
  // src/paths.ts
4
4
  import { createHash } from "crypto";
5
5
  import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "fs";
6
- import { homedir } from "os";
6
+ import { homedir, platform } from "os";
7
7
  import { join, resolve } from "path";
8
8
  var MAX_LOG_FILES = 20;
9
9
  function projectHash(cwd) {
@@ -11,7 +11,14 @@ function projectHash(cwd) {
11
11
  return createHash("sha256").update(absolute).digest("hex").slice(0, 12);
12
12
  }
13
13
  function getCacheDir(cwd) {
14
- const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
14
+ let base;
15
+ if (process.env.XDG_CACHE_HOME) {
16
+ base = process.env.XDG_CACHE_HOME;
17
+ } else if (platform() === "darwin") {
18
+ base = join(homedir(), "Library", "Caches");
19
+ } else {
20
+ base = join(homedir(), ".cache");
21
+ }
15
22
  return join(base, "lisa", projectHash(cwd));
16
23
  }
17
24
  function getLogsDir(cwd) {
@@ -193,7 +193,7 @@ var GitHubIssuesSource = class {
193
193
  const filterLabels = isOrphanDetection ? [config.pick_from] : Array.isArray(config.label) ? config.label : [config.label];
194
194
  const label = filterLabels.map((l) => encodeURIComponent(l)).join(",");
195
195
  const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
196
- const issues = await githubGet(path);
196
+ const issues = (await githubGet(path)).filter((i) => !i.pull_request);
197
197
  if (issues.length === 0) return null;
198
198
  const unblocked = [];
199
199
  const blocked = [];
@@ -333,7 +333,7 @@ var GitHubIssuesSource = class {
333
333
  const labels = Array.isArray(config.label) ? config.label : [config.label];
334
334
  const label = labels.map((l) => encodeURIComponent(l)).join(",");
335
335
  const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
336
- const issues = await githubGet(path);
336
+ const issues = (await githubGet(path)).filter((i) => !i.pull_request);
337
337
  return issues.map((issue) => ({
338
338
  id: makeIssueId(owner, repo, issue.number),
339
339
  title: issue.title,
@@ -448,10 +448,10 @@ var GitLabIssuesSource = class {
448
448
  );
449
449
  const activeBlockers = links.filter((link) => {
450
450
  if (link.link_type === "is_blocked_by") {
451
- return link.source.state !== "closed";
451
+ return link.state !== "closed";
452
452
  }
453
453
  return false;
454
- }).map((link) => link.source.iid);
454
+ }).map((link) => link.iid);
455
455
  if (activeBlockers.length === 0) {
456
456
  unblocked.push(issue2);
457
457
  } else {
@@ -640,6 +640,7 @@ function useKanbanState(bellEnabled) {
640
640
  const [cards, setCards] = useState([]);
641
641
  const [isEmpty, setIsEmpty] = useState(false);
642
642
  const [isWatching, setIsWatching] = useState(false);
643
+ const [isWatchPrompt, setIsWatchPrompt] = useState(false);
643
644
  const [workComplete, setWorkComplete] = useState(
644
645
  null
645
646
  );
@@ -763,10 +764,17 @@ function useKanbanState(bellEnabled) {
763
764
  const onComplete = (data) => setWorkComplete(data);
764
765
  const onWatching = () => setIsWatching(true);
765
766
  const onWatchResume = () => setIsWatching(false);
767
+ const onWatchPrompt = () => {
768
+ setIsWatchPrompt(true);
769
+ setIsWatching(false);
770
+ };
771
+ const onWatchPromptResolved = () => setIsWatchPrompt(false);
766
772
  kanbanEmitter.on("work:empty", onEmpty);
767
773
  kanbanEmitter.on("work:complete", onComplete);
768
774
  kanbanEmitter.on("work:watching", onWatching);
769
775
  kanbanEmitter.on("work:watch-resume", onWatchResume);
776
+ kanbanEmitter.on("work:watch-prompt", onWatchPrompt);
777
+ kanbanEmitter.on("work:watch-prompt-resolved", onWatchPromptResolved);
770
778
  const cleanupBell = registerBellListeners(bellEnabled);
771
779
  return () => {
772
780
  kanbanEmitter.off("issue:queued", onQueued);
@@ -784,13 +792,15 @@ function useKanbanState(bellEnabled) {
784
792
  kanbanEmitter.off("work:complete", onComplete);
785
793
  kanbanEmitter.off("work:watching", onWatching);
786
794
  kanbanEmitter.off("work:watch-resume", onWatchResume);
795
+ kanbanEmitter.off("work:watch-prompt", onWatchPrompt);
796
+ kanbanEmitter.off("work:watch-prompt-resolved", onWatchPromptResolved);
787
797
  cleanupBell();
788
798
  for (const issueId of activePolls.keys()) {
789
799
  stopMergePolling(issueId);
790
800
  }
791
801
  };
792
802
  }, [bellEnabled]);
793
- return { cards, isEmpty, isWatching, workComplete, modelInUse };
803
+ return { cards, isEmpty, isWatching, isWatchPrompt, workComplete, modelInUse };
794
804
  }
795
805
 
796
806
  export {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getGuardrailsPath
4
- } from "./chunk-OYQ6TOAG.js";
4
+ } from "./chunk-GZ2ZAQO4.js";
5
5
 
6
6
  // src/session/guardrails.ts
7
7
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -10,8 +10,8 @@ import {
10
10
  guardrailsPath,
11
11
  migrateGuardrails,
12
12
  readGuardrails
13
- } from "./chunk-NXGXGHS3.js";
14
- import "./chunk-OYQ6TOAG.js";
13
+ } from "./chunk-UQPR5OXK.js";
14
+ import "./chunk-GZ2ZAQO4.js";
15
15
  export {
16
16
  appendEntry,
17
17
  appendEntrySync,
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  extractContext,
7
7
  extractErrorType,
8
8
  migrateGuardrails
9
- } from "./chunk-NXGXGHS3.js";
9
+ } from "./chunk-UQPR5OXK.js";
10
10
  import {
11
11
  ensureCacheDir,
12
12
  getLogsDir,
@@ -14,7 +14,7 @@ import {
14
14
  getPlanPath,
15
15
  getPrCachePath,
16
16
  rotateLogFiles
17
- } from "./chunk-OYQ6TOAG.js";
17
+ } from "./chunk-GZ2ZAQO4.js";
18
18
  import {
19
19
  fetchPrFeedback,
20
20
  formatPrFeedbackEntry
@@ -32,7 +32,7 @@ import {
32
32
  ok,
33
33
  setOutputMode,
34
34
  warn
35
- } from "./chunk-Z6CNJAZF.js";
35
+ } from "./chunk-ITQEGO5A.js";
36
36
  import {
37
37
  notify,
38
38
  resetTitle,
@@ -53,7 +53,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
53
53
  import { resolve } from "path";
54
54
  import { parse, stringify } from "yaml";
55
55
  var DEFAULT_OVERSEER_CONFIG = {
56
- enabled: false,
56
+ enabled: true,
57
57
  check_interval: 30,
58
58
  stuck_threshold: 300
59
59
  };
@@ -363,10 +363,22 @@ function determineRepoPath(repos, issue2, workspace) {
363
363
  const first = repos[0];
364
364
  return first ? join(workspace, first.path) : void 0;
365
365
  }
366
+ async function hasCodeChanges(repoPath, baseBranch) {
367
+ try {
368
+ const { stdout } = await execa("git", ["diff", "--stat", `${baseBranch}..HEAD`], {
369
+ cwd: repoPath,
370
+ reject: false
371
+ });
372
+ const trimmed = stdout.trim();
373
+ return trimmed.length > 0;
374
+ } catch {
375
+ return false;
376
+ }
377
+ }
366
378
 
367
379
  // src/providers/aider.ts
368
380
  import { execSync } from "child_process";
369
- import { appendFileSync as appendFileSync2, mkdtempSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
381
+ import { appendFileSync as appendFileSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
370
382
  import { tmpdir } from "os";
371
383
  import { join as join2 } from "path";
372
384
 
@@ -520,7 +532,8 @@ var AIDER_API_KEY_ENV_VARS = [
520
532
  "COHERE_API_KEY",
521
533
  "MISTRAL_API_KEY",
522
534
  "DEEPSEEK_API_KEY",
523
- "AZURE_API_KEY"
535
+ "AZURE_API_KEY",
536
+ "XAI_API_KEY"
524
537
  ];
525
538
  var AiderProvider = class {
526
539
  name = "aider";
@@ -547,7 +560,7 @@ var AiderProvider = class {
547
560
  writeFileSync2(promptFile, prompt, "utf-8");
548
561
  try {
549
562
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
550
- const command = `aider --message "$(cat '${promptFile}')" --yes-always ${modelFlag}`;
563
+ const command = `aider --message-file '${promptFile}' --yes-always ${modelFlag}`;
551
564
  const { proc, isPty } = spawnWithPty(command, {
552
565
  cwd: opts.cwd,
553
566
  env: { ...process.env, ...opts.env }
@@ -601,7 +614,7 @@ var AiderProvider = class {
601
614
  };
602
615
  } finally {
603
616
  try {
604
- unlinkSync(promptFile);
617
+ rmSync2(tmpDir, { recursive: true, force: true });
605
618
  } catch {
606
619
  }
607
620
  }
@@ -610,7 +623,7 @@ var AiderProvider = class {
610
623
 
611
624
  // src/providers/claude.ts
612
625
  import { execSync as execSync2, spawn as spawn2 } from "child_process";
613
- import { appendFileSync as appendFileSync3, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
626
+ import { appendFileSync as appendFileSync3, mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
614
627
  import { tmpdir as tmpdir2 } from "os";
615
628
  import { join as join3 } from "path";
616
629
  var ClaudeProvider = class {
@@ -699,7 +712,7 @@ var ClaudeProvider = class {
699
712
  };
700
713
  } finally {
701
714
  try {
702
- unlinkSync2(promptFile);
715
+ rmSync3(tmpDir, { recursive: true, force: true });
703
716
  } catch {
704
717
  }
705
718
  }
@@ -708,14 +721,14 @@ var ClaudeProvider = class {
708
721
 
709
722
  // src/providers/codex.ts
710
723
  import { execSync as execSync3 } from "child_process";
711
- import { appendFileSync as appendFileSync4, mkdtempSync as mkdtempSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
724
+ import { appendFileSync as appendFileSync4, mkdtempSync as mkdtempSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
712
725
  import { tmpdir as tmpdir3 } from "os";
713
726
  import { join as join4 } from "path";
714
727
  var CodexProvider = class {
715
728
  name = "codex";
716
729
  async isAvailable() {
717
730
  try {
718
- execSync3("codex --version", { stdio: "ignore" });
731
+ execSync3("which codex", { stdio: "ignore" });
719
732
  return true;
720
733
  } catch {
721
734
  return false;
@@ -782,7 +795,7 @@ var CodexProvider = class {
782
795
  };
783
796
  } finally {
784
797
  try {
785
- unlinkSync3(promptFile);
798
+ rmSync4(tmpDir, { recursive: true, force: true });
786
799
  } catch {
787
800
  }
788
801
  }
@@ -791,7 +804,7 @@ var CodexProvider = class {
791
804
 
792
805
  // src/providers/copilot.ts
793
806
  import { execSync as execSync4 } from "child_process";
794
- import { appendFileSync as appendFileSync5, mkdtempSync as mkdtempSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
807
+ import { appendFileSync as appendFileSync5, mkdtempSync as mkdtempSync4, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
795
808
  import { tmpdir as tmpdir4 } from "os";
796
809
  import { join as join5 } from "path";
797
810
  var CopilotProvider = class {
@@ -810,7 +823,8 @@ var CopilotProvider = class {
810
823
  const promptFile = join5(tmpDir, "prompt.md");
811
824
  writeFileSync5(promptFile, prompt, "utf-8");
812
825
  try {
813
- const command = `copilot --allow-all -p "$(cat '${promptFile}')"`;
826
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
827
+ const command = `copilot --allow-all ${modelFlag} -p "$(cat '${promptFile}')"`;
814
828
  const { proc, isPty } = spawnWithPty(command, {
815
829
  cwd: opts.cwd,
816
830
  env: { ...process.env, ...opts.env }
@@ -864,7 +878,7 @@ var CopilotProvider = class {
864
878
  };
865
879
  } finally {
866
880
  try {
867
- unlinkSync4(promptFile);
881
+ rmSync5(tmpDir, { recursive: true, force: true });
868
882
  } catch {
869
883
  }
870
884
  }
@@ -873,13 +887,13 @@ var CopilotProvider = class {
873
887
 
874
888
  // src/providers/cursor.ts
875
889
  import { execSync as execSync5 } from "child_process";
876
- import { appendFileSync as appendFileSync6, mkdtempSync as mkdtempSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "fs";
890
+ import { appendFileSync as appendFileSync6, mkdtempSync as mkdtempSync5, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "fs";
877
891
  import { tmpdir as tmpdir5 } from "os";
878
892
  import { join as join6 } from "path";
879
893
  function findCursorBinary() {
880
894
  for (const bin of ["agent", "cursor-agent"]) {
881
895
  try {
882
- execSync5(`${bin} --version`, { stdio: "ignore" });
896
+ execSync5(`which ${bin}`, { stdio: "ignore" });
883
897
  return bin;
884
898
  } catch {
885
899
  }
@@ -888,12 +902,17 @@ function findCursorBinary() {
888
902
  }
889
903
  var CursorProvider = class {
890
904
  name = "cursor";
905
+ _bin = void 0;
906
+ resolveBin() {
907
+ if (this._bin === void 0) this._bin = findCursorBinary();
908
+ return this._bin;
909
+ }
891
910
  async isAvailable() {
892
- return findCursorBinary() !== null;
911
+ return this.resolveBin() !== null;
893
912
  }
894
913
  async run(prompt, opts) {
895
914
  const start = Date.now();
896
- const bin = findCursorBinary();
915
+ const bin = this.resolveBin();
897
916
  if (!bin) {
898
917
  return {
899
918
  success: false,
@@ -960,7 +979,7 @@ var CursorProvider = class {
960
979
  };
961
980
  } finally {
962
981
  try {
963
- unlinkSync5(promptFile);
982
+ rmSync6(tmpDir, { recursive: true, force: true });
964
983
  } catch {
965
984
  }
966
985
  }
@@ -969,7 +988,7 @@ var CursorProvider = class {
969
988
 
970
989
  // src/providers/gemini.ts
971
990
  import { execSync as execSync6 } from "child_process";
972
- import { appendFileSync as appendFileSync7, mkdtempSync as mkdtempSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "fs";
991
+ import { appendFileSync as appendFileSync7, mkdtempSync as mkdtempSync6, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "fs";
973
992
  import { tmpdir as tmpdir6 } from "os";
974
993
  import { join as join7 } from "path";
975
994
  var GEMINI_ERROR_PATTERN = /^Error (executing tool|generating content)/;
@@ -1044,7 +1063,7 @@ var GeminiProvider = class {
1044
1063
  };
1045
1064
  } finally {
1046
1065
  try {
1047
- unlinkSync6(promptFile);
1066
+ rmSync7(tmpDir, { recursive: true, force: true });
1048
1067
  } catch {
1049
1068
  }
1050
1069
  }
@@ -1053,7 +1072,7 @@ var GeminiProvider = class {
1053
1072
 
1054
1073
  // src/providers/goose.ts
1055
1074
  import { execSync as execSync7 } from "child_process";
1056
- import { appendFileSync as appendFileSync8, mkdtempSync as mkdtempSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync8 } from "fs";
1075
+ import { appendFileSync as appendFileSync8, mkdtempSync as mkdtempSync7, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
1057
1076
  import { tmpdir as tmpdir7 } from "os";
1058
1077
  import { join as join8 } from "path";
1059
1078
  var GooseProvider = class {
@@ -1128,7 +1147,7 @@ var GooseProvider = class {
1128
1147
  };
1129
1148
  } finally {
1130
1149
  try {
1131
- unlinkSync7(promptFile);
1150
+ rmSync8(tmpDir, { recursive: true, force: true });
1132
1151
  } catch {
1133
1152
  }
1134
1153
  }
@@ -1137,7 +1156,7 @@ var GooseProvider = class {
1137
1156
 
1138
1157
  // src/providers/opencode.ts
1139
1158
  import { execSync as execSync8 } from "child_process";
1140
- import { appendFileSync as appendFileSync9, mkdtempSync as mkdtempSync8, unlinkSync as unlinkSync8, writeFileSync as writeFileSync9 } from "fs";
1159
+ import { appendFileSync as appendFileSync9, mkdtempSync as mkdtempSync8, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
1141
1160
  import { tmpdir as tmpdir8 } from "os";
1142
1161
  import { join as join9 } from "path";
1143
1162
  var OpenCodeProvider = class {
@@ -1156,7 +1175,8 @@ var OpenCodeProvider = class {
1156
1175
  const promptFile = join9(tmpDir, "prompt.md");
1157
1176
  writeFileSync9(promptFile, prompt, "utf-8");
1158
1177
  try {
1159
- const command = `opencode run "$(cat '${promptFile}')"`;
1178
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
1179
+ const command = `opencode run ${modelFlag} "$(cat '${promptFile}')"`;
1160
1180
  const { proc, isPty } = spawnWithPty(command, {
1161
1181
  cwd: opts.cwd,
1162
1182
  env: { ...process.env, ...opts.env }
@@ -1210,7 +1230,7 @@ var OpenCodeProvider = class {
1210
1230
  };
1211
1231
  } finally {
1212
1232
  try {
1213
- unlinkSync8(promptFile);
1233
+ rmSync9(tmpDir, { recursive: true, force: true });
1214
1234
  } catch {
1215
1235
  }
1216
1236
  }
@@ -1255,15 +1275,16 @@ var ELIGIBLE_ERROR_PATTERNS = [
1255
1275
  /ECONNRESET/,
1256
1276
  /ENOTFOUND/,
1257
1277
  /fetch failed/i,
1258
- /timeout/i,
1259
- /timed?\s*out/i,
1278
+ /\btimeout\b/i,
1279
+ /\btimed?\s*out\b/i,
1260
1280
  /network.?error/i,
1261
1281
  /not installed/i,
1262
1282
  /not in PATH/i,
1263
1283
  /command not found/i,
1264
1284
  /lisa-overseer/i,
1265
1285
  /named models unavailable/i,
1266
- /free plans can only use/i
1286
+ /free plans can only use/i,
1287
+ /empty commit/i
1267
1288
  ];
1268
1289
  function isEligibleForFallback(output) {
1269
1290
  return ELIGIBLE_ERROR_PATTERNS.some((pattern) => pattern.test(output));
@@ -1521,9 +1542,12 @@ async function deleteProviderComments(prUrl) {
1521
1542
  const [, owner, repo, prNumber] = match;
1522
1543
  const { stdout } = await execa2("gh", [
1523
1544
  "api",
1545
+ "--paginate",
1546
+ "--jq",
1547
+ ".[]",
1524
1548
  `/repos/${owner}/${repo}/issues/${prNumber}/comments`
1525
1549
  ]);
1526
- const comments = JSON.parse(stdout);
1550
+ const comments = stdout.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
1527
1551
  for (const comment of comments) {
1528
1552
  if (PROVIDER_ATTRIBUTION_RE.test(comment.body)) {
1529
1553
  try {
@@ -1559,7 +1583,7 @@ async function appendPrAttribution(prUrl, providerUsed) {
1559
1583
  // src/cli/detection.ts
1560
1584
  function getVersion() {
1561
1585
  try {
1562
- const pkgPath = resolvePath(new URL(".", import.meta.url).pathname, "../../package.json");
1586
+ const pkgPath = resolvePath(new URL(".", import.meta.url).pathname, "../package.json");
1563
1587
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1564
1588
  return pkg.version;
1565
1589
  } catch {
@@ -1568,7 +1592,7 @@ function getVersion() {
1568
1592
  }
1569
1593
  var CURSOR_FREE_PLAN_ERROR = "Free plans can only use Auto";
1570
1594
  async function isCursorFreePlan() {
1571
- const { mkdtempSync: mkdtempSync9, unlinkSync: unlinkSync12, writeFileSync: writeFileSync11 } = await import("fs");
1595
+ const { mkdtempSync: mkdtempSync9, unlinkSync: unlinkSync4, writeFileSync: writeFileSync11 } = await import("fs");
1572
1596
  const tmpDir = mkdtempSync9(join10(tmpdir9(), "lisa-cursor-check-"));
1573
1597
  const promptFile = join10(tmpDir, "prompt.txt");
1574
1598
  writeFileSync11(promptFile, "test", "utf-8");
@@ -1593,7 +1617,7 @@ async function isCursorFreePlan() {
1593
1617
  return errorOutput.includes(CURSOR_FREE_PLAN_ERROR);
1594
1618
  } finally {
1595
1619
  try {
1596
- unlinkSync12(promptFile);
1620
+ unlinkSync4(promptFile);
1597
1621
  } catch {
1598
1622
  }
1599
1623
  try {
@@ -1899,9 +1923,50 @@ async function runConfigWizard(existing) {
1899
1923
  if (clack2.isCancel(selected)) return process.exit(0);
1900
1924
  providerName = selected;
1901
1925
  }
1926
+ let gooseProvider;
1927
+ if (providerName === "goose") {
1928
+ const gooseProviderAnswer = await clack2.select({
1929
+ message: "Which backend should Goose use?",
1930
+ initialValue: initial?.provider_options?.goose?.goose_provider ?? process.env.GOOSE_PROVIDER ?? "gemini-cli",
1931
+ options: [
1932
+ { value: "gemini-cli", label: "Gemini CLI", hint: "requires Gemini CLI installed" },
1933
+ { value: "anthropic", label: "Anthropic", hint: "requires ANTHROPIC_API_KEY" },
1934
+ { value: "openai", label: "OpenAI", hint: "requires OPENAI_API_KEY" },
1935
+ { value: "google", label: "Google (direct)", hint: "requires GOOGLE_API_KEY" },
1936
+ { value: "ollama", label: "Ollama", hint: "local models" }
1937
+ ]
1938
+ });
1939
+ if (clack2.isCancel(gooseProviderAnswer)) return process.exit(0);
1940
+ gooseProvider = gooseProviderAnswer;
1941
+ } else if (providerName === "aider") {
1942
+ clack2.log.info(
1943
+ `Aider requires a direct LLM API key in your environment.
1944
+ Set one of: ${pc.bold("GEMINI_API_KEY")}, ${pc.bold("OPENAI_API_KEY")}, ${pc.bold("ANTHROPIC_API_KEY")}, etc.
1945
+ Aider does not use OAuth or cached credentials.`
1946
+ );
1947
+ } else if (providerName === "opencode") {
1948
+ clack2.log.info(
1949
+ `OpenCode tip: if you have MCP entries in ${pc.cyan("~/.config/opencode/config.json")},
1950
+ remove them or set the file to ${pc.cyan("{}")} \u2014 MCP tools can cause OpenCode to hang.`
1951
+ );
1952
+ }
1902
1953
  let selectedModels = [];
1903
1954
  let availableModels = providerModels[providerName];
1904
- if (providerName === "cursor") {
1955
+ if (providerName === "goose" && gooseProvider) {
1956
+ const gooseModelsByBackend = {
1957
+ "gemini-cli": [
1958
+ "gemini-2.5-pro",
1959
+ "gemini-2.5-flash",
1960
+ "gemini-2.0-flash",
1961
+ "gemini-2.5-flash-lite"
1962
+ ],
1963
+ anthropic: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5", "claude-sonnet-4-5"],
1964
+ openai: ["gpt-4o", "gpt-4o-mini", "o3", "o4-mini"],
1965
+ google: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"],
1966
+ ollama: ["llama3.3", "qwen2.5-coder", "mistral"]
1967
+ };
1968
+ availableModels = gooseModelsByBackend[gooseProvider] ?? [];
1969
+ } else if (providerName === "cursor") {
1905
1970
  const isFree = await isCursorFreePlan();
1906
1971
  if (isFree) {
1907
1972
  availableModels = ["auto"];
@@ -2056,21 +2121,29 @@ Then reload: ${pc.cyan(`source ${shell}`)}`
2056
2121
  });
2057
2122
  if (clack2.isCancel(projectAnswer)) return process.exit(0);
2058
2123
  project = projectAnswer;
2124
+ const isLabelBasedSource = source === "github-issues" || source === "gitlab-issues";
2059
2125
  const pickFromAnswer = await clack2.text({
2060
- message: "Pick up issues in which status?",
2061
- initialValue: initial?.source_config.pick_from ?? "Backlog",
2062
- placeholder: "e.g. Backlog, Todo"
2126
+ message: isLabelBasedSource ? "Pick up issues in which state? (open, closed, or a label name)" : "Pick up issues in which status?",
2127
+ initialValue: initial?.source_config.pick_from ?? (isLabelBasedSource ? "open" : "Backlog"),
2128
+ placeholder: isLabelBasedSource ? "e.g. open" : "e.g. Backlog, Todo"
2063
2129
  });
2064
2130
  if (clack2.isCancel(pickFromAnswer)) return process.exit(0);
2065
2131
  pickFrom = pickFromAnswer;
2066
2132
  const inProgressAnswer = await clack2.text({
2067
- message: "Move to which status while the agent is working?",
2068
- initialValue: initial?.source_config.in_progress ?? "In Progress"
2133
+ message: isLabelBasedSource ? "Which label to apply while the agent is working? (must differ from pick_from label)" : "Move to which status while the agent is working?",
2134
+ initialValue: initial?.source_config.in_progress ?? "In Progress",
2135
+ placeholder: isLabelBasedSource ? "e.g. in-progress" : void 0
2069
2136
  });
2070
2137
  if (clack2.isCancel(inProgressAnswer)) return process.exit(0);
2071
2138
  inProgress = inProgressAnswer;
2139
+ if (isLabelBasedSource && inProgress === pickFrom) {
2140
+ clack2.log.warning(
2141
+ `"in_progress" label is the same as "pick_from" label ("${pickFrom}").
2142
+ This will cause Lisa to re-pick the issue on recovery. Consider using a different label.`
2143
+ );
2144
+ }
2072
2145
  const doneAnswer = await clack2.text({
2073
- message: "Move to which status after the PR is created?",
2146
+ message: isLabelBasedSource ? "Which label to apply after the PR is created?" : "Move to which status after the PR is created?",
2074
2147
  initialValue: initial?.source_config.done ?? "In Review"
2075
2148
  });
2076
2149
  if (clack2.isCancel(doneAnswer)) return process.exit(0);
@@ -2131,7 +2204,10 @@ Then reload: ${pc.cyan(`source ${shell}`)}`
2131
2204
  provider: providerName,
2132
2205
  provider_options: {
2133
2206
  ...initial?.provider_options || {},
2134
- [providerName]: { models: selectedModels }
2207
+ [providerName]: {
2208
+ models: selectedModels,
2209
+ ...gooseProvider ? { goose_provider: gooseProvider } : {}
2210
+ }
2135
2211
  },
2136
2212
  source,
2137
2213
  source_config: {
@@ -2204,8 +2280,8 @@ var feedback = defineCommand2({
2204
2280
  },
2205
2281
  async run({ args }) {
2206
2282
  const { fetchPrFeedback: fetchPrFeedback2, formatPrFeedbackEntry: formatPrFeedbackEntry2 } = await import("./pr-feedback-DGHNP3E7.js");
2207
- const { appendRawEntrySync } = await import("./guardrails-KI5NEJVE.js");
2208
- const { ensureCacheDir: ensureCacheDir2 } = await import("./paths-HQQDKACV.js");
2283
+ const { appendRawEntrySync } = await import("./guardrails-I5ACG5LQ.js");
2284
+ const { ensureCacheDir: ensureCacheDir2 } = await import("./paths-WQN4NBC6.js");
2209
2285
  const prUrl = args.pr;
2210
2286
  const issueId = args.issue ?? "unknown";
2211
2287
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -2768,14 +2844,14 @@ var LinearSource = class {
2768
2844
  `mutation($teamId: String!, $name: String!) {
2769
2845
  issueLabelCreate(input: { teamId: $teamId, name: $name }) {
2770
2846
  success
2771
- label { id name }
2847
+ issueLabel { id name }
2772
2848
  }
2773
2849
  }`,
2774
2850
  { teamId: issueData.issue.team.id, name: labelName }
2775
2851
  );
2776
- if (created.issueLabelCreate.success && created.issueLabelCreate.label) {
2852
+ if (created.issueLabelCreate.success && created.issueLabelCreate.issueLabel) {
2777
2853
  log(`Label "${labelName}" created automatically in team ${issueData.issue.team.id}`);
2778
- teamLabel = created.issueLabelCreate.label;
2854
+ teamLabel = created.issueLabelCreate.issueLabel;
2779
2855
  } else {
2780
2856
  const refetch = await gql(
2781
2857
  `query($identifier: String!) {
@@ -2976,7 +3052,7 @@ var PlaneSource = class {
2976
3052
  labelNames.map((name) => resolveLabelId(workspaceSlug, projectId, name))
2977
3053
  );
2978
3054
  const data = await planeGet(
2979
- `/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${stateId}&per_page=100`
3055
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/?state=${stateId}&per_page=100`
2980
3056
  );
2981
3057
  const matching = data.results.filter((i) => i.state === stateId).filter((i) => labelIds.every((lid) => i.labels.includes(lid)));
2982
3058
  if (matching.length === 0) return null;
@@ -2989,14 +3065,14 @@ var PlaneSource = class {
2989
3065
  const blocked = [];
2990
3066
  for (const issue3 of matching) {
2991
3067
  const relations = await fetchAll(
2992
- `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issue3.id}/relations/`
3068
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${issue3.id}/relations/`
2993
3069
  );
2994
3070
  const blockerIds = relations.filter((r) => r.relation_type === "blocked_by").map((r) => r.related_issue);
2995
3071
  const activeBlockers = [];
2996
3072
  for (const blockerId of blockerIds) {
2997
3073
  try {
2998
3074
  const blocker = await planeGet(
2999
- `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${blockerId}/`
3075
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${blockerId}/`
3000
3076
  );
3001
3077
  if (!doneStateIds.has(blocker.state)) {
3002
3078
  activeBlockers.push(blockerId);
@@ -3037,7 +3113,7 @@ var PlaneSource = class {
3037
3113
  try {
3038
3114
  const { workspaceSlug, projectId, issueId } = parseIssueId(id);
3039
3115
  const issue2 = await planeGet(
3040
- `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`
3116
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${issueId}/`
3041
3117
  );
3042
3118
  const webUrl = `${getAppUrl()}/${workspaceSlug}/projects/${projectId}/issues/${issue2.id}`;
3043
3119
  return {
@@ -3054,14 +3130,14 @@ var PlaneSource = class {
3054
3130
  const { workspaceSlug, projectId, issueId: planeIssueId } = parseIssueId(issueId);
3055
3131
  const stateId = await resolveStateId(workspaceSlug, projectId, stateName);
3056
3132
  await planePatch(
3057
- `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${planeIssueId}/`,
3133
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${planeIssueId}/`,
3058
3134
  { state: stateId }
3059
3135
  );
3060
3136
  }
3061
3137
  async attachPullRequest(issueId, prUrl) {
3062
3138
  const { workspaceSlug, projectId, issueId: planeIssueId } = parseIssueId(issueId);
3063
3139
  await planePost(
3064
- `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${planeIssueId}/comments/`,
3140
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${planeIssueId}/comments/`,
3065
3141
  { comment_html: `<p>Pull request: <a href="${prUrl}">${prUrl}</a></p>` }
3066
3142
  );
3067
3143
  }
@@ -3080,7 +3156,7 @@ var PlaneSource = class {
3080
3156
  labelNames.map((name) => resolveLabelId(workspaceSlug, projectId, name))
3081
3157
  );
3082
3158
  const data = await planeGet(
3083
- `/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${stateId}&per_page=100`
3159
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/?state=${stateId}&per_page=100`
3084
3160
  );
3085
3161
  return data.results.filter((i) => i.state === stateId).filter((i) => labelIds.every((lid) => i.labels.includes(lid))).map((i) => {
3086
3162
  const webUrl = `${getAppUrl()}/${workspaceSlug}/projects/${projectId}/issues/${i.id}`;
@@ -3095,14 +3171,14 @@ var PlaneSource = class {
3095
3171
  async removeLabel(issueId, labelName) {
3096
3172
  const { workspaceSlug, projectId, issueId: planeIssueId } = parseIssueId(issueId);
3097
3173
  const issue2 = await planeGet(
3098
- `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${planeIssueId}/`
3174
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${planeIssueId}/`
3099
3175
  );
3100
3176
  const labels = await fetchLabels(workspaceSlug, projectId);
3101
3177
  const labelObj = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
3102
3178
  if (!labelObj || !issue2.labels.includes(labelObj.id)) return;
3103
3179
  const updatedLabels = issue2.labels.filter((lid) => lid !== labelObj.id);
3104
3180
  await planePatch(
3105
- `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${planeIssueId}/`,
3181
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${planeIssueId}/`,
3106
3182
  { labels: updatedLabels }
3107
3183
  );
3108
3184
  }
@@ -3144,6 +3220,24 @@ function extractStories(result) {
3144
3220
  if (Array.isArray(result)) return result;
3145
3221
  return result.data ?? [];
3146
3222
  }
3223
+ function extractNext(result) {
3224
+ if (Array.isArray(result)) return null;
3225
+ return result.next ?? null;
3226
+ }
3227
+ async function searchStoriesAll(body) {
3228
+ const all = [];
3229
+ let next = null;
3230
+ do {
3231
+ const req = next ? { ...body, next } : body;
3232
+ const result = await shortcutPost(
3233
+ "/api/v3/stories/search",
3234
+ req
3235
+ );
3236
+ all.push(...extractStories(result));
3237
+ next = extractNext(result);
3238
+ } while (next);
3239
+ return all;
3240
+ }
3147
3241
  async function resolveWorkflowStateId(stateName) {
3148
3242
  const workflows = await shortcutGet("/api/v3/workflows");
3149
3243
  for (const workflow of workflows) {
@@ -3205,15 +3299,11 @@ var ShortcutSource = class {
3205
3299
  const seen = /* @__PURE__ */ new Set();
3206
3300
  const allStories = [];
3207
3301
  for (const stateId of stateIds) {
3208
- const searchResult = await shortcutPost(
3209
- "/api/v3/stories/search",
3210
- {
3211
- workflow_state_id: stateId,
3212
- label_name: primaryLabel,
3213
- archived: false
3214
- }
3215
- );
3216
- for (const story2 of extractStories(searchResult)) {
3302
+ for (const story2 of await searchStoriesAll({
3303
+ workflow_state_id: stateId,
3304
+ label_name: primaryLabel,
3305
+ archived: false
3306
+ })) {
3217
3307
  if (!seen.has(story2.id)) {
3218
3308
  seen.add(story2.id);
3219
3309
  allStories.push(story2);
@@ -3309,15 +3399,11 @@ var ShortcutSource = class {
3309
3399
  const seen = /* @__PURE__ */ new Set();
3310
3400
  const allStories = [];
3311
3401
  for (const stateId of stateIds) {
3312
- const searchResult = await shortcutPost(
3313
- "/api/v3/stories/search",
3314
- {
3315
- workflow_state_id: stateId,
3316
- label_name: primaryLabel,
3317
- archived: false
3318
- }
3319
- );
3320
- for (const story of extractStories(searchResult)) {
3402
+ for (const story of await searchStoriesAll({
3403
+ workflow_state_id: stateId,
3404
+ label_name: primaryLabel,
3405
+ archived: false
3406
+ })) {
3321
3407
  if (!seen.has(story.id)) {
3322
3408
  seen.add(story.id);
3323
3409
  allStories.push(story);
@@ -3686,6 +3772,7 @@ var userKilledSet = /* @__PURE__ */ new Set();
3686
3772
  var userSkippedSet = /* @__PURE__ */ new Set();
3687
3773
  var _shuttingDown = false;
3688
3774
  var _loopPaused = false;
3775
+ var _userQuitFromWatchPrompt = false;
3689
3776
  function isShuttingDown() {
3690
3777
  return _shuttingDown;
3691
3778
  }
@@ -3695,6 +3782,9 @@ function setShuttingDown(value) {
3695
3782
  function isLoopPaused() {
3696
3783
  return _loopPaused;
3697
3784
  }
3785
+ function hasUserQuitFromWatchPrompt() {
3786
+ return _userQuitFromWatchPrompt;
3787
+ }
3698
3788
  function killProviderForIssue(issueId) {
3699
3789
  const pid = activeProviderPids.get(issueId);
3700
3790
  if (!pid) return;
@@ -3792,6 +3882,10 @@ function setupEventListeners() {
3792
3882
  }
3793
3883
  }
3794
3884
  });
3885
+ kanbanEmitter.on("loop:quit", () => {
3886
+ _userQuitFromWatchPrompt = true;
3887
+ setShuttingDown(true);
3888
+ });
3795
3889
  }
3796
3890
 
3797
3891
  // src/loop/helpers.ts
@@ -3888,6 +3982,7 @@ async function injectRejectedPrFeedback(workspace, issueId, prUrls) {
3888
3982
  clearPrUrl(workspace, issueId);
3889
3983
  }
3890
3984
  async function recoverOrphanIssues(source, config2) {
3985
+ if (!config2.source_config.in_progress) return;
3891
3986
  if (config2.source_config.in_progress === config2.source_config.pick_from) return;
3892
3987
  const orphanConfig = {
3893
3988
  ...config2.source_config,
@@ -4588,6 +4683,7 @@ async function appendPrAttribution2(prUrl, providerUsed) {
4588
4683
  if (!getRes.ok) return;
4589
4684
  const prData = await getRes.json();
4590
4685
  const currentDescription = prData.description ?? "";
4686
+ const currentTitle = prData.title ?? "";
4591
4687
  const providerName = formatProviderName2(providerUsed);
4592
4688
  const attribution = `
4593
4689
 
@@ -4600,7 +4696,7 @@ async function appendPrAttribution2(prUrl, providerUsed) {
4600
4696
  Authorization: authHeader,
4601
4697
  "Content-Type": "application/json"
4602
4698
  },
4603
- body: JSON.stringify({ description: newDescription }),
4699
+ body: JSON.stringify({ description: newDescription, title: currentTitle }),
4604
4700
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS6)
4605
4701
  });
4606
4702
  } catch {
@@ -4701,7 +4797,7 @@ function buildPrCreateInstruction(platform2, targetBranch) {
4701
4797
  REPO=$(git remote get-url origin | sed 's/.*bitbucket\\.org[:/][^/]*\\///;s/\\.git$//')
4702
4798
  BRANCH=$(git branch --show-current)
4703
4799
  curl -X POST \\
4704
- -H "Authorization: Bearer $BITBUCKET_TOKEN" \\
4800
+ -H "Authorization: Basic $(printf '%s:%s' "$BITBUCKET_USERNAME" "$BITBUCKET_TOKEN" | base64)" \\
4705
4801
  -H "Content-Type: application/json" \\
4706
4802
  "https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO/pullrequests" \\
4707
4803
  --data "{\\"title\\":\\"<conventional-commit-title>\\",\\"description\\":\\"<markdown-summary>\\",\\"source\\":{\\"branch\\":{\\"name\\":\\"$BRANCH\\"}},\\"destination\\":{\\"branch\\":{\\"name\\":\\"${dest}\\"}}}"
@@ -4820,6 +4916,7 @@ function buildRulesSection(env, variant = "issue", extraRules = "") {
4820
4916
  - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
4821
4917
  - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
4822
4918
  - Do NOT install new dependencies unless the issue explicitly requires it.
4919
+ - Do NOT use documentation lookup MCP tools (e.g., Context7, codesearch, Exa) \u2014 they have free-tier rate limits that will block your execution. Read files directly from the repository. Web search is allowed only when strictly necessary (e.g., looking up an external API format not available in the codebase).
4823
4920
  ${envRule}${extraRules}- If you get stuck or the issue is unclear, STOP and explain why.
4824
4921
  ${scopeRule}
4825
4922
  - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
@@ -5151,14 +5248,17 @@ ${generatorBlock}
5151
5248
 
5152
5249
  2. **Determine execution order**: If multiple repos are affected, decide the order. Repos that produce APIs, schemas, or shared libraries should come first. Repos that consume them should come later.
5153
5250
 
5154
- 3. **Write the plan**: Create \`${resolvedPlanPath}\` with JSON:
5155
- \`\`\`json
5156
- {
5157
- "steps": [
5158
- { "repoPath": "<absolute path to repo>", "scope": "<what to implement in this repo>", "order": 1 },
5159
- { "repoPath": "<absolute path to repo>", "scope": "<what to implement in this repo>", "order": 2 }
5160
- ]
5161
- }
5251
+ 3. **Write the plan file to disk**: Use a bash command or file-write tool to write the plan to \`${resolvedPlanPath}\`.
5252
+ **You MUST write the file to disk. Do NOT print the JSON to stdout or in a code block.**
5253
+
5254
+ The file must be valid JSON with this structure (replace angle-bracket placeholders with real values):
5255
+ - \`repoPath\`: absolute path to the affected repository
5256
+ - \`scope\`: concise English description of what to implement in that repo
5257
+ - \`order\`: integer starting at 1 (lower = executes first)
5258
+
5259
+ Use your write_file tool, or a bash command such as:
5260
+ \`\`\`bash
5261
+ printf '%s' '{"steps":[{"repoPath":"/absolute/path","scope":"description of work","order":1}]}' > '${resolvedPlanPath}'
5162
5262
  \`\`\`
5163
5263
 
5164
5264
  ## Rules
@@ -5597,19 +5697,20 @@ function registerCleanup() {
5597
5697
  }
5598
5698
 
5599
5699
  // src/loop/manifest.ts
5600
- import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync9 } from "fs";
5700
+ import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync } from "fs";
5601
5701
  function readLisaManifest(cwd, issueId) {
5602
5702
  const manifestPath = getManifestPath(cwd, issueId);
5603
5703
  if (!existsSync8(manifestPath)) return null;
5604
5704
  try {
5605
5705
  return JSON.parse(readFileSync8(manifestPath, "utf-8").trim());
5606
5706
  } catch {
5707
+ warn(`Failed to parse manifest at ${manifestPath} \u2014 agent may not have written it correctly`);
5607
5708
  return null;
5608
5709
  }
5609
5710
  }
5610
5711
  function cleanupManifest(cwd, issueId) {
5611
5712
  try {
5612
- unlinkSync9(getManifestPath(cwd, issueId));
5713
+ unlinkSync(getManifestPath(cwd, issueId));
5613
5714
  } catch {
5614
5715
  }
5615
5716
  }
@@ -5643,14 +5744,13 @@ function readPlanFile(filePath) {
5643
5744
  }
5644
5745
 
5645
5746
  // src/loop/multi-repo-session.ts
5646
- import { appendFileSync as appendFileSync10, unlinkSync as unlinkSync10 } from "fs";
5747
+ import { appendFileSync as appendFileSync10, unlinkSync as unlinkSync2 } from "fs";
5647
5748
  import { join as join14, resolve as resolve7 } from "path";
5648
5749
  async function runWorktreeMultiRepoSession(config2, issue2, logFile, session, models) {
5649
5750
  const workspace = resolve7(config2.workspace);
5650
- const safeId = issue2.id.replace(/[^a-zA-Z0-9_-]/g, "_");
5651
- const planPath = join14(workspace, `.lisa-plan-${safeId}.json`);
5751
+ const planPath = getPlanPath(workspace, issue2.id);
5652
5752
  try {
5653
- unlinkSync10(planPath);
5753
+ unlinkSync2(planPath);
5654
5754
  } catch {
5655
5755
  }
5656
5756
  initLogFile(logFile);
@@ -5689,7 +5789,7 @@ ${planResult.output}
5689
5789
  if (!planResult.success) {
5690
5790
  error(`Planning phase failed for ${issue2.id}. Check ${logFile}`);
5691
5791
  try {
5692
- unlinkSync10(planPath);
5792
+ unlinkSync2(planPath);
5693
5793
  } catch {
5694
5794
  }
5695
5795
  activeProviderPids.delete(issue2.id);
@@ -5704,7 +5804,7 @@ ${planResult.output}
5704
5804
  if (!plan?.steps || plan.steps.length === 0) {
5705
5805
  error(`Agent did not produce a valid execution plan for ${issue2.id}. Aborting.`);
5706
5806
  try {
5707
- unlinkSync10(planPath);
5807
+ unlinkSync2(planPath);
5708
5808
  } catch {
5709
5809
  }
5710
5810
  activeProviderPids.delete(issue2.id);
@@ -5720,7 +5820,7 @@ ${planResult.output}
5720
5820
  `Plan produced ${sortedSteps.length} step(s): ${sortedSteps.map((s) => s.repoPath).join(" \u2192 ")}`
5721
5821
  );
5722
5822
  try {
5723
- unlinkSync10(planPath);
5823
+ unlinkSync2(planPath);
5724
5824
  } catch {
5725
5825
  }
5726
5826
  const prUrls = [];
@@ -6003,6 +6103,34 @@ ${result.output}
6003
6103
  cleanupManifest(workspace, issue2.id);
6004
6104
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
6005
6105
  }
6106
+ const hasChanges = await hasCodeChanges(repoPath, _defaultBranch);
6107
+ if (!hasChanges) {
6108
+ error(
6109
+ `Provider reported success but no code changes detected. Treating as failure for ${issue2.id}.`
6110
+ );
6111
+ cleanupManifest(workspace, issue2.id);
6112
+ const emptyCommitResult = {
6113
+ success: false,
6114
+ output: "Provider reported success but no code changes detected",
6115
+ duration: result.duration,
6116
+ providerUsed: result.providerUsed,
6117
+ attempts: [
6118
+ {
6119
+ provider: result.providerUsed,
6120
+ model: "",
6121
+ success: false,
6122
+ error: "Eligible error (empty commit)",
6123
+ duration: result.duration
6124
+ }
6125
+ ]
6126
+ };
6127
+ return {
6128
+ success: false,
6129
+ providerUsed: result.providerUsed,
6130
+ prUrls: [],
6131
+ fallback: emptyCommitResult
6132
+ };
6133
+ }
6006
6134
  const manifest = readLisaManifest(workspace, issue2.id);
6007
6135
  cleanupManifest(workspace, issue2.id);
6008
6136
  let prUrl = manifest?.prUrl;
@@ -6131,6 +6259,34 @@ ${result.output}
6131
6259
  await cleanupWorktree(repoPath, worktreePath);
6132
6260
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
6133
6261
  }
6262
+ const hasChanges = await hasCodeChanges(worktreePath, baseBranch);
6263
+ if (!hasChanges) {
6264
+ error(
6265
+ `Provider reported success but no code changes detected. Treating as failure for ${issue2.id}.`
6266
+ );
6267
+ await cleanupWorktree(repoPath, worktreePath);
6268
+ const emptyCommitResult = {
6269
+ success: false,
6270
+ output: "Provider reported success but no code changes detected",
6271
+ duration: result.duration,
6272
+ providerUsed: result.providerUsed,
6273
+ attempts: [
6274
+ {
6275
+ provider: result.providerUsed,
6276
+ model: "",
6277
+ success: false,
6278
+ error: "Eligible error (empty commit)",
6279
+ duration: result.duration
6280
+ }
6281
+ ]
6282
+ };
6283
+ return {
6284
+ success: false,
6285
+ providerUsed: result.providerUsed,
6286
+ prUrls: [],
6287
+ fallback: emptyCommitResult
6288
+ };
6289
+ }
6134
6290
  const manifest = readManifestFile(manifestPath);
6135
6291
  let prUrl = manifest?.prUrl;
6136
6292
  if (!prUrl) {
@@ -6269,6 +6425,18 @@ async function runConcurrentLoop(config2, source, models, workspace, opts) {
6269
6425
  if (!issue2) {
6270
6426
  if (opts.watch) {
6271
6427
  if (activeWorkers.size === 0) {
6428
+ if (completedCount > 0) {
6429
+ ok(`All issues resolved. Prompting user to continue watching...`);
6430
+ kanbanEmitter.emit("work:watch-prompt");
6431
+ setTitle("Lisa \u2014 all resolved");
6432
+ await waitIfPaused();
6433
+ if (hasUserQuitFromWatchPrompt() || isShuttingDown()) {
6434
+ noMoreIssues = true;
6435
+ break;
6436
+ }
6437
+ kanbanEmitter.emit("work:watch-prompt-resumed");
6438
+ ok(`Resuming watch mode (polling every ${WATCH_POLL_INTERVAL_MS / 1e3}s)...`);
6439
+ }
6272
6440
  ok(
6273
6441
  `No issues ready. Watching for new issues (polling every ${WATCH_POLL_INTERVAL_MS / 1e3}s)...`
6274
6442
  );
@@ -6381,13 +6549,13 @@ async function getChangedFiles(repoPath, baseBranch, dependencyBranch) {
6381
6549
  }
6382
6550
 
6383
6551
  // src/loop/branch-session.ts
6384
- import { appendFileSync as appendFileSync12, unlinkSync as unlinkSync11 } from "fs";
6552
+ import { appendFileSync as appendFileSync12, unlinkSync as unlinkSync3 } from "fs";
6385
6553
  import { join as join16, resolve as resolve10 } from "path";
6386
6554
  async function runBranchSession(config2, issue2, logFile, session, models) {
6387
6555
  const workspace = resolve10(config2.workspace);
6388
6556
  const manifestPath = join16(workspace, ".lisa-manifest.json");
6389
6557
  try {
6390
- unlinkSync11(manifestPath);
6558
+ unlinkSync3(manifestPath);
6391
6559
  } catch {
6392
6560
  }
6393
6561
  const testRunner = detectTestRunner(workspace);
@@ -6461,9 +6629,36 @@ ${result.output}
6461
6629
  error(`Session ${session} failed for ${issue2.id}. Check ${logFile}`);
6462
6630
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
6463
6631
  }
6632
+ const hasChanges = await hasCodeChanges(workspace, config2.base_branch);
6633
+ if (!hasChanges) {
6634
+ error(
6635
+ `Provider reported success but no code changes detected. Treating as failure for ${issue2.id}.`
6636
+ );
6637
+ const emptyCommitResult = {
6638
+ success: false,
6639
+ output: "Provider reported success but no code changes detected",
6640
+ duration: result.duration,
6641
+ providerUsed: result.providerUsed,
6642
+ attempts: [
6643
+ {
6644
+ provider: result.providerUsed,
6645
+ model: "",
6646
+ success: false,
6647
+ error: "Eligible error (empty commit)",
6648
+ duration: result.duration
6649
+ }
6650
+ ]
6651
+ };
6652
+ return {
6653
+ success: false,
6654
+ providerUsed: result.providerUsed,
6655
+ prUrls: [],
6656
+ fallback: emptyCommitResult
6657
+ };
6658
+ }
6464
6659
  const manifest = readManifestFile(manifestPath);
6465
6660
  try {
6466
- unlinkSync11(manifestPath);
6661
+ unlinkSync3(manifestPath);
6467
6662
  } catch {
6468
6663
  }
6469
6664
  let prUrl = manifest?.prUrl;
@@ -6556,6 +6751,23 @@ async function runSequentialLoop(config2, source, models, workspace, opts) {
6556
6751
  break;
6557
6752
  }
6558
6753
  if (opts.watch) {
6754
+ if (completedCount > 0) {
6755
+ ok(`All issues resolved. Prompting user to continue watching...`);
6756
+ kanbanEmitter.emit("work:watch-prompt");
6757
+ setTitle("Lisa \u2014 all resolved");
6758
+ await waitIfPaused();
6759
+ if (hasUserQuitFromWatchPrompt() || isShuttingDown()) {
6760
+ break;
6761
+ }
6762
+ kanbanEmitter.emit("work:watch-prompt-resumed");
6763
+ ok(`Resuming watch mode (polling every ${WATCH_POLL_INTERVAL_MS / 1e3}s)...`);
6764
+ kanbanEmitter.emit("work:watching");
6765
+ setTitle("Lisa \u2014 watching...");
6766
+ await sleep2(WATCH_POLL_INTERVAL_MS);
6767
+ kanbanEmitter.emit("work:watch-resume");
6768
+ session--;
6769
+ continue;
6770
+ }
6559
6771
  ok(
6560
6772
  `No issues ready. Watching for new issues (polling every ${WATCH_POLL_INTERVAL_MS / 1e3}s)...`
6561
6773
  );
@@ -6903,7 +7115,7 @@ var run = defineCommand5({
6903
7115
  if (isTTY) {
6904
7116
  const { render } = await import("ink");
6905
7117
  const { createElement } = await import("react");
6906
- const { KanbanApp } = await import("./kanban-VSEZ7NUK.js");
7118
+ const { KanbanApp } = await import("./kanban-QZ5NRPJ5.js");
6907
7119
  const demoConfig = {
6908
7120
  provider: "claude",
6909
7121
  source: "linear",
@@ -6940,6 +7152,12 @@ var run = defineCommand5({
6940
7152
  label: args.label,
6941
7153
  bell: args.bell
6942
7154
  });
7155
+ if (merged.provider === "goose") {
7156
+ const gooseProvider = merged.provider_options?.goose?.goose_provider;
7157
+ if (gooseProvider && !process.env.GOOSE_PROVIDER) {
7158
+ process.env.GOOSE_PROVIDER = gooseProvider;
7159
+ }
7160
+ }
6943
7161
  if (args.lifecycle || args["lifecycle-timeout"]) {
6944
7162
  const lifecycleTimeout = args["lifecycle-timeout"] ? Number.parseInt(args["lifecycle-timeout"], 10) : void 0;
6945
7163
  merged.lifecycle = {
@@ -6972,7 +7190,7 @@ Add them to your ${shell} and run: source ${shell}`));
6972
7190
  if (isTTY) {
6973
7191
  const { render } = await import("ink");
6974
7192
  const { createElement } = await import("react");
6975
- const { KanbanApp } = await import("./kanban-VSEZ7NUK.js");
7193
+ const { KanbanApp } = await import("./kanban-QZ5NRPJ5.js");
6976
7194
  render(createElement(KanbanApp, { config: merged }), { exitOnCtrlC: false });
6977
7195
  }
6978
7196
  await runLoop(merged, {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  kanbanEmitter,
4
4
  useKanbanState
5
- } from "./chunk-Z6CNJAZF.js";
5
+ } from "./chunk-ITQEGO5A.js";
6
6
  import {
7
7
  resetTitle,
8
8
  startSpinner,
@@ -56,21 +56,6 @@ function wrapTitle(title, maxWidth) {
56
56
  const line2 = remaining.length > maxWidth ? `${remaining.slice(0, maxWidth - 1)}\u2026` : remaining;
57
57
  return [line1, line2];
58
58
  }
59
- function getLastOutputLine(outputLog, maxWidth) {
60
- if (!outputLog) return "";
61
- const ansiPattern = /\x1B(?:\[[0-?]*[ -/]*[@-~]|\].*?(?:\x07|\x1B\\))/g;
62
- const stripped = outputLog.replace(ansiPattern, "");
63
- const lines = stripped.split(/\r?\n/).map((line) => {
64
- const parts = line.split("\r");
65
- return (parts[parts.length - 1] ?? "").trim();
66
- }).filter((line) => line.length > 0);
67
- if (lines.length === 0) return "";
68
- const lastLine = lines[lines.length - 1] ?? "";
69
- if (lastLine.length > maxWidth) {
70
- return `${lastLine.slice(0, maxWidth - 1)}\u2026`;
71
- }
72
- return lastLine;
73
- }
74
59
  function Card({
75
60
  card,
76
61
  isSelected = false,
@@ -134,9 +119,7 @@ function Card({
134
119
  ] }),
135
120
  /* @__PURE__ */ jsx(Text, { bold: isSelected, dimColor: !isSelected, children: stripDoubleWidth(titleLine1).padEnd(cardWidth) }),
136
121
  /* @__PURE__ */ jsx(Text, { bold: isSelected, dimColor: !isSelected, children: stripDoubleWidth(titleLine2).padEnd(cardWidth) }),
137
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: stripDoubleWidth(
138
- card.column === "in_progress" ? getLastOutputLine(card.outputLog, cardWidth) : ""
139
- ).padEnd(cardWidth) }),
122
+ /* @__PURE__ */ jsx(Text, { children: " ".repeat(cardWidth) }),
140
123
  card.column === "in_progress" ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", marginTop: 0, children: [
141
124
  isPausedInProgress ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u23F8" }) : /* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
142
125
  /* @__PURE__ */ jsx(Text, { color: isPausedInProgress ? "gray" : "yellow", dimColor: isPausedInProgress, children: elapsedMs !== null ? ` ${formatElapsed(elapsedMs)}` : "" })
@@ -220,7 +203,6 @@ function Column({
220
203
  const hiddenBelow = Math.max(0, sortedCards.length - scrollOffset - visibleCount);
221
204
  const borderColor = isFocused ? "yellow" : "gray";
222
205
  const headerColor = isFocused ? "yellow" : "white";
223
- const runningCount = cards.filter((c) => c.column === "in_progress").length;
224
206
  const errorCount = cards.filter((c) => c.hasError).length;
225
207
  return /* @__PURE__ */ jsxs2(
226
208
  Box2,
@@ -241,7 +223,6 @@ function Column({
241
223
  ] }),
242
224
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
243
225
  errorCount > 0 && /* @__PURE__ */ jsx2(Text2, { color: "red", bold: true, children: `\u2716${errorCount} ` }),
244
- runningCount > 0 && /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: `\u25CF${runningCount} ` }),
245
226
  /* @__PURE__ */ jsx2(Text2, { color: headerColor, children: `[${cards.length}]` })
246
227
  ] })
247
228
  ] }),
@@ -268,7 +249,7 @@ function Column({
268
249
  }
269
250
 
270
251
  // src/ui/board.tsx
271
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
252
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
272
253
  function formatDuration(ms) {
273
254
  const totalSeconds = Math.floor(ms / 1e3);
274
255
  const minutes = Math.floor(totalSeconds / 60);
@@ -281,6 +262,7 @@ function Board({
281
262
  labels,
282
263
  isEmpty,
283
264
  isWatching = false,
265
+ isWatchPrompt = false,
284
266
  workComplete,
285
267
  activeColIndex = 0,
286
268
  activeCardIndex = 0,
@@ -303,7 +285,36 @@ function Board({
303
285
  /* @__PURE__ */ jsx3(Box3, { height: 1 }),
304
286
  /* @__PURE__ */ jsx3(Text3, { color: "white", dimColor: true, children: "Polling every 60s for new issues with the ready label." }),
305
287
  /* @__PURE__ */ jsx3(Box3, { height: 1 }),
306
- /* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Press Ctrl+C to stop" })
288
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Press [q] to quit" })
289
+ ]
290
+ }
291
+ ) });
292
+ }
293
+ if (isWatchPrompt) {
294
+ return /* @__PURE__ */ jsx3(Box3, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs3(
295
+ Box3,
296
+ {
297
+ flexDirection: "column",
298
+ borderStyle: "single",
299
+ borderColor: "green",
300
+ paddingX: 3,
301
+ paddingY: 1,
302
+ children: [
303
+ workComplete && /* @__PURE__ */ jsxs3(Fragment, { children: [
304
+ /* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: `\u25C8 ${workComplete.total} issue${workComplete.total !== 1 ? "s" : ""} resolved` }),
305
+ /* @__PURE__ */ jsx3(Box3, { height: 1 })
306
+ ] }),
307
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "\u25CE CONTINUE WATCHING?" }),
308
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
309
+ /* @__PURE__ */ jsx3(Text3, { color: "white", dimColor: true, children: "All issues have been processed." }),
310
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
311
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
312
+ "[",
313
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "w" }),
314
+ "] Watch / [",
315
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "q" }),
316
+ "] Quit"
317
+ ] })
307
318
  ]
308
319
  }
309
320
  ) });
@@ -655,7 +666,7 @@ function Sidebar({
655
666
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
656
667
  function KanbanApp({ config }) {
657
668
  const { exit } = useApp();
658
- const { cards, isEmpty, isWatching, workComplete, modelInUse } = useKanbanState(
669
+ const { cards, isEmpty, isWatching, isWatchPrompt, workComplete, modelInUse } = useKanbanState(
659
670
  config.bell ?? true
660
671
  );
661
672
  const { rows } = useTerminalSize();
@@ -712,6 +723,18 @@ function KanbanApp({ config }) {
712
723
  setActiveCardIndex(newCardIndex);
713
724
  }, [cards, selectedCardId, activeView]);
714
725
  useInput2((input, key) => {
726
+ if (isWatchPrompt) {
727
+ if (input === "w") {
728
+ kanbanEmitter.emit("work:watch-prompt-resolved");
729
+ kanbanEmitter.emit("loop:resume");
730
+ return;
731
+ }
732
+ if (input === "q") {
733
+ kanbanEmitter.emit("loop:quit");
734
+ return;
735
+ }
736
+ return;
737
+ }
715
738
  if (input === "q") {
716
739
  process.emit("SIGINT");
717
740
  return;
@@ -810,6 +833,7 @@ function KanbanApp({ config }) {
810
833
  labels,
811
834
  isEmpty,
812
835
  isWatching,
836
+ isWatchPrompt,
813
837
  workComplete,
814
838
  activeColIndex,
815
839
  activeCardIndex,
@@ -9,7 +9,7 @@ import {
9
9
  getPrCachePath,
10
10
  projectHash,
11
11
  rotateLogFiles
12
- } from "./chunk-OYQ6TOAG.js";
12
+ } from "./chunk-GZ2ZAQO4.js";
13
13
  export {
14
14
  ensureCacheDir,
15
15
  getCacheDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.14.2",
3
+ "version": "1.16.0",
4
4
  "description": "Autonomous issue resolver",
5
5
  "keywords": [
6
6
  "loop",
@@ -27,7 +27,7 @@
27
27
  "lisa": "dist/index.js"
28
28
  },
29
29
  "dependencies": {
30
- "@clack/prompts": "^1.0.1",
30
+ "@clack/prompts": "^1.1.0",
31
31
  "citty": "^0.2.1",
32
32
  "execa": "^9.6.1",
33
33
  "ink": "^6.8.0",
@@ -43,7 +43,7 @@
43
43
  "@vitest/coverage-v8": "^4.0.18",
44
44
  "concurrently": "^9.2.1",
45
45
  "husky": "^9.1.7",
46
- "lint-staged": "^16.3.1",
46
+ "lint-staged": "^16.3.2",
47
47
  "tsup": "^8.5.1",
48
48
  "tsx": "^4.21.0",
49
49
  "typescript": "^5.9.3",