@tarcisiopgs/lisa 1.16.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -189,6 +189,7 @@ repos:
189
189
  loop:
190
190
  cooldown: 10 # seconds between issues
191
191
  max_sessions: 0 # 0 = unlimited
192
+ session_timeout: 0 # seconds per provider run (0 = disabled)
192
193
 
193
194
  # Optional — kill stuck providers
194
195
  overseer:
@@ -227,6 +228,19 @@ When `--concurrency` is greater than 1, worktree mode is enforced automatically.
227
228
 
228
229
  ---
229
230
 
231
+ ### Session Timeout
232
+
233
+ If a provider hangs (e.g. misconfigured model, network issue), Lisa can kill it after a configurable duration:
234
+
235
+ ```yaml
236
+ loop:
237
+ session_timeout: 300 # kill provider after 5 minutes (0 = disabled, default)
238
+ ```
239
+
240
+ When the timeout fires, the provider process is killed and the error is eligible for fallback — Lisa will try the next model in your chain. This is disabled by default so long-running sessions work uninterrupted.
241
+
242
+ ---
243
+
230
244
  ## Writing Issues
231
245
 
232
246
  Issue quality is the single biggest factor in PR quality. Lisa validates issues before accepting them — vague tickets without clear criteria are skipped and labelled `needs-spec`.
@@ -743,6 +743,9 @@ function useKanbanState(bellEnabled) {
743
743
  })
744
744
  );
745
745
  };
