@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.
- package/README.md +9 -8
- package/dist/index.js +119 -26
- 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
|
@@ -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 \`
|
|
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
|
-
|
|
2629
|
+
const pm = detectPackageManager(cwd);
|
|
2630
|
+
log(`Running test validation (${testRunner} via ${pm})...`);
|
|
2612
2631
|
try {
|
|
2613
|
-
await execa3(
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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?
|
|
3466
|
-
options: availableModels.map((m
|
|
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
|
});
|