@tarcisiopgs/lisa 1.3.1 → 1.5.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 +9 -8
- package/dist/index.js +406 -91
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,16 +101,17 @@ All providers use `child_process.spawn` with `sh -c`. Prompts are written to a t
|
|
|
101
101
|
|
|
102
102
|
### Fallback Chain
|
|
103
103
|
|
|
104
|
-
Configure a fallback chain in the `models` array. Lisa tries each
|
|
104
|
+
Configure a fallback chain in the `models` array. Lisa tries each model in order — transient errors (429, quota, timeout, network) trigger the next model. Non-transient errors stop the chain immediately.
|
|
105
105
|
|
|
106
106
|
```yaml
|
|
107
|
+
provider: claude
|
|
107
108
|
models:
|
|
108
|
-
- claude
|
|
109
|
-
-
|
|
110
|
-
-
|
|
109
|
+
- claude-sonnet-4-6 # primary
|
|
110
|
+
- claude-opus-4-6 # fallback 1
|
|
111
|
+
- claude-haiku-4-5 # fallback 2
|
|
111
112
|
```
|
|
112
113
|
|
|
113
|
-
If `models` is not set, Lisa uses the
|
|
114
|
+
If `models` is not set, Lisa uses the provider's default model.
|
|
114
115
|
|
|
115
116
|
## Workflow Modes
|
|
116
117
|
|
|
@@ -208,7 +209,7 @@ repos:
|
|
|
208
209
|
- "npx prisma db push"
|
|
209
210
|
```
|
|
210
211
|
|
|
211
|
-
Lisa starts resources before the agent runs, waits for the port to be ready, runs setup commands, then stops everything after the session.
|
|
212
|
+
Lisa starts resources before the agent runs, waits for the port to be ready, runs setup commands, then stops everything after the session. In multi-repo workflows, resources are started and stopped per repo step.
|
|
212
213
|
|
|
213
214
|
## How It Works
|
|
214
215
|
|
|
@@ -237,9 +238,9 @@ Lisa starts resources before the agent runs, waits for the port to be ready, run
|
|
|
237
238
|
|
|
238
239
|
Lisa can detect stuck providers — agents that appear to be running but are making no progress. When enabled, the overseer periodically checks `git status` in the working directory. If no changes are detected within the `stuck_threshold`, the provider process is killed and the error is eligible for fallback to the next model in the chain.
|
|
239
240
|
|
|
240
|
-
### Test Runner Auto-Detection
|
|
241
|
+
### Test Runner and Package Manager Auto-Detection
|
|
241
242
|
|
|
242
|
-
Lisa auto-detects `vitest` or `jest` in the project's `package.json` dependencies. When a test runner is found, mandatory test instructions are injected into the agent prompt
|
|
243
|
+
Lisa auto-detects `vitest` or `jest` in the project's `package.json` dependencies. It also detects the package manager from lockfiles (`bun.lockb`/`bun.lock` → `bun`, `pnpm-lock.yaml` → `pnpm`, `yarn.lock` → `yarn`, otherwise `npm`). When a test runner is found, mandatory test instructions are injected into the agent prompt with the correct test command (e.g., `bun run test`, `pnpm run test`).
|
|
243
244
|
|
|
244
245
|
### PR Body Formatting
|
|
245
246
|
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { execSync as
|
|
4
|
+
import { execSync as execSync6 } from "child_process";
|
|
5
5
|
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6 } from "fs";
|
|
6
|
-
import {
|
|
6
|
+
import { tmpdir as tmpdir6 } from "os";
|
|
7
|
+
import { join as join10, resolve as resolvePath } from "path";
|
|
7
8
|
import * as clack from "@clack/prompts";
|
|
8
9
|
import { defineCommand, runMain } from "citty";
|
|
9
10
|
import pc2 from "picocolors";
|
|
@@ -331,8 +332,8 @@ function banner() {
|
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
// src/loop.ts
|
|
334
|
-
import { appendFileSync as
|
|
335
|
-
import { join as
|
|
335
|
+
import { appendFileSync as appendFileSync8, existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync6 } from "fs";
|
|
336
|
+
import { join as join9, resolve as resolve5 } from "path";
|
|
336
337
|
import { execa as execa3 } from "execa";
|
|
337
338
|
|
|
338
339
|
// src/lifecycle.ts
|
|
@@ -516,6 +517,12 @@ function sanitizePrBody(raw) {
|
|
|
516
517
|
// src/prompt.ts
|
|
517
518
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
518
519
|
import { join, resolve as resolve3 } from "path";
|
|
520
|
+
function detectPackageManager(cwd) {
|
|
521
|
+
if (existsSync3(join(cwd, "bun.lockb")) || existsSync3(join(cwd, "bun.lock"))) return "bun";
|
|
522
|
+
if (existsSync3(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
523
|
+
if (existsSync3(join(cwd, "yarn.lock"))) return "yarn";
|
|
524
|
+
return "npm";
|
|
525
|
+
}
|
|
519
526
|
function detectTestRunner(cwd) {
|
|
520
527
|
const packageJsonPath = join(cwd, "package.json");
|
|
521
528
|
if (!existsSync3(packageJsonPath)) return null;
|
|
@@ -529,20 +536,21 @@ function detectTestRunner(cwd) {
|
|
|
529
536
|
return null;
|
|
530
537
|
}
|
|
531
538
|
}
|
|
532
|
-
function buildImplementPrompt(issue, config2, testRunner) {
|
|
539
|
+
function buildImplementPrompt(issue, config2, testRunner, pm) {
|
|
533
540
|
if (config2.workflow === "worktree") {
|
|
534
|
-
return buildWorktreePrompt(issue, testRunner);
|
|
541
|
+
return buildWorktreePrompt(issue, testRunner, pm);
|
|
535
542
|
}
|
|
536
|
-
return buildBranchPrompt(issue, config2, testRunner);
|
|
543
|
+
return buildBranchPrompt(issue, config2, testRunner, pm);
|
|
537
544
|
}
|
|
538
|
-
function buildTestInstructions(testRunner) {
|
|
545
|
+
function buildTestInstructions(testRunner, pm = "npm") {
|
|
539
546
|
if (!testRunner) return "";
|
|
547
|
+
const testCmd = pm === "bun" ? "bun run test" : `${pm} run test`;
|
|
540
548
|
return `
|
|
541
549
|
**MANDATORY \u2014 Unit Tests:**
|
|
542
550
|
This project uses **${testRunner}** as its test runner.
|
|
543
551
|
- You MUST write unit tests (\`*.test.ts\`) for every new file or module you create.
|
|
544
552
|
- Tests should cover the main functionality, edge cases, and error scenarios.
|
|
545
|
-
- Run \`
|
|
553
|
+
- Run \`${testCmd}\` and ensure ALL tests pass before committing.
|
|
546
554
|
- Do NOT skip writing tests \u2014 the PR will be blocked if tests are missing or failing.
|
|
547
555
|
`;
|
|
548
556
|
}
|
|
@@ -591,8 +599,8 @@ function buildPrBodyInstructions() {
|
|
|
591
599
|
\`\`\`
|
|
592
600
|
Write in English. Do NOT write a wall of text \u2014 structure the summary using the template above.`;
|
|
593
601
|
}
|
|
594
|
-
function buildWorktreePrompt(issue, testRunner) {
|
|
595
|
-
const testBlock = buildTestInstructions(testRunner ?? null);
|
|
602
|
+
function buildWorktreePrompt(issue, testRunner, pm) {
|
|
603
|
+
const testBlock = buildTestInstructions(testRunner ?? null, pm);
|
|
596
604
|
const readmeBlock = buildReadmeInstructions();
|
|
597
605
|
const hookBlock = buildPreCommitHookInstructions();
|
|
598
606
|
return `You are an autonomous implementation agent. Your job is to implement a single
|
|
@@ -652,13 +660,13 @@ ${testBlock}${readmeBlock}${hookBlock}
|
|
|
652
660
|
- Do NOT create pull requests \u2014 the caller handles that.
|
|
653
661
|
- Do NOT update the issue tracker \u2014 the caller handles that.`;
|
|
654
662
|
}
|
|
655
|
-
function buildBranchPrompt(issue, config2, testRunner) {
|
|
663
|
+
function buildBranchPrompt(issue, config2, testRunner, pm) {
|
|
656
664
|
const workspace = resolve3(config2.workspace);
|
|
657
665
|
const repoEntries = config2.repos.map(
|
|
658
666
|
(r) => ` - If it says "Repo: ${r.name}" or title starts with "${r.match}" \u2192 \`${resolve3(workspace, r.path)}\` (base branch: \`${r.base_branch}\`)`
|
|
659
667
|
).join("\n");
|
|
660
668
|
const baseBranchInstruction = config2.repos.length > 0 ? "From the repo's base branch (listed above)" : `From \`${config2.base_branch}\``;
|
|
661
|
-
const testBlock = buildTestInstructions(testRunner ?? null);
|
|
669
|
+
const testBlock = buildTestInstructions(testRunner ?? null, pm);
|
|
662
670
|
const readmeBlock = buildReadmeInstructions();
|
|
663
671
|
const hookBlock = buildPreCommitHookInstructions();
|
|
664
672
|
const manifestPath = join(workspace, ".lisa-manifest.json");
|
|
@@ -748,8 +756,8 @@ ${hookErrors}
|
|
|
748
756
|
|
|
749
757
|
Focus only on fixing the hook errors. Do not make unrelated changes.`;
|
|
750
758
|
}
|
|
751
|
-
function buildNativeWorktreePrompt(issue, repoPath, testRunner) {
|
|
752
|
-
const testBlock = buildTestInstructions(testRunner ?? null);
|
|
759
|
+
function buildNativeWorktreePrompt(issue, repoPath, testRunner, pm) {
|
|
760
|
+
const testBlock = buildTestInstructions(testRunner ?? null, pm);
|
|
753
761
|
const readmeBlock = buildReadmeInstructions();
|
|
754
762
|
const hookBlock = buildPreCommitHookInstructions();
|
|
755
763
|
const prBodyBlock = buildPrBodyInstructions();
|
|
@@ -864,8 +872,8 @@ ${repoBlock}
|
|
|
864
872
|
- Do NOT push, create pull requests, or update the issue tracker.
|
|
865
873
|
- If only one repo is affected, the plan should have a single step.`;
|
|
866
874
|
}
|
|
867
|
-
function buildScopedImplementPrompt(issue, step, previousResults, testRunner) {
|
|
868
|
-
const testBlock = buildTestInstructions(testRunner ?? null);
|
|
875
|
+
function buildScopedImplementPrompt(issue, step, previousResults, testRunner, pm) {
|
|
876
|
+
const testBlock = buildTestInstructions(testRunner ?? null, pm);
|
|
869
877
|
const readmeBlock = buildReadmeInstructions();
|
|
870
878
|
const hookBlock = buildPreCommitHookInstructions();
|
|
871
879
|
const prBodyBlock = buildPrBodyInstructions();
|
|
@@ -1128,6 +1136,9 @@ var ClaudeProvider = class {
|
|
|
1128
1136
|
writeFileSync4(promptFile, prompt, "utf-8");
|
|
1129
1137
|
try {
|
|
1130
1138
|
const flags = ["-p", "--dangerously-skip-permissions"];
|
|
1139
|
+
if (opts.model) {
|
|
1140
|
+
flags.push("--model", opts.model);
|
|
1141
|
+
}
|
|
1131
1142
|
if (opts.useNativeWorktree) {
|
|
1132
1143
|
flags.push("--worktree");
|
|
1133
1144
|
}
|
|
@@ -1184,16 +1195,16 @@ var ClaudeProvider = class {
|
|
|
1184
1195
|
}
|
|
1185
1196
|
};
|
|
1186
1197
|
|
|
1187
|
-
// src/providers/
|
|
1198
|
+
// src/providers/copilot.ts
|
|
1188
1199
|
import { execSync as execSync2, spawn as spawn3 } from "child_process";
|
|
1189
1200
|
import { appendFileSync as appendFileSync3, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
1190
1201
|
import { tmpdir as tmpdir2 } from "os";
|
|
1191
1202
|
import { join as join4 } from "path";
|
|
1192
|
-
var
|
|
1193
|
-
name = "
|
|
1203
|
+
var CopilotProvider = class {
|
|
1204
|
+
name = "copilot";
|
|
1194
1205
|
async isAvailable() {
|
|
1195
1206
|
try {
|
|
1196
|
-
execSync2("
|
|
1207
|
+
execSync2("copilot version", { stdio: "ignore" });
|
|
1197
1208
|
return true;
|
|
1198
1209
|
} catch {
|
|
1199
1210
|
return false;
|
|
@@ -1205,7 +1216,7 @@ var GeminiProvider = class {
|
|
|
1205
1216
|
const promptFile = join4(tmpDir, "prompt.md");
|
|
1206
1217
|
writeFileSync5(promptFile, prompt, "utf-8");
|
|
1207
1218
|
try {
|
|
1208
|
-
const proc = spawn3("sh", ["-c", `
|
|
1219
|
+
const proc = spawn3("sh", ["-c", `copilot --allow-all -p "$(cat '${promptFile}')"`], {
|
|
1209
1220
|
cwd: opts.cwd,
|
|
1210
1221
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1211
1222
|
});
|
|
@@ -1257,16 +1268,181 @@ var GeminiProvider = class {
|
|
|
1257
1268
|
}
|
|
1258
1269
|
};
|
|
1259
1270
|
|
|
1260
|
-
// src/providers/
|
|
1271
|
+
// src/providers/cursor.ts
|
|
1261
1272
|
import { execSync as execSync3, spawn as spawn4 } from "child_process";
|
|
1262
1273
|
import { appendFileSync as appendFileSync4, mkdtempSync as mkdtempSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
1263
1274
|
import { tmpdir as tmpdir3 } from "os";
|
|
1264
1275
|
import { join as join5 } from "path";
|
|
1276
|
+
function findCursorBinary() {
|
|
1277
|
+
for (const bin of ["agent", "cursor-agent"]) {
|
|
1278
|
+
try {
|
|
1279
|
+
execSync3(`${bin} --version`, { stdio: "ignore" });
|
|
1280
|
+
return bin;
|
|
1281
|
+
} catch {
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
var CursorProvider = class {
|
|
1287
|
+
name = "cursor";
|
|
1288
|
+
async isAvailable() {
|
|
1289
|
+
return findCursorBinary() !== null;
|
|
1290
|
+
}
|
|
1291
|
+
async run(prompt, opts) {
|
|
1292
|
+
const start = Date.now();
|
|
1293
|
+
const bin = findCursorBinary();
|
|
1294
|
+
if (!bin) {
|
|
1295
|
+
return {
|
|
1296
|
+
success: false,
|
|
1297
|
+
output: "cursor agent (agent / cursor-agent) is not installed or not in PATH",
|
|
1298
|
+
duration: Date.now() - start
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
const tmpDir = mkdtempSync3(join5(tmpdir3(), "lisa-"));
|
|
1302
|
+
const promptFile = join5(tmpDir, "prompt.md");
|
|
1303
|
+
writeFileSync6(promptFile, prompt, "utf-8");
|
|
1304
|
+
try {
|
|
1305
|
+
const modelFlag = opts.model ? `--model ${opts.model}` : "";
|
|
1306
|
+
const proc = spawn4(
|
|
1307
|
+
"sh",
|
|
1308
|
+
["-c", `${bin} -p "$(cat '${promptFile}')" --output-format text --force ${modelFlag}`],
|
|
1309
|
+
{
|
|
1310
|
+
cwd: opts.cwd,
|
|
1311
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1312
|
+
}
|
|
1313
|
+
);
|
|
1314
|
+
const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
|
|
1315
|
+
const chunks = [];
|
|
1316
|
+
proc.stdout.on("data", (chunk) => {
|
|
1317
|
+
const text2 = chunk.toString();
|
|
1318
|
+
process.stdout.write(text2);
|
|
1319
|
+
chunks.push(text2);
|
|
1320
|
+
try {
|
|
1321
|
+
appendFileSync4(opts.logFile, text2);
|
|
1322
|
+
} catch {
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
proc.stderr.on("data", (chunk) => {
|
|
1326
|
+
const text2 = chunk.toString();
|
|
1327
|
+
process.stderr.write(text2);
|
|
1328
|
+
try {
|
|
1329
|
+
appendFileSync4(opts.logFile, text2);
|
|
1330
|
+
} catch {
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
const exitCode = await new Promise((resolve6) => {
|
|
1334
|
+
proc.on("close", (code) => {
|
|
1335
|
+
overseer?.stop();
|
|
1336
|
+
resolve6(code ?? 1);
|
|
1337
|
+
});
|
|
1338
|
+
});
|
|
1339
|
+
if (overseer?.wasKilled()) {
|
|
1340
|
+
chunks.push(STUCK_MESSAGE);
|
|
1341
|
+
}
|
|
1342
|
+
return {
|
|
1343
|
+
success: exitCode === 0 && !overseer?.wasKilled(),
|
|
1344
|
+
output: chunks.join(""),
|
|
1345
|
+
duration: Date.now() - start
|
|
1346
|
+
};
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
return {
|
|
1349
|
+
success: false,
|
|
1350
|
+
output: err instanceof Error ? err.message : String(err),
|
|
1351
|
+
duration: Date.now() - start
|
|
1352
|
+
};
|
|
1353
|
+
} finally {
|
|
1354
|
+
try {
|
|
1355
|
+
unlinkSync3(promptFile);
|
|
1356
|
+
} catch {
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// src/providers/gemini.ts
|
|
1363
|
+
import { execSync as execSync4, spawn as spawn5 } from "child_process";
|
|
1364
|
+
import { appendFileSync as appendFileSync5, mkdtempSync as mkdtempSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
1365
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
1366
|
+
import { join as join6 } from "path";
|
|
1367
|
+
var GeminiProvider = class {
|
|
1368
|
+
name = "gemini";
|
|
1369
|
+
async isAvailable() {
|
|
1370
|
+
try {
|
|
1371
|
+
execSync4("gemini --version", { stdio: "ignore" });
|
|
1372
|
+
return true;
|
|
1373
|
+
} catch {
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
async run(prompt, opts) {
|
|
1378
|
+
const start = Date.now();
|
|
1379
|
+
const tmpDir = mkdtempSync4(join6(tmpdir4(), "lisa-"));
|
|
1380
|
+
const promptFile = join6(tmpDir, "prompt.md");
|
|
1381
|
+
writeFileSync7(promptFile, prompt, "utf-8");
|
|
1382
|
+
try {
|
|
1383
|
+
const modelFlag = opts.model ? `--model ${opts.model}` : "";
|
|
1384
|
+
const proc = spawn5("sh", ["-c", `gemini --yolo ${modelFlag} -p "$(cat '${promptFile}')"`], {
|
|
1385
|
+
cwd: opts.cwd,
|
|
1386
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1387
|
+
});
|
|
1388
|
+
const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
|
|
1389
|
+
const chunks = [];
|
|
1390
|
+
proc.stdout.on("data", (chunk) => {
|
|
1391
|
+
const text2 = chunk.toString();
|
|
1392
|
+
process.stdout.write(text2);
|
|
1393
|
+
chunks.push(text2);
|
|
1394
|
+
try {
|
|
1395
|
+
appendFileSync5(opts.logFile, text2);
|
|
1396
|
+
} catch {
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
proc.stderr.on("data", (chunk) => {
|
|
1400
|
+
const text2 = chunk.toString();
|
|
1401
|
+
process.stderr.write(text2);
|
|
1402
|
+
try {
|
|
1403
|
+
appendFileSync5(opts.logFile, text2);
|
|
1404
|
+
} catch {
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
const exitCode = await new Promise((resolve6) => {
|
|
1408
|
+
proc.on("close", (code) => {
|
|
1409
|
+
overseer?.stop();
|
|
1410
|
+
resolve6(code ?? 1);
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
if (overseer?.wasKilled()) {
|
|
1414
|
+
chunks.push(STUCK_MESSAGE);
|
|
1415
|
+
}
|
|
1416
|
+
return {
|
|
1417
|
+
success: exitCode === 0 && !overseer?.wasKilled(),
|
|
1418
|
+
output: chunks.join(""),
|
|
1419
|
+
duration: Date.now() - start
|
|
1420
|
+
};
|
|
1421
|
+
} catch (err) {
|
|
1422
|
+
return {
|
|
1423
|
+
success: false,
|
|
1424
|
+
output: err instanceof Error ? err.message : String(err),
|
|
1425
|
+
duration: Date.now() - start
|
|
1426
|
+
};
|
|
1427
|
+
} finally {
|
|
1428
|
+
try {
|
|
1429
|
+
unlinkSync4(promptFile);
|
|
1430
|
+
} catch {
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
// src/providers/opencode.ts
|
|
1437
|
+
import { execSync as execSync5, spawn as spawn6 } from "child_process";
|
|
1438
|
+
import { appendFileSync as appendFileSync6, mkdtempSync as mkdtempSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
1439
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
1440
|
+
import { join as join7 } from "path";
|
|
1265
1441
|
var OpenCodeProvider = class {
|
|
1266
1442
|
name = "opencode";
|
|
1267
1443
|
async isAvailable() {
|
|
1268
1444
|
try {
|
|
1269
|
-
|
|
1445
|
+
execSync5("opencode --version", { stdio: "ignore" });
|
|
1270
1446
|
return true;
|
|
1271
1447
|
} catch {
|
|
1272
1448
|
return false;
|
|
@@ -1274,11 +1450,11 @@ var OpenCodeProvider = class {
|
|
|
1274
1450
|
}
|
|
1275
1451
|
async run(prompt, opts) {
|
|
1276
1452
|
const start = Date.now();
|
|
1277
|
-
const tmpDir =
|
|
1278
|
-
const promptFile =
|
|
1279
|
-
|
|
1453
|
+
const tmpDir = mkdtempSync5(join7(tmpdir5(), "lisa-"));
|
|
1454
|
+
const promptFile = join7(tmpDir, "prompt.md");
|
|
1455
|
+
writeFileSync8(promptFile, prompt, "utf-8");
|
|
1280
1456
|
try {
|
|
1281
|
-
const proc =
|
|
1457
|
+
const proc = spawn6("sh", ["-c", `opencode run "$(cat '${promptFile}')"`], {
|
|
1282
1458
|
cwd: opts.cwd,
|
|
1283
1459
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1284
1460
|
});
|
|
@@ -1289,7 +1465,7 @@ var OpenCodeProvider = class {
|
|
|
1289
1465
|
process.stdout.write(text2);
|
|
1290
1466
|
chunks.push(text2);
|
|
1291
1467
|
try {
|
|
1292
|
-
|
|
1468
|
+
appendFileSync6(opts.logFile, text2);
|
|
1293
1469
|
} catch {
|
|
1294
1470
|
}
|
|
1295
1471
|
});
|
|
@@ -1297,7 +1473,7 @@ var OpenCodeProvider = class {
|
|
|
1297
1473
|
const text2 = chunk.toString();
|
|
1298
1474
|
process.stderr.write(text2);
|
|
1299
1475
|
try {
|
|
1300
|
-
|
|
1476
|
+
appendFileSync6(opts.logFile, text2);
|
|
1301
1477
|
} catch {
|
|
1302
1478
|
}
|
|
1303
1479
|
});
|
|
@@ -1323,7 +1499,7 @@ var OpenCodeProvider = class {
|
|
|
1323
1499
|
};
|
|
1324
1500
|
} finally {
|
|
1325
1501
|
try {
|
|
1326
|
-
|
|
1502
|
+
unlinkSync5(promptFile);
|
|
1327
1503
|
} catch {
|
|
1328
1504
|
}
|
|
1329
1505
|
}
|
|
@@ -1334,7 +1510,9 @@ var OpenCodeProvider = class {
|
|
|
1334
1510
|
var providers = {
|
|
1335
1511
|
claude: () => new ClaudeProvider(),
|
|
1336
1512
|
gemini: () => new GeminiProvider(),
|
|
1337
|
-
opencode: () => new OpenCodeProvider()
|
|
1513
|
+
opencode: () => new OpenCodeProvider(),
|
|
1514
|
+
copilot: () => new CopilotProvider(),
|
|
1515
|
+
cursor: () => new CursorProvider()
|
|
1338
1516
|
};
|
|
1339
1517
|
async function getAvailableProviders() {
|
|
1340
1518
|
const all = Object.values(providers).map((f) => f());
|
|
@@ -1371,31 +1549,39 @@ var ELIGIBLE_ERROR_PATTERNS = [
|
|
|
1371
1549
|
/not installed/i,
|
|
1372
1550
|
/not in PATH/i,
|
|
1373
1551
|
/command not found/i,
|
|
1374
|
-
/lisa-overseer/i
|
|
1552
|
+
/lisa-overseer/i,
|
|
1553
|
+
/named models unavailable/i,
|
|
1554
|
+
/free plans can only use/i
|
|
1375
1555
|
];
|
|
1376
1556
|
function isEligibleForFallback(output) {
|
|
1377
1557
|
return ELIGIBLE_ERROR_PATTERNS.some((pattern) => pattern.test(output));
|
|
1378
1558
|
}
|
|
1559
|
+
function isCompleteProviderExhaustion(attempts) {
|
|
1560
|
+
if (attempts.length === 0) return false;
|
|
1561
|
+
return attempts.every((a) => !a.success && a.error !== "Non-eligible error");
|
|
1562
|
+
}
|
|
1379
1563
|
async function runWithFallback(models, prompt, opts) {
|
|
1380
1564
|
const attempts = [];
|
|
1381
|
-
for (const
|
|
1382
|
-
const provider = createProvider(
|
|
1565
|
+
for (const spec of models) {
|
|
1566
|
+
const provider = createProvider(spec.provider);
|
|
1383
1567
|
const available = await provider.isAvailable();
|
|
1384
1568
|
if (!available) {
|
|
1385
1569
|
attempts.push({
|
|
1386
|
-
provider:
|
|
1570
|
+
provider: spec.provider,
|
|
1571
|
+
model: spec.model,
|
|
1387
1572
|
success: false,
|
|
1388
|
-
error: `Provider "${
|
|
1573
|
+
error: `Provider "${spec.provider}" is not installed or not in PATH`,
|
|
1389
1574
|
duration: 0
|
|
1390
1575
|
});
|
|
1391
1576
|
continue;
|
|
1392
1577
|
}
|
|
1393
1578
|
const guardrailsSection = opts.guardrailsDir ? buildGuardrailsSection(opts.guardrailsDir) : "";
|
|
1394
1579
|
const fullPrompt = guardrailsSection ? `${prompt}${guardrailsSection}` : prompt;
|
|
1395
|
-
const result = await provider.run(fullPrompt, opts);
|
|
1580
|
+
const result = await provider.run(fullPrompt, { ...opts, model: spec.model });
|
|
1396
1581
|
if (result.success) {
|
|
1397
1582
|
attempts.push({
|
|
1398
|
-
provider:
|
|
1583
|
+
provider: spec.provider,
|
|
1584
|
+
model: spec.model,
|
|
1399
1585
|
success: true,
|
|
1400
1586
|
duration: result.duration
|
|
1401
1587
|
});
|
|
@@ -1403,7 +1589,7 @@ async function runWithFallback(models, prompt, opts) {
|
|
|
1403
1589
|
success: true,
|
|
1404
1590
|
output: result.output,
|
|
1405
1591
|
duration: result.duration,
|
|
1406
|
-
providerUsed: model,
|
|
1592
|
+
providerUsed: spec.model ? `${spec.provider}/${spec.model}` : spec.provider,
|
|
1407
1593
|
provider,
|
|
1408
1594
|
attempts
|
|
1409
1595
|
};
|
|
@@ -1412,14 +1598,15 @@ async function runWithFallback(models, prompt, opts) {
|
|
|
1412
1598
|
appendEntry(opts.guardrailsDir, {
|
|
1413
1599
|
issueId: opts.issueId,
|
|
1414
1600
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1415
|
-
provider:
|
|
1601
|
+
provider: spec.provider,
|
|
1416
1602
|
errorType: extractErrorType(result.output),
|
|
1417
1603
|
context: extractContext(result.output)
|
|
1418
1604
|
});
|
|
1419
1605
|
}
|
|
1420
1606
|
const eligible = isEligibleForFallback(result.output);
|
|
1421
1607
|
attempts.push({
|
|
1422
|
-
provider:
|
|
1608
|
+
provider: spec.provider,
|
|
1609
|
+
model: spec.model,
|
|
1423
1610
|
success: false,
|
|
1424
1611
|
error: eligible ? "Eligible error (quota/unavailable/timeout)" : "Non-eligible error",
|
|
1425
1612
|
duration: result.duration
|
|
@@ -1429,7 +1616,7 @@ async function runWithFallback(models, prompt, opts) {
|
|
|
1429
1616
|
success: false,
|
|
1430
1617
|
output: result.output,
|
|
1431
1618
|
duration: result.duration,
|
|
1432
|
-
providerUsed: model,
|
|
1619
|
+
providerUsed: spec.model ? `${spec.provider}/${spec.model}` : spec.provider,
|
|
1433
1620
|
provider,
|
|
1434
1621
|
attempts
|
|
1435
1622
|
};
|
|
@@ -1440,7 +1627,7 @@ async function runWithFallback(models, prompt, opts) {
|
|
|
1440
1627
|
success: false,
|
|
1441
1628
|
output: formatAttemptsReport(attempts),
|
|
1442
1629
|
duration: totalDuration,
|
|
1443
|
-
providerUsed: attempts[attempts.length - 1]?.provider ?? models[0] ?? "claude",
|
|
1630
|
+
providerUsed: attempts[attempts.length - 1]?.provider ?? models[0]?.provider ?? "claude",
|
|
1444
1631
|
attempts
|
|
1445
1632
|
};
|
|
1446
1633
|
}
|
|
@@ -1450,7 +1637,8 @@ function formatAttemptsReport(attempts) {
|
|
|
1450
1637
|
const status2 = a.success ? "OK" : "FAILED";
|
|
1451
1638
|
const error2 = a.error ? ` \u2014 ${a.error}` : "";
|
|
1452
1639
|
const duration = a.duration > 0 ? ` (${Math.round(a.duration / 1e3)}s)` : "";
|
|
1453
|
-
|
|
1640
|
+
const label = a.model ? `${a.provider}/${a.model}` : a.provider;
|
|
1641
|
+
lines.push(` ${i + 1}. ${label}: ${status2}${error2}${duration}`);
|
|
1454
1642
|
}
|
|
1455
1643
|
return lines.join("\n");
|
|
1456
1644
|
}
|
|
@@ -1893,8 +2081,8 @@ function resetTitle() {
|
|
|
1893
2081
|
}
|
|
1894
2082
|
|
|
1895
2083
|
// src/worktree.ts
|
|
1896
|
-
import { appendFileSync as
|
|
1897
|
-
import { join as
|
|
2084
|
+
import { appendFileSync as appendFileSync7, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
2085
|
+
import { join as join8, resolve as resolve4 } from "path";
|
|
1898
2086
|
import { execa as execa2 } from "execa";
|
|
1899
2087
|
var WORKTREES_DIR = ".worktrees";
|
|
1900
2088
|
function generateBranchName(issueId, title) {
|
|
@@ -1909,7 +2097,7 @@ async function cleanupOrphanedWorktree(repoRoot, branchName) {
|
|
|
1909
2097
|
if (!branchList.trim()) {
|
|
1910
2098
|
return false;
|
|
1911
2099
|
}
|
|
1912
|
-
const worktreePath =
|
|
2100
|
+
const worktreePath = join8(repoRoot, WORKTREES_DIR, branchName);
|
|
1913
2101
|
const { stdout: worktreeList } = await execa2("git", ["worktree", "list", "--porcelain"], {
|
|
1914
2102
|
cwd: repoRoot,
|
|
1915
2103
|
reject: false
|
|
@@ -1922,7 +2110,7 @@ async function cleanupOrphanedWorktree(repoRoot, branchName) {
|
|
|
1922
2110
|
return true;
|
|
1923
2111
|
}
|
|
1924
2112
|
async function createWorktree(repoRoot, branchName, baseBranch) {
|
|
1925
|
-
const worktreePath =
|
|
2113
|
+
const worktreePath = join8(repoRoot, WORKTREES_DIR, branchName);
|
|
1926
2114
|
await cleanupOrphanedWorktree(repoRoot, branchName);
|
|
1927
2115
|
await execa2("git", ["fetch", "origin", baseBranch], { cwd: repoRoot });
|
|
1928
2116
|
await execa2("git", ["worktree", "add", "-b", branchName, worktreePath, `origin/${baseBranch}`], {
|
|
@@ -1937,16 +2125,16 @@ async function removeWorktree(repoRoot, worktreePath) {
|
|
|
1937
2125
|
await execa2("git", ["worktree", "prune"], { cwd: repoRoot });
|
|
1938
2126
|
}
|
|
1939
2127
|
function ensureWorktreeGitignore(repoRoot) {
|
|
1940
|
-
const gitignorePath =
|
|
2128
|
+
const gitignorePath = join8(repoRoot, ".gitignore");
|
|
1941
2129
|
if (!existsSync5(gitignorePath)) {
|
|
1942
|
-
|
|
2130
|
+
appendFileSync7(gitignorePath, `${WORKTREES_DIR}
|
|
1943
2131
|
`);
|
|
1944
2132
|
return;
|
|
1945
2133
|
}
|
|
1946
2134
|
const content = readFileSync4(gitignorePath, "utf-8");
|
|
1947
2135
|
if (!content.split("\n").some((line) => line.trim() === WORKTREES_DIR)) {
|
|
1948
2136
|
const separator = content.endsWith("\n") ? "" : "\n";
|
|
1949
|
-
|
|
2137
|
+
appendFileSync7(gitignorePath, `${separator}${WORKTREES_DIR}
|
|
1950
2138
|
`);
|
|
1951
2139
|
}
|
|
1952
2140
|
}
|
|
@@ -1977,15 +2165,15 @@ function determineRepoPath(repos, issue, workspace) {
|
|
|
1977
2165
|
if (repos.length === 0) return void 0;
|
|
1978
2166
|
if (issue.repo) {
|
|
1979
2167
|
const match = repos.find((r) => r.name === issue.repo);
|
|
1980
|
-
if (match) return
|
|
2168
|
+
if (match) return join8(workspace, match.path);
|
|
1981
2169
|
}
|
|
1982
2170
|
for (const r of repos) {
|
|
1983
2171
|
if (r.match && issue.title.startsWith(r.match)) {
|
|
1984
|
-
return
|
|
2172
|
+
return join8(workspace, r.path);
|
|
1985
2173
|
}
|
|
1986
2174
|
}
|
|
1987
2175
|
const first = repos[0];
|
|
1988
|
-
return first ?
|
|
2176
|
+
return first ? join8(workspace, first.path) : void 0;
|
|
1989
2177
|
}
|
|
1990
2178
|
async function detectFeatureBranches(repos, issueId, workspace, globalBaseBranch) {
|
|
1991
2179
|
const entries = repos.length > 0 ? repos.map((r) => ({ path: resolve4(workspace, r.path), baseBranch: r.base_branch })) : [{ path: workspace, baseBranch: globalBaseBranch }];
|
|
@@ -2025,8 +2213,30 @@ async function detectFeatureBranches(repos, issueId, workspace, globalBaseBranch
|
|
|
2025
2213
|
var activeCleanup = null;
|
|
2026
2214
|
var shuttingDown = false;
|
|
2027
2215
|
function resolveModels(config2) {
|
|
2028
|
-
if (config2.models
|
|
2029
|
-
|
|
2216
|
+
if (!config2.models || config2.models.length === 0) {
|
|
2217
|
+
return [{ provider: config2.provider }];
|
|
2218
|
+
}
|
|
2219
|
+
const knownProviders = /* @__PURE__ */ new Set(["claude", "gemini", "opencode", "copilot", "cursor"]);
|
|
2220
|
+
for (const m of config2.models) {
|
|
2221
|
+
if (knownProviders.has(m) && m !== config2.provider) {
|
|
2222
|
+
warn(
|
|
2223
|
+
`Model "${m}" looks like a provider name but provider is "${config2.provider}". Since v1.4.0, "models" lists model names within the configured provider, not provider names. Update your .lisa/config.yaml.`
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
if (config2.provider === "cursor") {
|
|
2228
|
+
const hasAuto = config2.models.some((m) => m.toLowerCase() === "auto");
|
|
2229
|
+
if (!hasAuto) {
|
|
2230
|
+
warn(
|
|
2231
|
+
"Cursor Free plan detected (or model not set to 'auto'). Forcing 'auto' model. Set model to 'auto' explicitly in .lisa/config.yaml to silence this warning."
|
|
2232
|
+
);
|
|
2233
|
+
return [{ provider: config2.provider, model: "auto" }];
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
return config2.models.map((m) => ({
|
|
2237
|
+
provider: config2.provider,
|
|
2238
|
+
model: m === config2.provider ? void 0 : m
|
|
2239
|
+
}));
|
|
2030
2240
|
}
|
|
2031
2241
|
function buildPrBody(providerUsed, description) {
|
|
2032
2242
|
const lines = [];
|
|
@@ -2046,7 +2256,7 @@ function buildPrBody(providerUsed, description) {
|
|
|
2046
2256
|
var PR_TITLE_FILE = ".pr-title";
|
|
2047
2257
|
function readPrTitle(cwd) {
|
|
2048
2258
|
try {
|
|
2049
|
-
const title = readFileSync5(
|
|
2259
|
+
const title = readFileSync5(join9(cwd, PR_TITLE_FILE), "utf-8").trim().split("\n")[0]?.trim();
|
|
2050
2260
|
return title || null;
|
|
2051
2261
|
} catch {
|
|
2052
2262
|
return null;
|
|
@@ -2054,13 +2264,13 @@ function readPrTitle(cwd) {
|
|
|
2054
2264
|
}
|
|
2055
2265
|
function cleanupPrTitle(cwd) {
|
|
2056
2266
|
try {
|
|
2057
|
-
|
|
2267
|
+
unlinkSync6(join9(cwd, PR_TITLE_FILE));
|
|
2058
2268
|
} catch {
|
|
2059
2269
|
}
|
|
2060
2270
|
}
|
|
2061
2271
|
var PLAN_FILE = ".lisa-plan.json";
|
|
2062
2272
|
function readLisaPlan(dir) {
|
|
2063
|
-
const planPath =
|
|
2273
|
+
const planPath = join9(dir, PLAN_FILE);
|
|
2064
2274
|
if (!existsSync6(planPath)) return null;
|
|
2065
2275
|
try {
|
|
2066
2276
|
return JSON.parse(readFileSync5(planPath, "utf-8").trim());
|
|
@@ -2070,13 +2280,13 @@ function readLisaPlan(dir) {
|
|
|
2070
2280
|
}
|
|
2071
2281
|
function cleanupPlan(dir) {
|
|
2072
2282
|
try {
|
|
2073
|
-
|
|
2283
|
+
unlinkSync6(join9(dir, PLAN_FILE));
|
|
2074
2284
|
} catch {
|
|
2075
2285
|
}
|
|
2076
2286
|
}
|
|
2077
2287
|
var MANIFEST_FILE = ".lisa-manifest.json";
|
|
2078
2288
|
function readLisaManifest(dir) {
|
|
2079
|
-
const manifestPath =
|
|
2289
|
+
const manifestPath = join9(dir, MANIFEST_FILE);
|
|
2080
2290
|
if (!existsSync6(manifestPath)) return null;
|
|
2081
2291
|
try {
|
|
2082
2292
|
return JSON.parse(readFileSync5(manifestPath, "utf-8").trim());
|
|
@@ -2086,7 +2296,7 @@ function readLisaManifest(dir) {
|
|
|
2086
2296
|
}
|
|
2087
2297
|
function cleanupManifest(dir) {
|
|
2088
2298
|
try {
|
|
2089
|
-
|
|
2299
|
+
unlinkSync6(join9(dir, MANIFEST_FILE));
|
|
2090
2300
|
} catch {
|
|
2091
2301
|
}
|
|
2092
2302
|
}
|
|
@@ -2212,7 +2422,7 @@ async function runLoop(config2, opts) {
|
|
|
2212
2422
|
const models = resolveModels(config2);
|
|
2213
2423
|
installSignalHandlers();
|
|
2214
2424
|
log(
|
|
2215
|
-
`Starting loop (models: ${models.join(" \u2192 ")}, source: ${config2.source}, label: ${config2.source_config.label}, workflow: ${config2.workflow})`
|
|
2425
|
+
`Starting loop (models: ${models.map((m) => m.model ? `${m.provider}/${m.model}` : m.provider).join(" \u2192 ")}, source: ${config2.source}, label: ${config2.source_config.label}, workflow: ${config2.workflow})`
|
|
2216
2426
|
);
|
|
2217
2427
|
if (!opts.dryRun) {
|
|
2218
2428
|
await recoverOrphanIssues(source, config2);
|
|
@@ -2243,7 +2453,9 @@ async function runLoop(config2, opts) {
|
|
|
2243
2453
|
);
|
|
2244
2454
|
}
|
|
2245
2455
|
log(`[dry-run] Workflow mode: ${config2.workflow}`);
|
|
2246
|
-
log(
|
|
2456
|
+
log(
|
|
2457
|
+
`[dry-run] Models priority: ${models.map((m) => m.model ? `${m.provider}/${m.model}` : m.provider).join(" \u2192 ")}`
|
|
2458
|
+
);
|
|
2247
2459
|
log("[dry-run] Then implement, push, create PR, and update issue status");
|
|
2248
2460
|
break;
|
|
2249
2461
|
}
|
|
@@ -2319,6 +2531,12 @@ async function runLoop(config2, opts) {
|
|
|
2319
2531
|
log("Single iteration mode. Exiting.");
|
|
2320
2532
|
break;
|
|
2321
2533
|
}
|
|
2534
|
+
if (isCompleteProviderExhaustion(sessionResult.fallback.attempts)) {
|
|
2535
|
+
error(
|
|
2536
|
+
"All providers exhausted due to infrastructure issues (quota, plan limits, or not installed). Fix your provider configuration and restart lisa."
|
|
2537
|
+
);
|
|
2538
|
+
break;
|
|
2539
|
+
}
|
|
2322
2540
|
log(`Cooling down ${config2.loop.cooldown}s before next issue...`);
|
|
2323
2541
|
setTitle("Lisa \u2014 cooling down...");
|
|
2324
2542
|
await sleep(config2.loop.cooldown * 1e3);
|
|
@@ -2408,9 +2626,10 @@ function findRepoConfig(config2, issue) {
|
|
|
2408
2626
|
async function runTestValidation(cwd) {
|
|
2409
2627
|
const testRunner = detectTestRunner(cwd);
|
|
2410
2628
|
if (!testRunner) return true;
|
|
2411
|
-
|
|
2629
|
+
const pm = detectPackageManager(cwd);
|
|
2630
|
+
log(`Running test validation (${testRunner} via ${pm})...`);
|
|
2412
2631
|
try {
|
|
2413
|
-
await execa3(
|
|
2632
|
+
await execa3(pm, ["run", "test"], { cwd, stdio: "pipe" });
|
|
2414
2633
|
ok("Tests passed.");
|
|
2415
2634
|
return true;
|
|
2416
2635
|
} catch (err) {
|
|
@@ -2444,7 +2663,7 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
|
|
|
2444
2663
|
const workspace = resolve5(config2.workspace);
|
|
2445
2664
|
const repoPath = determineRepoPath(config2.repos, issue, workspace) ?? workspace;
|
|
2446
2665
|
const defaultBranch = resolveBaseBranch(config2, repoPath);
|
|
2447
|
-
const primaryProvider = createProvider(models[0] ?? "claude");
|
|
2666
|
+
const primaryProvider = createProvider(models[0]?.provider ?? "claude");
|
|
2448
2667
|
const useNativeWorktree = primaryProvider.supportsNativeWorktree === true;
|
|
2449
2668
|
if (useNativeWorktree) {
|
|
2450
2669
|
return runNativeWorktreeSession(
|
|
@@ -2473,13 +2692,14 @@ async function runNativeWorktreeSession(config2, issue, logFile, session, models
|
|
|
2473
2692
|
stopSpinner();
|
|
2474
2693
|
if (!started) {
|
|
2475
2694
|
error(`Lifecycle startup failed for ${issue.id}. Aborting session.`);
|
|
2476
|
-
return failResult(models[0] ?? "claude");
|
|
2695
|
+
return failResult(models[0]?.provider ?? "claude");
|
|
2477
2696
|
}
|
|
2478
2697
|
}
|
|
2479
2698
|
const testRunner = detectTestRunner(repoPath);
|
|
2480
2699
|
if (testRunner) log(`Detected test runner: ${testRunner}`);
|
|
2700
|
+
const pm = detectPackageManager(repoPath);
|
|
2481
2701
|
cleanupManifest(repoPath);
|
|
2482
|
-
const prompt = buildNativeWorktreePrompt(issue, repoPath, testRunner);
|
|
2702
|
+
const prompt = buildNativeWorktreePrompt(issue, repoPath, testRunner, pm);
|
|
2483
2703
|
startSpinner(`${issue.id} \u2014 implementing (native worktree)...`);
|
|
2484
2704
|
log(`Implementing with native worktree... (log: ${logFile})`);
|
|
2485
2705
|
initLogFile(logFile);
|
|
@@ -2493,7 +2713,7 @@ async function runNativeWorktreeSession(config2, issue, logFile, session, models
|
|
|
2493
2713
|
});
|
|
2494
2714
|
stopSpinner();
|
|
2495
2715
|
try {
|
|
2496
|
-
|
|
2716
|
+
appendFileSync8(
|
|
2497
2717
|
logFile,
|
|
2498
2718
|
`
|
|
2499
2719
|
${"=".repeat(80)}
|
|
@@ -2589,13 +2809,13 @@ async function runManualWorktreeSession(config2, issue, logFile, session, models
|
|
|
2589
2809
|
error(`Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`);
|
|
2590
2810
|
return {
|
|
2591
2811
|
success: false,
|
|
2592
|
-
providerUsed: models[0] ?? "claude",
|
|
2812
|
+
providerUsed: models[0]?.provider ?? "claude",
|
|
2593
2813
|
prUrls: [],
|
|
2594
2814
|
fallback: {
|
|
2595
2815
|
success: false,
|
|
2596
2816
|
output: "",
|
|
2597
2817
|
duration: 0,
|
|
2598
|
-
providerUsed: models[0] ?? "claude",
|
|
2818
|
+
providerUsed: models[0]?.provider ?? "claude",
|
|
2599
2819
|
attempts: []
|
|
2600
2820
|
}
|
|
2601
2821
|
};
|
|
@@ -2612,13 +2832,13 @@ async function runManualWorktreeSession(config2, issue, logFile, session, models
|
|
|
2612
2832
|
await cleanupWorktree(repoPath, worktreePath);
|
|
2613
2833
|
return {
|
|
2614
2834
|
success: false,
|
|
2615
|
-
providerUsed: models[0] ?? "claude",
|
|
2835
|
+
providerUsed: models[0]?.provider ?? "claude",
|
|
2616
2836
|
prUrls: [],
|
|
2617
2837
|
fallback: {
|
|
2618
2838
|
success: false,
|
|
2619
2839
|
output: "",
|
|
2620
2840
|
duration: 0,
|
|
2621
|
-
providerUsed: models[0] ?? "claude",
|
|
2841
|
+
providerUsed: models[0]?.provider ?? "claude",
|
|
2622
2842
|
attempts: []
|
|
2623
2843
|
}
|
|
2624
2844
|
};
|
|
@@ -2628,7 +2848,8 @@ async function runManualWorktreeSession(config2, issue, logFile, session, models
|
|
|
2628
2848
|
if (testRunner) {
|
|
2629
2849
|
log(`Detected test runner: ${testRunner}`);
|
|
2630
2850
|
}
|
|
2631
|
-
const
|
|
2851
|
+
const pm = detectPackageManager(worktreePath);
|
|
2852
|
+
const prompt = buildImplementPrompt(issue, config2, testRunner, pm);
|
|
2632
2853
|
startSpinner(`${issue.id} \u2014 implementing...`);
|
|
2633
2854
|
log(`Implementing in worktree... (log: ${logFile})`);
|
|
2634
2855
|
initLogFile(logFile);
|
|
@@ -2641,7 +2862,7 @@ async function runManualWorktreeSession(config2, issue, logFile, session, models
|
|
|
2641
2862
|
});
|
|
2642
2863
|
stopSpinner();
|
|
2643
2864
|
try {
|
|
2644
|
-
|
|
2865
|
+
appendFileSync8(
|
|
2645
2866
|
logFile,
|
|
2646
2867
|
`
|
|
2647
2868
|
${"=".repeat(80)}
|
|
@@ -2745,7 +2966,7 @@ async function runWorktreeMultiRepoSession(config2, issue, logFile, session, mod
|
|
|
2745
2966
|
});
|
|
2746
2967
|
stopSpinner();
|
|
2747
2968
|
try {
|
|
2748
|
-
|
|
2969
|
+
appendFileSync8(
|
|
2749
2970
|
logFile,
|
|
2750
2971
|
`
|
|
2751
2972
|
${"=".repeat(80)}
|
|
@@ -2838,13 +3059,25 @@ async function runMultiRepoStep(config2, issue, step, previousResults, logFile,
|
|
|
2838
3059
|
} catch (err) {
|
|
2839
3060
|
stopSpinner();
|
|
2840
3061
|
error(`Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`);
|
|
2841
|
-
return failResult(models[0] ?? "claude");
|
|
3062
|
+
return failResult(models[0]?.provider ?? "claude");
|
|
2842
3063
|
}
|
|
2843
3064
|
stopSpinner();
|
|
2844
3065
|
ok(`Worktree created at ${worktreePath}`);
|
|
2845
3066
|
const testRunner = detectTestRunner(worktreePath);
|
|
2846
3067
|
if (testRunner) log(`Detected test runner: ${testRunner}`);
|
|
2847
|
-
const
|
|
3068
|
+
const pm = detectPackageManager(worktreePath);
|
|
3069
|
+
const repoConfig = config2.repos.find((r) => resolve5(config2.workspace, r.path) === step.repoPath);
|
|
3070
|
+
if (repoConfig?.lifecycle) {
|
|
3071
|
+
startSpinner(`${issue.id} step ${stepNum} \u2014 starting resources...`);
|
|
3072
|
+
const started = await startResources(repoConfig, worktreePath);
|
|
3073
|
+
stopSpinner();
|
|
3074
|
+
if (!started) {
|
|
3075
|
+
error(`Lifecycle startup failed for step ${stepNum}. Aborting.`);
|
|
3076
|
+
await cleanupWorktree(repoPath, worktreePath);
|
|
3077
|
+
return failResult(models[0]?.provider ?? "claude");
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
const prompt = buildScopedImplementPrompt(issue, step, previousResults, testRunner, pm);
|
|
2848
3081
|
startSpinner(`${issue.id} step ${stepNum} \u2014 implementing...`);
|
|
2849
3082
|
const result = await runWithFallback(models, prompt, {
|
|
2850
3083
|
logFile,
|
|
@@ -2854,8 +3087,9 @@ async function runMultiRepoStep(config2, issue, step, previousResults, logFile,
|
|
|
2854
3087
|
overseer: config2.overseer
|
|
2855
3088
|
});
|
|
2856
3089
|
stopSpinner();
|
|
3090
|
+
if (repoConfig?.lifecycle) await stopResources();
|
|
2857
3091
|
try {
|
|
2858
|
-
|
|
3092
|
+
appendFileSync8(
|
|
2859
3093
|
logFile,
|
|
2860
3094
|
`
|
|
2861
3095
|
${"=".repeat(80)}
|
|
@@ -2948,7 +3182,8 @@ async function runBranchSession(config2, issue, logFile, session, models) {
|
|
|
2948
3182
|
if (testRunner) {
|
|
2949
3183
|
log(`Detected test runner: ${testRunner}`);
|
|
2950
3184
|
}
|
|
2951
|
-
const
|
|
3185
|
+
const pm = detectPackageManager(workspace);
|
|
3186
|
+
const prompt = buildImplementPrompt(issue, config2, testRunner, pm);
|
|
2952
3187
|
const repo = findRepoConfig(config2, issue);
|
|
2953
3188
|
if (repo?.lifecycle) {
|
|
2954
3189
|
startSpinner(`${issue.id} \u2014 starting resources...`);
|
|
@@ -2959,13 +3194,13 @@ async function runBranchSession(config2, issue, logFile, session, models) {
|
|
|
2959
3194
|
error(`Lifecycle startup failed for ${issue.id}. Aborting session.`);
|
|
2960
3195
|
return {
|
|
2961
3196
|
success: false,
|
|
2962
|
-
providerUsed: models[0] ?? "claude",
|
|
3197
|
+
providerUsed: models[0]?.provider ?? "claude",
|
|
2963
3198
|
prUrls: [],
|
|
2964
3199
|
fallback: {
|
|
2965
3200
|
success: false,
|
|
2966
3201
|
output: "",
|
|
2967
3202
|
duration: 0,
|
|
2968
|
-
providerUsed: models[0] ?? "claude",
|
|
3203
|
+
providerUsed: models[0]?.provider ?? "claude",
|
|
2969
3204
|
attempts: []
|
|
2970
3205
|
}
|
|
2971
3206
|
};
|
|
@@ -2983,7 +3218,7 @@ async function runBranchSession(config2, issue, logFile, session, models) {
|
|
|
2983
3218
|
});
|
|
2984
3219
|
stopSpinner();
|
|
2985
3220
|
try {
|
|
2986
|
-
|
|
3221
|
+
appendFileSync8(
|
|
2987
3222
|
logFile,
|
|
2988
3223
|
`
|
|
2989
3224
|
${"=".repeat(80)}
|
|
@@ -3208,6 +3443,56 @@ function getVersion() {
|
|
|
3208
3443
|
return "0.0.0";
|
|
3209
3444
|
}
|
|
3210
3445
|
}
|
|
3446
|
+
var CURSOR_FREE_PLAN_ERROR = "Free plans can only use Auto";
|
|
3447
|
+
async function isCursorFreePlan() {
|
|
3448
|
+
const { mkdtempSync: mkdtempSync6, unlinkSync: unlinkSync7, writeFileSync: writeFileSync9 } = await import("fs");
|
|
3449
|
+
const tmpDir = mkdtempSync6(join10(tmpdir6(), "lisa-cursor-check-"));
|
|
3450
|
+
const promptFile = join10(tmpDir, "prompt.txt");
|
|
3451
|
+
writeFileSync9(promptFile, "test", "utf-8");
|
|
3452
|
+
try {
|
|
3453
|
+
const bin = ["agent", "cursor-agent"].find((b) => {
|
|
3454
|
+
try {
|
|
3455
|
+
execSync6(`${b} --version`, { stdio: "ignore" });
|
|
3456
|
+
return true;
|
|
3457
|
+
} catch {
|
|
3458
|
+
return false;
|
|
3459
|
+
}
|
|
3460
|
+
});
|
|
3461
|
+
if (!bin) return false;
|
|
3462
|
+
const output = execSync6(`${bin} -p "$(cat '${promptFile}')" --output-format text`, {
|
|
3463
|
+
cwd: process.cwd(),
|
|
3464
|
+
encoding: "utf-8",
|
|
3465
|
+
timeout: 3e4
|
|
3466
|
+
});
|
|
3467
|
+
return output.includes(CURSOR_FREE_PLAN_ERROR);
|
|
3468
|
+
} catch (err) {
|
|
3469
|
+
const errorOutput = err instanceof Error ? err.message : String(err);
|
|
3470
|
+
return errorOutput.includes(CURSOR_FREE_PLAN_ERROR);
|
|
3471
|
+
} finally {
|
|
3472
|
+
try {
|
|
3473
|
+
unlinkSync7(promptFile);
|
|
3474
|
+
} catch {
|
|
3475
|
+
}
|
|
3476
|
+
try {
|
|
3477
|
+
execSync6(`rm -rf ${tmpDir}`, { stdio: "ignore" });
|
|
3478
|
+
} catch {
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
var CURSOR_MODELS = [
|
|
3483
|
+
"auto",
|
|
3484
|
+
"composer-1.5",
|
|
3485
|
+
"composer-1",
|
|
3486
|
+
"gpt-5.3-codex",
|
|
3487
|
+
"gpt-5.3-codex-low",
|
|
3488
|
+
"gpt-5.3-codex-high",
|
|
3489
|
+
"gpt-5.3-codex-xhigh",
|
|
3490
|
+
"gpt-5.3-codex-fast",
|
|
3491
|
+
"sonnet-4.6",
|
|
3492
|
+
"sonnet-4.6-thinking",
|
|
3493
|
+
"sonnet-4.5",
|
|
3494
|
+
"sonnet-4.5-thinking"
|
|
3495
|
+
];
|
|
3211
3496
|
var main = defineCommand({
|
|
3212
3497
|
meta: {
|
|
3213
3498
|
name: "lisa",
|
|
@@ -3221,7 +3506,13 @@ async function runConfigWizard() {
|
|
|
3221
3506
|
const providerLabels = {
|
|
3222
3507
|
claude: "Claude Code",
|
|
3223
3508
|
gemini: "Gemini CLI",
|
|
3224
|
-
opencode: "OpenCode"
|
|
3509
|
+
opencode: "OpenCode",
|
|
3510
|
+
copilot: "GitHub Copilot CLI",
|
|
3511
|
+
cursor: "Cursor Agent"
|
|
3512
|
+
};
|
|
3513
|
+
const providerModels = {
|
|
3514
|
+
claude: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"],
|
|
3515
|
+
gemini: ["gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro"]
|
|
3225
3516
|
};
|
|
3226
3517
|
const available = await getAvailableProviders();
|
|
3227
3518
|
if (available.length === 0) {
|
|
@@ -3252,6 +3543,29 @@ After installing, run ${pc2.cyan("lisa init")} again.`
|
|
|
3252
3543
|
if (clack.isCancel(selected)) return process.exit(0);
|
|
3253
3544
|
providerName = selected;
|
|
3254
3545
|
}
|
|
3546
|
+
let selectedModels = [];
|
|
3547
|
+
let availableModels = providerModels[providerName];
|
|
3548
|
+
if (providerName === "cursor") {
|
|
3549
|
+
const isFree = await isCursorFreePlan();
|
|
3550
|
+
if (isFree) {
|
|
3551
|
+
availableModels = ["auto"];
|
|
3552
|
+
clack.log.info("Cursor Free plan detected. Using 'auto' model only.");
|
|
3553
|
+
} else {
|
|
3554
|
+
availableModels = CURSOR_MODELS;
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
if (availableModels && availableModels.length > 0) {
|
|
3558
|
+
const modelSelection = await clack.multiselect({
|
|
3559
|
+
message: "Which models to use? Select in order: primary first, then fallbacks",
|
|
3560
|
+
options: availableModels.map((m) => ({
|
|
3561
|
+
value: m,
|
|
3562
|
+
label: m
|
|
3563
|
+
})),
|
|
3564
|
+
required: false
|
|
3565
|
+
});
|
|
3566
|
+
if (clack.isCancel(modelSelection)) return process.exit(0);
|
|
3567
|
+
selectedModels = modelSelection ?? [];
|
|
3568
|
+
}
|
|
3255
3569
|
const source = await clack.select({
|
|
3256
3570
|
message: "Where do your issues live?",
|
|
3257
3571
|
options: [
|
|
@@ -3378,6 +3692,7 @@ Then run: ${pc2.cyan(`source ${shell}`)}`
|
|
|
3378
3692
|
}
|
|
3379
3693
|
const cfg = {
|
|
3380
3694
|
provider: providerName,
|
|
3695
|
+
...selectedModels.length > 0 ? { models: selectedModels } : {},
|
|
3381
3696
|
source,
|
|
3382
3697
|
source_config: {
|
|
3383
3698
|
team,
|
|
@@ -3424,12 +3739,12 @@ async function detectGitHubMethod() {
|
|
|
3424
3739
|
}
|
|
3425
3740
|
async function detectGitRepos() {
|
|
3426
3741
|
const cwd = process.cwd();
|
|
3427
|
-
if (existsSync7(
|
|
3742
|
+
if (existsSync7(join10(cwd, ".git"))) {
|
|
3428
3743
|
clack.log.info(`Detected git repository in current directory.`);
|
|
3429
3744
|
return [];
|
|
3430
3745
|
}
|
|
3431
3746
|
const entries = readdirSync(cwd, { withFileTypes: true });
|
|
3432
|
-
const gitDirs = entries.filter((e) => e.isDirectory() && existsSync7(
|
|
3747
|
+
const gitDirs = entries.filter((e) => e.isDirectory() && existsSync7(join10(cwd, e.name, ".git"))).map((e) => e.name);
|
|
3433
3748
|
if (gitDirs.length === 0) {
|
|
3434
3749
|
return [];
|
|
3435
3750
|
}
|
|
@@ -3439,7 +3754,7 @@ async function detectGitRepos() {
|
|
|
3439
3754
|
});
|
|
3440
3755
|
if (clack.isCancel(selected)) return process.exit(0);
|
|
3441
3756
|
return selected.map((dir) => ({
|
|
3442
|
-
name: getGitRepoName(
|
|
3757
|
+
name: getGitRepoName(join10(cwd, dir)) ?? dir,
|
|
3443
3758
|
path: `./${dir}`,
|
|
3444
3759
|
match: "",
|
|
3445
3760
|
base_branch: ""
|
|
@@ -3447,7 +3762,7 @@ async function detectGitRepos() {
|
|
|
3447
3762
|
}
|
|
3448
3763
|
function detectDefaultBranch(repoPath) {
|
|
3449
3764
|
try {
|
|
3450
|
-
const ref =
|
|
3765
|
+
const ref = execSync6("git symbolic-ref refs/remotes/origin/HEAD --short", {
|
|
3451
3766
|
cwd: repoPath,
|
|
3452
3767
|
encoding: "utf-8"
|
|
3453
3768
|
}).trim();
|
|
@@ -3458,7 +3773,7 @@ function detectDefaultBranch(repoPath) {
|
|
|
3458
3773
|
}
|
|
3459
3774
|
function getGitRepoName(repoPath) {
|
|
3460
3775
|
try {
|
|
3461
|
-
const url =
|
|
3776
|
+
const url = execSync6("git remote get-url origin", { cwd: repoPath, encoding: "utf-8" }).trim();
|
|
3462
3777
|
const match = url.match(/\/([^/]+?)(?:\.git)?$/) ?? url.match(/:([^/]+?)(?:\.git)?$/);
|
|
3463
3778
|
return match?.[1] ?? null;
|
|
3464
3779
|
} catch {
|