@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.
Files changed (3) hide show
  1. package/README.md +9 -8
  2. package/dist/index.js +406 -91
  3. 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 provider in order — transient errors (429, quota, timeout, network) trigger the next provider. Non-transient errors stop the chain immediately.
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
- - gemini
110
- - opencode
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 single `provider` field.
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, requiring the agent to write unit tests for new code and run `npm run test` before committing.
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 execSync4 } from "child_process";
4
+ import { execSync as execSync6 } from "child_process";
5
5
  import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6 } from "fs";
6
- import { join as join8, resolve as resolvePath } from "path";
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 appendFileSync6, existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync4 } from "fs";
335
- import { join as join7, resolve as resolve5 } from "path";
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 \`npm run test\` and ensure ALL tests pass before committing.
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/gemini.ts
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 GeminiProvider = class {
1193
- name = "gemini";
1203
+ var CopilotProvider = class {
1204
+ name = "copilot";
1194
1205
  async isAvailable() {
1195
1206
  try {
1196
- execSync2("gemini --version", { stdio: "ignore" });
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", `gemini --yolo -p "$(cat '${promptFile}')"`], {
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/opencode.ts
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
- execSync3("opencode --version", { stdio: "ignore" });
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 = mkdtempSync3(join5(tmpdir3(), "lisa-"));
1278
- const promptFile = join5(tmpDir, "prompt.md");
1279
- writeFileSync6(promptFile, prompt, "utf-8");
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 = spawn4("sh", ["-c", `opencode run "$(cat '${promptFile}')"`], {
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
- appendFileSync4(opts.logFile, text2);
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
- appendFileSync4(opts.logFile, text2);
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
- unlinkSync3(promptFile);
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 model of models) {
1382
- const provider = createProvider(model);
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: model,
1570
+ provider: spec.provider,
1571
+ model: spec.model,
1387
1572
  success: false,
1388
- error: `Provider "${model}" is not installed or not in PATH`,
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: model,
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: model,
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: model,
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
- lines.push(` ${i + 1}. ${a.provider}: ${status2}${error2}${duration}`);
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 appendFileSync5, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1897
- import { join as join6, resolve as resolve4 } from "path";
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 = join6(repoRoot, WORKTREES_DIR, branchName);
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 = join6(repoRoot, WORKTREES_DIR, branchName);
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 = join6(repoRoot, ".gitignore");
2128
+ const gitignorePath = join8(repoRoot, ".gitignore");
1941
2129
  if (!existsSync5(gitignorePath)) {
1942
- appendFileSync5(gitignorePath, `${WORKTREES_DIR}
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
- appendFileSync5(gitignorePath, `${separator}${WORKTREES_DIR}
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 join6(workspace, match.path);
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 join6(workspace, r.path);
2172
+ return join8(workspace, r.path);
1985
2173
  }
1986
2174
  }
1987
2175
  const first = repos[0];
1988
- return first ? join6(workspace, first.path) : void 0;
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 && config2.models.length > 0) return config2.models;
2029
- return [config2.provider];
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(join7(cwd, PR_TITLE_FILE), "utf-8").trim().split("\n")[0]?.trim();
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
- unlinkSync4(join7(cwd, PR_TITLE_FILE));
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 = join7(dir, PLAN_FILE);
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
- unlinkSync4(join7(dir, PLAN_FILE));
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 = join7(dir, MANIFEST_FILE);
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
- unlinkSync4(join7(dir, MANIFEST_FILE));
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(`[dry-run] Models priority: ${models.join(" \u2192 ")}`);
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
- log(`Running test validation (${testRunner} detected)...`);
2629
+ const pm = detectPackageManager(cwd);
2630
+ log(`Running test validation (${testRunner} via ${pm})...`);
2412
2631
  try {
2413
- await execa3("npm", ["run", "test"], { cwd, stdio: "pipe" });
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
- appendFileSync6(
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 prompt = buildImplementPrompt(issue, config2, testRunner);
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
- appendFileSync6(
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
- appendFileSync6(
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 prompt = buildScopedImplementPrompt(issue, step, previousResults, testRunner);
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
- appendFileSync6(
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 prompt = buildImplementPrompt(issue, config2, testRunner);
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
- appendFileSync6(
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(join8(cwd, ".git"))) {
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(join8(cwd, e.name, ".git"))).map((e) => e.name);
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(join8(cwd, dir)) ?? dir,
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 = execSync4("git symbolic-ref refs/remotes/origin/HEAD --short", {
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 = execSync4("git remote get-url origin", { cwd: repoPath, encoding: "utf-8" }).trim();
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "Deterministic autonomous issue resolver — structured AI agent loop for Linear/Trello",
5
5
  "license": "MIT",
6
6
  "type": "module",