@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-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
1669
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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: [
|