@tarcisiopgs/lisa 1.4.0 → 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 +119 -26
  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
@@ -3,6 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { execSync as execSync6 } from "child_process";
5
5
  import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6 } from "fs";
6
+ import { tmpdir as tmpdir6 } from "os";
6
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";
@@ -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();
@@ -1294,9 +1302,10 @@ var CursorProvider = class {
1294
1302
  const promptFile = join5(tmpDir, "prompt.md");
1295
1303
  writeFileSync6(promptFile, prompt, "utf-8");
1296
1304
  try {
1305
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
1297
1306
  const proc = spawn4(
1298
1307
  "sh",
1299
- ["-c", `${bin} -p "$(cat '${promptFile}')" --output-format text --force`],
1308
+ ["-c", `${bin} -p "$(cat '${promptFile}')" --output-format text --force ${modelFlag}`],
1300
1309
  {
1301
1310
  cwd: opts.cwd,
1302
1311
  stdio: ["ignore", "pipe", "pipe"]
@@ -2215,6 +2224,15 @@ function resolveModels(config2) {
2215
2224
  );
2216
2225
  }
2217
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
+ }
2218
2236
  return config2.models.map((m) => ({
2219
2237
  provider: config2.provider,
2220
2238
  model: m === config2.provider ? void 0 : m
@@ -2608,9 +2626,10 @@ function findRepoConfig(config2, issue) {
2608
2626
  async function runTestValidation(cwd) {
2609
2627
  const testRunner = detectTestRunner(cwd);
2610
2628
  if (!testRunner) return true;
2611
- log(`Running test validation (${testRunner} detected)...`);
2629
+ const pm = detectPackageManager(cwd);
2630
+ log(`Running test validation (${testRunner} via ${pm})...`);
2612
2631
  try {
2613
- await execa3("npm", ["run", "test"], { cwd, stdio: "pipe" });
2632
+ await execa3(pm, ["run", "test"], { cwd, stdio: "pipe" });
2614
2633
  ok("Tests passed.");
2615
2634
  return true;
2616
2635
  } catch (err) {
@@ -2678,8 +2697,9 @@ async function runNativeWorktreeSession(config2, issue, logFile, session, models
2678
2697
  }
2679
2698
  const testRunner = detectTestRunner(repoPath);
2680
2699
  if (testRunner) log(`Detected test runner: ${testRunner}`);
2700
+ const pm = detectPackageManager(repoPath);
2681
2701
  cleanupManifest(repoPath);
2682
- const prompt = buildNativeWorktreePrompt(issue, repoPath, testRunner);
2702
+ const prompt = buildNativeWorktreePrompt(issue, repoPath, testRunner, pm);
2683
2703
  startSpinner(`${issue.id} \u2014 implementing (native worktree)...`);
2684
2704
  log(`Implementing with native worktree... (log: ${logFile})`);
2685
2705
  initLogFile(logFile);
@@ -2828,7 +2848,8 @@ async function runManualWorktreeSession(config2, issue, logFile, session, models
2828
2848
  if (testRunner) {
2829
2849
  log(`Detected test runner: ${testRunner}`);
2830
2850
  }
2831
- const prompt = buildImplementPrompt(issue, config2, testRunner);
2851
+ const pm = detectPackageManager(worktreePath);
2852
+ const prompt = buildImplementPrompt(issue, config2, testRunner, pm);
2832
2853
  startSpinner(`${issue.id} \u2014 implementing...`);
2833
2854
  log(`Implementing in worktree... (log: ${logFile})`);
2834
2855
  initLogFile(logFile);
@@ -3044,7 +3065,19 @@ async function runMultiRepoStep(config2, issue, step, previousResults, logFile,
3044
3065
  ok(`Worktree created at ${worktreePath}`);
3045
3066
  const testRunner = detectTestRunner(worktreePath);
3046
3067
  if (testRunner) log(`Detected test runner: ${testRunner}`);
3047
- 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);
3048
3081
  startSpinner(`${issue.id} step ${stepNum} \u2014 implementing...`);
3049
3082
  const result = await runWithFallback(models, prompt, {
3050
3083
  logFile,
@@ -3054,6 +3087,7 @@ async function runMultiRepoStep(config2, issue, step, previousResults, logFile,
3054
3087
  overseer: config2.overseer
3055
3088
  });
3056
3089
  stopSpinner();
3090
+ if (repoConfig?.lifecycle) await stopResources();
3057
3091
  try {
3058
3092
  appendFileSync8(
3059
3093
  logFile,
@@ -3148,7 +3182,8 @@ async function runBranchSession(config2, issue, logFile, session, models) {
3148
3182
  if (testRunner) {
3149
3183
  log(`Detected test runner: ${testRunner}`);
3150
3184
  }
3151
- const prompt = buildImplementPrompt(issue, config2, testRunner);
3185
+ const pm = detectPackageManager(workspace);
3186
+ const prompt = buildImplementPrompt(issue, config2, testRunner, pm);
3152
3187
  const repo = findRepoConfig(config2, issue);
3153
3188
  if (repo?.lifecycle) {
3154
3189
  startSpinner(`${issue.id} \u2014 starting resources...`);
@@ -3408,6 +3443,56 @@ function getVersion() {
3408
3443
  return "0.0.0";
3409
3444
  }
3410
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
+ ];
3411
3496
  var main = defineCommand({
3412
3497
  meta: {
3413
3498
  name: "lisa",
@@ -3426,7 +3511,7 @@ async function runConfigWizard() {
3426
3511
  cursor: "Cursor Agent"
3427
3512
  };
3428
3513
  const providerModels = {
3429
- claude: ["claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5"],
3514
+ claude: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"],
3430
3515
  gemini: ["gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro"]
3431
3516
  };
3432
3517
  const available = await getAvailableProviders();
@@ -3459,14 +3544,22 @@ After installing, run ${pc2.cyan("lisa init")} again.`
3459
3544
  providerName = selected;
3460
3545
  }
3461
3546
  let selectedModels = [];
3462
- const availableModels = providerModels[providerName];
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
+ }
3463
3557
  if (availableModels && availableModels.length > 0) {
3464
3558
  const modelSelection = await clack.multiselect({
3465
- message: "Which models to use? (first = primary, rest = fallbacks in order)",
3466
- options: availableModels.map((m, i) => ({
3559
+ message: "Which models to use? Select in order: primary first, then fallbacks",
3560
+ options: availableModels.map((m) => ({
3467
3561
  value: m,
3468
- label: m,
3469
- hint: i === 0 ? "primary" : `fallback ${i}`
3562
+ label: m
3470
3563
  })),
3471
3564
  required: false
3472
3565
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.4.0",
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",