746
+ const onLogFile = (issueId, logFile) => {
747
+ setCards((prev) => prev.map((c) => c.id === issueId ? { ...c, logFile } : c));
748
+ };
746
749
  const onOutput = (issueId, text) => {
747
750
  setCards(
748
751
  (prev) => prev.map((c) => c.id === issueId ? { ...c, outputLog: c.outputLog + text } : c)
@@ -757,6 +760,7 @@ function useKanbanState(bellEnabled) {
757
760
  kanbanEmitter.on("issue:killed", onKilled);
758
761
  kanbanEmitter.on("provider:paused", onProviderPaused);
759
762
  kanbanEmitter.on("provider:resumed", onProviderResumed);
763
+ kanbanEmitter.on("issue:log-file", onLogFile);
760
764
  kanbanEmitter.on("issue:output", onOutput);
761
765
  const onModelChanged = (model) => setModelInUse(model);
762
766
  kanbanEmitter.on("provider:model-changed", onModelChanged);
@@ -786,6 +790,7 @@ function useKanbanState(bellEnabled) {
786
790
  kanbanEmitter.off("issue:killed", onKilled);
787
791
  kanbanEmitter.off("provider:paused", onProviderPaused);
788
792
  kanbanEmitter.off("provider:resumed", onProviderResumed);
793
+ kanbanEmitter.off("issue:log-file", onLogFile);
789
794
  kanbanEmitter.off("issue:output", onOutput);
790
795
  kanbanEmitter.off("provider:model-changed", onModelChanged);
791
796
  kanbanEmitter.off("work:empty", onEmpty);
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  ok,
33
33
  setOutputMode,
34
34
  warn
35
- } from "./chunk-ITQEGO5A.js";
35
+ } from "./chunk-WDGLMZB7.js";
36
36
  import {
37
37
  notify,
38
38
  resetTitle,
@@ -522,6 +522,28 @@ function spawnWithPty(command, options = {}) {
522
522
  return { proc, isPty: false };
523
523
  }
524
524
 
525
+ // src/providers/timeout.ts
526
+ var TIMEOUT_MESSAGE = "\n[lisa-timeout] Provider killed: exceeded session_timeout. Eligible for fallback.\n";
527
+ function createSessionTimeout(proc, timeoutSeconds) {
528
+ if (!timeoutSeconds || timeoutSeconds <= 0) {
529
+ return { stop() {
530
+ }, wasTimedOut: () => false };
531
+ }
532
+ let timedOut = false;
533
+ const timer = setTimeout(() => {
534
+ timedOut = true;
535
+ proc.kill("SIGTERM");
536
+ }, timeoutSeconds * 1e3);
537
+ return {
538
+ stop() {
539
+ clearTimeout(timer);
540
+ },
541
+ wasTimedOut() {
542
+ return timedOut;
543
+ }
544
+ };
545
+ }
546
+
525
547
  // src/providers/aider.ts
526
548
  var AIDER_API_KEY_ENV_VARS = [
527
549
  "OPENAI_API_KEY",
@@ -561,12 +583,25 @@ var AiderProvider = class {
561
583
  try {
562
584
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
563
585
  const command = `aider --message-file '${promptFile}' --yes-always ${modelFlag}`;
586
+ log(
587
+ `[aider] Running: aider --message-file --yes-always ${modelFlag || "(default model)"}`.trim()
588
+ );
589
+ if (opts.issueId) {
590
+ kanbanEmitter.emit(
591
+ "issue:output",
592
+ opts.issueId,
593
+ `${`$ aider --message-file --yes-always ${modelFlag || "(default model)"}
594
+ `.trim()}
595
+ `
596
+ );
597
+ }
564
598
  const { proc, isPty } = spawnWithPty(command, {
565
599
  cwd: opts.cwd,
566
600
  env: { ...process.env, ...opts.env }
567
601
  });
568
602
  if (proc.pid) opts.onProcess?.(proc.pid);
569
603
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
604
+ const sessionTimeout = createSessionTimeout(proc, opts.sessionTimeout);
570
605
  const errorLoopDetector = createErrorLoopDetector(proc, /^Error /);
571
606
  const chunks = [];
572
607
  proc.stdout?.on("data", (chunk) => {
@@ -595,14 +630,17 @@ var AiderProvider = class {
595
630
  const exitCode = await new Promise((resolve13) => {
596
631
  proc.on("close", (code) => {
597
632
  overseer?.stop();
633
+ sessionTimeout.stop();
598
634
  resolve13(code ?? 1);
599
635
  });
600
636
  });
601
- if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
637
+ if (sessionTimeout.wasTimedOut()) {
638
+ chunks.push(TIMEOUT_MESSAGE);
639
+ } else if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
602
640
  chunks.push(STUCK_MESSAGE);
603
641
  }
604
642
  return {
605
- success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled(),
643
+ success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled() && !sessionTimeout.wasTimedOut(),
606
644
  output: chunks.join(""),
607
645
  duration: Date.now() - start
608
646
  };
@@ -649,6 +687,11 @@ var ClaudeProvider = class {
649
687
  flags.push("--model", opts.model);
650
688
  }
651
689
  const command = `claude ${flags.join(" ")} "$(cat '${promptFile}')"`;
690
+ log(`[claude] Running: claude ${flags.join(" ")}`.trim());
691
+ if (opts.issueId) {
692
+ kanbanEmitter.emit("issue:output", opts.issueId, `$ claude ${flags.join(" ")}
693
+ `);
694
+ }
652
695
  const spawnEnv = { ...process.env, ...opts.env, CLAUDECODE: void 0 };
653
696
  const isNestedInClaude = Boolean(process.env.CLAUDECODE);
654
697
  let proc;
@@ -665,6 +708,7 @@ var ClaudeProvider = class {
665
708
  }
666
709
  if (proc.pid) opts.onProcess?.(proc.pid);
667
710
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
711
+ const sessionTimeout = createSessionTimeout(proc, opts.sessionTimeout);
668
712
  const errorLoopDetector = createErrorLoopDetector(proc, /^Error /);
669
713
  const chunks = [];
670
714
  proc.stdout?.on("data", (chunk) => {
@@ -693,14 +737,17 @@ var ClaudeProvider = class {
693
737
  const exitCode = await new Promise((resolve13) => {
694
738
  proc.on("close", (code) => {
695
739
  overseer?.stop();
740
+ sessionTimeout.stop();
696
741
  resolve13(code ?? 1);
697
742
  });
698
743
  });
699
- if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
744
+ if (sessionTimeout.wasTimedOut()) {
745
+ chunks.push(TIMEOUT_MESSAGE);
746
+ } else if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
700
747
  chunks.push(STUCK_MESSAGE);
701
748
  }
702
749
  return {
703
- success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled(),
750
+ success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled() && !sessionTimeout.wasTimedOut(),
704
751
  output: chunks.join(""),
705
752
  duration: Date.now() - start
706
753
  };
@@ -742,12 +789,24 @@ var CodexProvider = class {
742
789
  try {
743
790
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
744
791
  const command = `codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral ${modelFlag} "$(cat '${promptFile}')"`;
792
+ log(
793
+ `[codex] Running: codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral ${modelFlag || "(default model)"}`.trim()
794
+ );
795
+ if (opts.issueId) {
796
+ kanbanEmitter.emit(
797
+ "issue:output",
798
+ opts.issueId,
799
+ `$ codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral ${modelFlag || "(default model)"}
800
+ `.trim() + "\n"
801
+ );
802
+ }
745
803
  const { proc, isPty } = spawnWithPty(command, {
746
804
  cwd: opts.cwd,
747
805
  env: { ...process.env, ...opts.env, CODEX_QUIET_MODE: "1" }
748
806
  });
749
807
  if (proc.pid) opts.onProcess?.(proc.pid);
750
808
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
809
+ const sessionTimeout = createSessionTimeout(proc, opts.sessionTimeout);
751
810
  const errorLoopDetector = createErrorLoopDetector(proc, /^Error /);
752
811
  const chunks = [];
753
812
  proc.stdout?.on("data", (chunk) => {
@@ -776,14 +835,17 @@ var CodexProvider = class {
776
835
  const exitCode = await new Promise((resolve13) => {
777
836
  proc.on("close", (code) => {
778
837
  overseer?.stop();
838
+ sessionTimeout.stop();
779
839
  resolve13(code ?? 1);
780
840
  });
781
841
  });
782
- if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
842
+ if (sessionTimeout.wasTimedOut()) {
843
+ chunks.push(TIMEOUT_MESSAGE);
844
+ } else if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
783
845
  chunks.push(STUCK_MESSAGE);
784
846
  }
785
847
  return {
786
- success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled(),
848
+ success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled() && !sessionTimeout.wasTimedOut(),
787
849
  output: chunks.join(""),
788
850
  duration: Date.now() - start
789
851
  };
@@ -825,12 +887,25 @@ var CopilotProvider = class {
825
887
  try {
826
888
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
827
889
  const command = `copilot --allow-all ${modelFlag} -p "$(cat '${promptFile}')"`;
890
+ log(
891
+ `[copilot] Running: copilot --allow-all ${modelFlag || "(default model)"} -p`.trim()
892
+ );
893
+ if (opts.issueId) {
894
+ kanbanEmitter.emit(
895
+ "issue:output",
896
+ opts.issueId,
897
+ `${`$ copilot --allow-all ${modelFlag || "(default model)"} -p
898
+ `.trim()}
899
+ `
900
+ );
901
+ }
828
902
  const { proc, isPty } = spawnWithPty(command, {
829
903
  cwd: opts.cwd,
830
904
  env: { ...process.env, ...opts.env }
831
905
  });
832
906
  if (proc.pid) opts.onProcess?.(proc.pid);
833
907
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
908
+ const sessionTimeout = createSessionTimeout(proc, opts.sessionTimeout);
834
909
  const errorLoopDetector = createErrorLoopDetector(proc, /^Error /);
835
910
  const chunks = [];
836
911
  proc.stdout?.on("data", (chunk) => {
@@ -859,14 +934,17 @@ var CopilotProvider = class {
859
934
  const exitCode = await new Promise((resolve13) => {
860
935
  proc.on("close", (code) => {
861
936
  overseer?.stop();
937
+ sessionTimeout.stop();
862
938
  resolve13(code ?? 1);
863
939
  });
864
940
  });
865
- if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
941
+ if (sessionTimeout.wasTimedOut()) {
942
+ chunks.push(TIMEOUT_MESSAGE);
943
+ } else if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
866
944
  chunks.push(STUCK_MESSAGE);
867
945
  }
868
946
  return {
869
- success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled(),
947
+ success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled() && !sessionTimeout.wasTimedOut(),
870
948
  output: chunks.join(""),
871
949
  duration: Date.now() - start
872
950
  };
@@ -926,12 +1004,24 @@ var CursorProvider = class {
926
1004
  try {
927
1005
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
928
1006
  const command = `${bin} -p "$(cat '${promptFile}')" --output-format text --force ${modelFlag}`;
1007
+ log(
1008
+ `[cursor] Running: ${bin} -p --output-format text --force ${modelFlag || "(default model)"}`.trim()
1009
+ );
1010
+ if (opts.issueId) {
1011
+ kanbanEmitter.emit(
1012
+ "issue:output",
1013
+ opts.issueId,
1014
+ `$ ${bin} -p --output-format text --force ${modelFlag || "(default model)"}
1015
+ `.trim() + "\n"
1016
+ );
1017
+ }
929
1018
  const { proc, isPty } = spawnWithPty(command, {
930
1019
  cwd: opts.cwd,
931
1020
  env: { ...process.env, ...opts.env }
932
1021
  });
933
1022
  if (proc.pid) opts.onProcess?.(proc.pid);
934
1023
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1024
+ const sessionTimeout = createSessionTimeout(proc, opts.sessionTimeout);
935
1025
  const errorLoopDetector = createErrorLoopDetector(proc, /^Error /);
936
1026
  const chunks = [];
937
1027
  proc.stdout?.on("data", (chunk) => {
@@ -960,14 +1050,17 @@ var CursorProvider = class {
960
1050
  const exitCode = await new Promise((resolve13) => {
961
1051
  proc.on("close", (code) => {
962
1052
  overseer?.stop();
1053
+ sessionTimeout.stop();
963
1054
  resolve13(code ?? 1);
964
1055
  });
965
1056
  });
966
- if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
1057
+ if (sessionTimeout.wasTimedOut()) {
1058
+ chunks.push(TIMEOUT_MESSAGE);
1059
+ } else if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
967
1060
  chunks.push(STUCK_MESSAGE);
968
1061
  }
969
1062
  return {
970
- success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled(),
1063
+ success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled() && !sessionTimeout.wasTimedOut(),
971
1064
  output: chunks.join(""),
972
1065
  duration: Date.now() - start
973
1066
  };
@@ -1010,12 +1103,23 @@ var GeminiProvider = class {
1010
1103
  try {
1011
1104
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1012
1105
  const command = `gemini --yolo ${modelFlag} -p "$(cat '${promptFile}')"`;
1106
+ log(`[gemini] Running: gemini --yolo ${modelFlag || "(default model)"} -p`.trim());
1107
+ if (opts.issueId) {
1108
+ kanbanEmitter.emit(
1109
+ "issue:output",
1110
+ opts.issueId,
1111
+ `${`$ gemini --yolo ${modelFlag || "(default model)"} -p
1112
+ `.trim()}
1113
+ `
1114
+ );
1115
+ }
1013
1116
  const { proc, isPty } = spawnWithPty(command, {
1014
1117
  cwd: opts.cwd,
1015
1118
  env: { ...process.env, ...opts.env }
1016
1119
  });
1017
1120
  if (proc.pid) opts.onProcess?.(proc.pid);
1018
1121
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1122
+ const sessionTimeout = createSessionTimeout(proc, opts.sessionTimeout);
1019
1123
  const errorLoopDetector = createErrorLoopDetector(proc, GEMINI_ERROR_PATTERN);
1020
1124
  const chunks = [];
1021
1125
  proc.stdout?.on("data", (chunk) => {
@@ -1044,14 +1148,17 @@ var GeminiProvider = class {
1044
1148
  const exitCode = await new Promise((resolve13) => {
1045
1149
  proc.on("close", (code) => {
1046
1150
  overseer?.stop();
1151
+ sessionTimeout.stop();
1047
1152
  resolve13(code ?? 1);
1048
1153
  });
1049
1154
  });
1050
- if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
1155
+ if (sessionTimeout.wasTimedOut()) {
1156
+ chunks.push(TIMEOUT_MESSAGE);
1157
+ } else if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
1051
1158
  chunks.push(STUCK_MESSAGE);
1052
1159
  }
1053
1160
  return {
1054
- success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled(),
1161
+ success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled() && !sessionTimeout.wasTimedOut(),
1055
1162
  output: chunks.join(""),
1056
1163
  duration: Date.now() - start
1057
1164
  };
@@ -1094,12 +1201,25 @@ var GooseProvider = class {
1094
1201
  const providerFlag = process.env.GOOSE_PROVIDER ? `--provider ${process.env.GOOSE_PROVIDER}` : "";
1095
1202
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1096
1203
  const command = `goose run ${providerFlag} ${modelFlag} --text "$(cat '${promptFile}')"`;
1204
+ log(
1205
+ `[goose] Running: goose run ${providerFlag} ${modelFlag || "(default model)"} --text`.trim()
1206
+ );
1207
+ if (opts.issueId) {
1208
+ kanbanEmitter.emit(
1209
+ "issue:output",
1210
+ opts.issueId,
1211
+ `${`$ goose run ${providerFlag} ${modelFlag || "(default model)"} --text
1212
+ `.trim()}
1213
+ `
1214
+ );
1215
+ }
1097
1216
  const { proc, isPty } = spawnWithPty(command, {
1098
1217
  cwd: opts.cwd,
1099
1218
  env: { ...process.env, ...opts.env }
1100
1219
  });
1101
1220
  if (proc.pid) opts.onProcess?.(proc.pid);
1102
1221
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1222
+ const sessionTimeout = createSessionTimeout(proc, opts.sessionTimeout);
1103
1223
  const errorLoopDetector = createErrorLoopDetector(proc, /^Error /);
1104
1224
  const chunks = [];
1105
1225
  proc.stdout?.on("data", (chunk) => {
@@ -1128,14 +1248,17 @@ var GooseProvider = class {
1128
1248
  const exitCode = await new Promise((resolve13) => {
1129
1249
  proc.on("close", (code) => {
1130
1250
  overseer?.stop();
1251
+ sessionTimeout.stop();
1131
1252
  resolve13(code ?? 1);
1132
1253
  });
1133
1254
  });
1134
- if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
1255
+ if (sessionTimeout.wasTimedOut()) {
1256
+ chunks.push(TIMEOUT_MESSAGE);
1257
+ } else if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
1135
1258
  chunks.push(STUCK_MESSAGE);
1136
1259
  }
1137
1260
  return {
1138
- success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled(),
1261
+ success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled() && !sessionTimeout.wasTimedOut(),
1139
1262
  output: chunks.join(""),
1140
1263
  duration: Date.now() - start
1141
1264
  };
@@ -1177,12 +1300,23 @@ var OpenCodeProvider = class {
1177
1300
  try {
1178
1301
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1179
1302
  const command = `opencode run ${modelFlag} "$(cat '${promptFile}')"`;
1303
+ log(`[opencode] Running: opencode run ${modelFlag || "(default model)"}`.trim());
1304
+ if (opts.issueId) {
1305
+ kanbanEmitter.emit(
1306
+ "issue:output",
1307
+ opts.issueId,
1308
+ `${`$ opencode run ${modelFlag || "(default model)"}
1309
+ `.trim()}
1310
+ `
1311
+ );
1312
+ }
1180
1313
  const { proc, isPty } = spawnWithPty(command, {
1181
1314
  cwd: opts.cwd,
1182
1315
  env: { ...process.env, ...opts.env }
1183
1316
  });
1184
1317
  if (proc.pid) opts.onProcess?.(proc.pid);
1185
1318
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1319
+ const sessionTimeout = createSessionTimeout(proc, opts.sessionTimeout);
1186
1320
  const errorLoopDetector = createErrorLoopDetector(proc, /^Error /);
1187
1321
  const chunks = [];
1188
1322
  proc.stdout?.on("data", (chunk) => {
@@ -1211,14 +1345,17 @@ var OpenCodeProvider = class {
1211
1345
  const exitCode = await new Promise((resolve13) => {
1212
1346
  proc.on("close", (code) => {
1213
1347
  overseer?.stop();
1348
+ sessionTimeout.stop();
1214
1349
  resolve13(code ?? 1);
1215
1350
  });
1216
1351
  });
1217
- if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
1352
+ if (sessionTimeout.wasTimedOut()) {
1353
+ chunks.push(TIMEOUT_MESSAGE);
1354
+ } else if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
1218
1355
  chunks.push(STUCK_MESSAGE);
1219
1356
  }
1220
1357
  return {
1221
- success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled(),
1358
+ success: exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled() && !sessionTimeout.wasTimedOut(),
1222
1359
  output: chunks.join(""),
1223
1360
  duration: Date.now() - start
1224
1361
  };
@@ -1282,6 +1419,7 @@ var ELIGIBLE_ERROR_PATTERNS = [
1282
1419
  /not in PATH/i,
1283
1420
  /command not found/i,
1284
1421
  /lisa-overseer/i,
1422
+ /lisa-timeout/i,
1285
1423
  /named models unavailable/i,
1286
1424
  /free plans can only use/i,
1287
1425
  /empty commit/i
@@ -1665,29 +1803,8 @@ function fetchCursorModels() {
1665
1803
  function fetchOpenCodeModels() {
1666
1804
  try {
1667
1805
  const raw = execSync9("opencode models", { encoding: "utf-8", timeout: 1e4 });
1668
- const hasAnthropic = Boolean(process.env.ANTHROPIC_API_KEY);
1669
- const hasGoogle = Boolean(
1670
- process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY
1671
- );
1672
- const hasOpenAI = Boolean(process.env.OPENAI_API_KEY);
1673
- const hasCopilot = Boolean(process.env.GITHUB_COPILOT_API_KEY || process.env.GITHUB_TOKEN);
1674
- const hasGroq = Boolean(process.env.GROQ_API_KEY);
1675
- const hasMistral = Boolean(process.env.MISTRAL_API_KEY);
1676
- const hasDeepSeek = Boolean(process.env.DEEPSEEK_API_KEY);
1677
- return raw.split("\n").map((l) => l.trim()).filter((m) => {
1678
- if (/^opencode\//.test(m)) return true;
1679
- if (/^anthropic\/claude-(opus|sonnet|haiku)-4-\d+$/.test(m)) return hasAnthropic;
1680
- if (/^google\/gemini-(3\.1-pro-preview|3-pro-preview|3-flash-preview|2\.5-(pro|flash|flash-lite))$/.test(
1681
- m
1682
- ))
1683
- return hasGoogle;
1684
- if (/^openai\//.test(m)) return hasOpenAI;
1685
- if (/^github-copilot\//.test(m)) return hasCopilot;
1686
- if (/^groq\//.test(m)) return hasGroq;
1687
- if (/^mistral\//.test(m)) return hasMistral;
1688
- if (/^deepseek\//.test(m)) return hasDeepSeek;
1689
- return false;
1690
- });
1806
+ const clean = raw.replace(/\x1b\[[0-9;]*[mGKHFA-Z]/g, "");
1807
+ return clean.split("\n").map((l) => l.trim()).filter((m) => /^[a-z0-9][\w.-]*\/.+/i.test(m));
1691
1808
  } catch {
1692
1809
  return [];
1693
1810
  }
@@ -3946,6 +4063,13 @@ function resolveModels(config2) {
3946
4063
  );
3947
4064
  }
3948
4065
  }
4066
+ for (const m of providerModels) {
4067
+ if (m.includes("/") && m.startsWith(`${config2.provider}/`)) {
4068
+ warn(
4069
+ `Model "${m}" starts with the provider name "${config2.provider}/". Most provider CLIs expect just the model name (e.g. "${m.slice(config2.provider.length + 1)}"). If the provider fails silently, try removing the "${config2.provider}/" prefix.`
4070
+ );
4071
+ }
4072
+ }
3949
4073
  if (config2.provider === "cursor") {
3950
4074
  const hasAuto = providerModels.some((m) => m.toLowerCase() === "auto");
3951
4075
  if (!hasAuto) {
@@ -5754,6 +5878,7 @@ async function runWorktreeMultiRepoSession(config2, issue2, logFile, session, mo
5754
5878
  } catch {
5755
5879
  }
5756
5880
  initLogFile(logFile);
5881
+ kanbanEmitter.emit("issue:log-file", issue2.id, logFile);
5757
5882
  startSpinner(`${issue2.id} \u2014 analyzing issue...`);
5758
5883
  log(`Multi-repo planning phase for ${issue2.id}`);
5759
5884
  const repoGenerators = /* @__PURE__ */ new Map();
@@ -5769,6 +5894,7 @@ async function runWorktreeMultiRepoSession(config2, issue2, logFile, session, mo
5769
5894
  guardrailsDir: workspace,
5770
5895
  issueId: issue2.id,
5771
5896
  overseer: config2.overseer,
5897
+ sessionTimeout: config2.loop.session_timeout,
5772
5898
  onProcess: (pid) => {
5773
5899
  activeProviderPids.set(issue2.id, pid);
5774
5900
  },
@@ -5928,6 +6054,7 @@ async function runMultiRepoStep(config2, issue2, step, previousResults, logFile,
5928
6054
  guardrailsDir: workspace,
5929
6055
  issueId: issue2.id,
5930
6056
  overseer: config2.overseer,
6057
+ sessionTimeout: config2.loop.session_timeout,
5931
6058
  env: Object.keys(lifecycleEnv).length > 0 ? lifecycleEnv : void 0,
5932
6059
  onProcess: (pid) => {
5933
6060
  activeProviderPids.set(issue2.id, pid);
@@ -6069,6 +6196,7 @@ async function runNativeWorktreeSession(config2, issue2, logFile, session, model
6069
6196
  config2.platform
6070
6197
  );
6071
6198
  initLogFile(logFile);
6199
+ kanbanEmitter.emit("issue:log-file", issue2.id, logFile);
6072
6200
  startSpinner(`${issue2.id} \u2014 implementing (native worktree)...`);
6073
6201
  log(`Implementing with native worktree... (log: ${logFile})`);
6074
6202
  const result = await runWithFallback(models, prompt, {
@@ -6077,6 +6205,7 @@ async function runNativeWorktreeSession(config2, issue2, logFile, session, model
6077
6205
  guardrailsDir: workspace,
6078
6206
  issueId: issue2.id,
6079
6207
  overseer: config2.overseer,
6208
+ sessionTimeout: config2.loop.session_timeout,
6080
6209
  useNativeWorktree: true,
6081
6210
  env: Object.keys(lifecycleEnv).length > 0 ? lifecycleEnv : void 0,
6082
6211
  onProcess: (pid) => {
@@ -6226,6 +6355,7 @@ async function runManualWorktreeSession(config2, issue2, logFile, session, model
6226
6355
  manifestPath
6227
6356
  );
6228
6357
  initLogFile(logFile);
6358
+ kanbanEmitter.emit("issue:log-file", issue2.id, logFile);
6229
6359
  startSpinner(`${issue2.id} \u2014 implementing...`);
6230
6360
  log(`Implementing in worktree... (log: ${logFile})`);
6231
6361
  const result = await runWithFallback(models, prompt, {
@@ -6234,6 +6364,7 @@ async function runManualWorktreeSession(config2, issue2, logFile, session, model
6234
6364
  guardrailsDir: workspace,
6235
6365
  issueId: issue2.id,
6236
6366
  overseer: config2.overseer,
6367
+ sessionTimeout: config2.loop.session_timeout,
6237
6368
  env: Object.keys(lifecycleEnv).length > 0 ? lifecycleEnv : void 0,
6238
6369
  onProcess: (pid) => {
6239
6370
  activeProviderPids.set(issue2.id, pid);
@@ -6597,6 +6728,7 @@ async function runBranchSession(config2, issue2, logFile, session, models) {
6597
6728
  manifestPath
6598
6729
  );
6599
6730
  initLogFile(logFile);
6731
+ kanbanEmitter.emit("issue:log-file", issue2.id, logFile);
6600
6732
  startSpinner(`${issue2.id} \u2014 implementing...`);
6601
6733
  log(`Implementing... (log: ${logFile})`);
6602
6734
  const result = await runWithFallback(models, prompt, {
@@ -6605,6 +6737,7 @@ async function runBranchSession(config2, issue2, logFile, session, models) {
6605
6737
  guardrailsDir: workspace,
6606
6738
  issueId: issue2.id,
6607
6739
  overseer: config2.overseer,
6740
+ sessionTimeout: config2.loop.session_timeout,
6608
6741
  env: Object.keys(lifecycleEnv).length > 0 ? lifecycleEnv : void 0,
6609
6742
  onProcess: (pid) => {
6610
6743
  activeProviderPids.set(issue2.id, pid);
@@ -7115,7 +7248,7 @@ var run = defineCommand5({
7115
7248
  if (isTTY) {
7116
7249
  const { render } = await import("ink");
7117
7250
  const { createElement } = await import("react");
7118
- const { KanbanApp } = await import("./kanban-QZ5NRPJ5.js");
7251
+ const { KanbanApp } = await import("./kanban-LG26AUFK.js");
7119
7252
  const demoConfig = {
7120
7253
  provider: "claude",
7121
7254
  source: "linear",
@@ -7190,7 +7323,7 @@ Add them to your ${shell} and run: source ${shell}`));
7190
7323
  if (isTTY) {
7191
7324
  const { render } = await import("ink");
7192
7325
  const { createElement } = await import("react");
7193
- const { KanbanApp } = await import("./kanban-QZ5NRPJ5.js");
7326
+ const { KanbanApp } = await import("./kanban-LG26AUFK.js");
7194
7327
  render(createElement(KanbanApp, { config: merged }), { exitOnCtrlC: false });
7195
7328
  }
7196
7329
  await runLoop(merged, {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  kanbanEmitter,
4
4
  useKanbanState
5
- } from "./chunk-ITQEGO5A.js";
5
+ } from "./chunk-WDGLMZB7.js";
6
6
  import {
7
7
  resetTitle,
8
8
  startSpinner,
@@ -532,6 +532,10 @@ function IssueDetail({ card, onBack }) {
532
532
  /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: card.prUrls.length === 1 ? "PR: " : `PR ${i + 1}: ` }),
533
533
  /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: hyperlink(url, url) })
534
534
  ] }, url)),
535
+ card.logFile && /* @__PURE__ */ jsxs4(Box4, { marginTop: 0, children: [
536
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "LOG: " }),
537
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: card.logFile })
538
+ ] }),
535
539
  /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: separator }) }),
536
540
  /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", justifyContent: "space-between", children: [
537
541
  /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.16.0",
3
+ "version": "1.17.1",
4
4
  "description": "Autonomous issue resolver",
5
5
  "keywords": [
6
6
  "loop",