@tarcisiopgs/lisa 0.9.3 → 0.9.5

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 (2) hide show
  1. package/dist/index.js +260 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { execSync as execSync4 } from "child_process";
5
- import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync6 } from "fs";
5
+ import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6 } from "fs";
6
6
  import { join as join8, resolve as resolvePath } from "path";
7
7
  import * as clack from "@clack/prompts";
8
8
  import { defineCommand, runMain } from "citty";
@@ -331,7 +331,7 @@ function banner() {
331
331
  }
332
332
 
333
333
  // src/loop.ts
334
- import { appendFileSync as appendFileSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync4 } from "fs";
334
+ import { appendFileSync as appendFileSync6, existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync4 } from "fs";
335
335
  import { join as join7, resolve as resolve5 } from "path";
336
336
  import { execa as execa3 } from "execa";
337
337
 
@@ -554,6 +554,96 @@ Do NOT update README.md for:
554
554
  If an update is needed, keep the existing README style and structure. Include the README change in the same commit as the implementation.
555
555
  `;
556
556
  }
557
+ function buildWorktreeMultiRepoPrompt(issue, config2) {
558
+ const workspace = resolve3(config2.workspace);
559
+ const repoBlock = config2.repos.map((r) => {
560
+ const absPath = resolve3(workspace, r.path);
561
+ return [
562
+ `- **${r.name}**: \`${absPath}\``,
563
+ ` - Base branch: \`${r.base_branch}\``,
564
+ ` - Worktrees dir: \`${join(absPath, ".worktrees")}\``
565
+ ].join("\n");
566
+ }).join("\n\n");
567
+ const readmeBlock = buildReadmeInstructions();
568
+ const manifestPath = join(workspace, ".lisa-manifest.json");
569
+ return `You are an autonomous implementation agent working in a multi-repository workspace.
570
+ Your job is to determine the correct repository, create an English-named branch, implement the issue, commit, and write a manifest file.
571
+
572
+ You are in the workspace: \`${workspace}\`
573
+
574
+ ## Issue
575
+
576
+ - **ID:** ${issue.id}
577
+ - **Title:** ${issue.title}
578
+ - **URL:** ${issue.url}
579
+
580
+ ### Description
581
+
582
+ ${issue.description}
583
+
584
+ ## Available Repositories
585
+
586
+ ${repoBlock}
587
+
588
+ ## Instructions
589
+
590
+ 1. **Identify the correct repository**: Read the issue title and description carefully.
591
+ Determine which single repository above is the right target. Consider:
592
+ - File paths or module names mentioned in the description
593
+ - Technologies and frameworks referenced
594
+ - The nature of the change (e.g., API endpoint \u2192 api repo, UI component \u2192 frontend repo)
595
+
596
+ 2. **Choose an English branch name**: Create a slug in English following:
597
+ \`feat/${issue.id.toLowerCase()}-short-english-description\`
598
+ The description part MUST be in English regardless of the issue title language.
599
+ Example: for "${issue.id} Implementar rate limiting na API" \u2192 \`feat/${issue.id.toLowerCase()}-add-rate-limiting-to-api\`
600
+
601
+ 3. **Set up the worktree**: In the chosen repo, run:
602
+ \`\`\`
603
+ git fetch origin <base_branch>
604
+ git worktree add -b <your-english-branch> <repoPath>/.worktrees/<your-english-branch> origin/<base_branch>
605
+ cd <repoPath>/.worktrees/<your-english-branch>
606
+ \`\`\`
607
+
608
+ 4. **Implement**: Work inside the worktree. Follow the issue description exactly:
609
+ - Read all relevant files listed in the description first (if present)
610
+ - Follow the implementation instructions exactly
611
+ - Verify each acceptance criteria (if present)
612
+ - Respect any stack or technical constraints (if present)
613
+ ${readmeBlock}
614
+ 5. **Validate**: Run the project's linter/typecheck/tests if available:
615
+ - Check \`package.json\` for lint, typecheck, check, or test scripts.
616
+ - Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`, \`npm run test\`).
617
+ - Fix any errors before proceeding.
618
+
619
+ 6. **Commit (do NOT push)**: Make atomic commits with conventional commit messages.
620
+ Do NOT run \`git push\` \u2014 the caller handles pushing.
621
+ **IMPORTANT \u2014 Language rules:**
622
+ - All commit messages MUST be in English.
623
+ - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
624
+
625
+ 7. **Write the manifest**: After committing, create \`${manifestPath}\` with JSON:
626
+ \`\`\`json
627
+ {
628
+ "repoPath": "<absolute path to the chosen repo>",
629
+ "branch": "<your English branch name>",
630
+ "prTitle": "<PR title in English, conventional commit format>"
631
+ }
632
+ \`\`\`
633
+ Do NOT commit this file.
634
+
635
+ ## Rules
636
+
637
+ - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
638
+ - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
639
+ - Do NOT push \u2014 the caller handles that.
640
+ - Do NOT create pull requests \u2014 the caller handles that.
641
+ - Do NOT update the issue tracker \u2014 the caller handles that.
642
+ - Do NOT install new dependencies unless the issue explicitly requires it.
643
+ - If you get stuck or the issue is unclear, STOP and explain why.
644
+ - One issue only. Do not pick up additional issues.
645
+ - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
646
+ }
557
647
  function buildWorktreePrompt(issue, testRunner) {
558
648
  const testBlock = buildTestInstructions(testRunner ?? null);
559
649
  const readmeBlock = buildReadmeInstructions();
@@ -586,21 +676,26 @@ ${testBlock}${readmeBlock}
586
676
  - Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`).
587
677
  - Fix any errors before proceeding.
588
678
 
589
- 3. **Commit & Push**: Make atomic commits with conventional commit messages.
590
- Push the branch to origin.
679
+ 3. **Commit**: Make atomic commits with conventional commit messages.
680
+ **Branch name must be in English.** The branch was pre-created with an auto-generated name.
681
+ If that name contains non-English words, rename it before committing:
682
+ \`git branch -m <current-name> feat/${issue.id.toLowerCase()}-short-english-slug\`
683
+ Do NOT push \u2014 the caller handles pushing.
591
684
  **IMPORTANT \u2014 Language rules:**
592
685
  - All commit messages MUST be in English.
593
686
  - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
594
687
 
595
- 4. **PR Metadata**: Before finishing, create a file named \`.pr-title\` at the repository root
596
- containing a single line with the PR title in **English** using conventional commit format
597
- (e.g., \`feat: add user authentication\`, \`fix: resolve null pointer in login flow\`).
598
- This file is used by the caller to create the pull request. Do NOT commit this file.
688
+ 4. **Write manifest**: Create \`.lisa-manifest.json\` in the **current directory** with JSON:
689
+ \`\`\`json
690
+ {"branch": "<final English branch name>", "prTitle": "<English PR title, conventional commit format>"}
691
+ \`\`\`
692
+ Do NOT commit this file.
599
693
 
600
694
  ## Rules
601
695
 
602
- - **ALL git commits, PR titles, and PR descriptions MUST be in English.**
696
+ - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
603
697
  - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
698
+ - Do NOT push \u2014 the caller handles that.
604
699
  - Do NOT install new dependencies unless the issue explicitly requires it.
605
700
  - If you get stuck or the issue is unclear, STOP and explain why.
606
701
  - One issue only. Do not pick up additional issues.
@@ -616,6 +711,7 @@ function buildBranchPrompt(issue, config2, testRunner) {
616
711
  const baseBranchInstruction = config2.repos.length > 0 ? "From the repo's base branch (listed above)" : `From \`${config2.base_branch}\``;
617
712
  const testBlock = buildTestInstructions(testRunner ?? null);
618
713
  const readmeBlock = buildReadmeInstructions();
714
+ const manifestPath = join(workspace, ".lisa-manifest.json");
619
715
  return `You are an autonomous implementation agent. Your job is to implement a single
620
716
  issue, validate it, commit, and push the branch.
621
717
 
@@ -635,8 +731,10 @@ ${issue.description}
635
731
  ${repoEntries}
636
732
  - If it references multiple repos, pick the PRIMARY one (the one with the most files listed).
637
733
 
638
- 2. **Create a branch**: ${baseBranchInstruction}, create a branch named after the issue
639
- (e.g., \`feat/${issue.id.toLowerCase()}-short-description\`).
734
+ 2. **Create a branch**: ${baseBranchInstruction}, create a branch with an **English** slug:
735
+ \`feat/${issue.id.toLowerCase()}-short-english-description\`
736
+ The description MUST be in English \u2014 translate or summarize the issue title if it's in another language.
737
+ Example: "Implementar rate limiting na API" \u2192 \`feat/${issue.id.toLowerCase()}-add-rate-limiting-to-api\`
640
738
 
641
739
  3. **Implement**: Follow the issue description exactly:
642
740
  - Read all relevant files listed in the description first (if present)
@@ -655,10 +753,11 @@ ${testBlock}${readmeBlock}
655
753
  - All commit messages MUST be in English.
656
754
  - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
657
755
 
658
- 6. **PR Metadata**: Before finishing, create a file named \`.pr-title\` at the repository root
659
- containing a single line with the PR title in **English** using conventional commit format
660
- (e.g., \`feat: add user authentication\`, \`fix: resolve null pointer in login flow\`).
661
- This file is used by the caller to create the pull request. Do NOT commit this file.
756
+ 6. **Write manifest**: Before finishing, create \`${manifestPath}\` with JSON:
757
+ \`\`\`json
758
+ {"repoPath": "<absolute path to this repo>", "branch": "<branch name>", "prTitle": "<English PR title, conventional commit format>"}
759
+ \`\`\`
760
+ Do NOT commit this file.
662
761
 
663
762
  ## Rules
664
763
 
@@ -1677,6 +1776,22 @@ function cleanupPrTitle(cwd) {
1677
1776
  } catch {
1678
1777
  }
1679
1778
  }
1779
+ var MANIFEST_FILE = ".lisa-manifest.json";
1780
+ function readLisaManifest(dir) {
1781
+ const manifestPath = join7(dir, MANIFEST_FILE);
1782
+ if (!existsSync6(manifestPath)) return null;
1783
+ try {
1784
+ return JSON.parse(readFileSync5(manifestPath, "utf-8").trim());
1785
+ } catch {
1786
+ return null;
1787
+ }
1788
+ }
1789
+ function cleanupManifest(dir) {
1790
+ try {
1791
+ unlinkSync4(join7(dir, MANIFEST_FILE));
1792
+ } catch {
1793
+ }
1794
+ }
1680
1795
  function installSignalHandlers() {
1681
1796
  const cleanup = async (signal) => {
1682
1797
  if (shuttingDown) {
@@ -1942,6 +2057,9 @@ async function runTestValidation(cwd) {
1942
2057
  }
1943
2058
  }
1944
2059
  async function runWorktreeSession(config2, issue, logFile, session, models) {
2060
+ if (config2.repos.length > 1) {
2061
+ return runWorktreeMultiRepoSession(config2, issue, logFile, session, models);
2062
+ }
1945
2063
  const workspace = resolve5(config2.workspace);
1946
2064
  const repoPath = determineRepoPath(config2.repos, issue, workspace) ?? workspace;
1947
2065
  const defaultBranch = resolveBaseBranch(config2, repoPath);
@@ -2026,17 +2144,33 @@ ${result.output}
2026
2144
  await cleanupWorktree(repoPath, worktreePath);
2027
2145
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2028
2146
  }
2147
+ const manifest = readLisaManifest(worktreePath);
2148
+ let effectiveBranch = branchName;
2149
+ if (manifest?.branch && manifest.branch !== branchName) {
2150
+ log(`Renaming branch to English name: ${manifest.branch}`);
2151
+ try {
2152
+ await execa3("git", ["branch", "-m", branchName, manifest.branch], { cwd: worktreePath });
2153
+ effectiveBranch = manifest.branch;
2154
+ ok(`Branch renamed to ${effectiveBranch}`);
2155
+ } catch (err) {
2156
+ warn(
2157
+ `Branch rename failed, using original: ${err instanceof Error ? err.message : String(err)}`
2158
+ );
2159
+ }
2160
+ }
2029
2161
  try {
2030
- await execa3("git", ["push", "-u", "origin", branchName], { cwd: worktreePath });
2162
+ await execa3("git", ["push", "-u", "origin", effectiveBranch], { cwd: worktreePath });
2031
2163
  } catch (err) {
2032
2164
  error(
2033
2165
  `Failed to push branch to remote: ${err instanceof Error ? err.message : String(err)}`
2034
2166
  );
2167
+ cleanupManifest(worktreePath);
2035
2168
  await cleanupWorktree(repoPath, worktreePath);
2036
2169
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2037
2170
  }
2038
- const prTitle = readPrTitle(worktreePath) ?? issue.title;
2171
+ const prTitle = manifest?.prTitle ?? readPrTitle(worktreePath) ?? issue.title;
2039
2172
  cleanupPrTitle(worktreePath);
2173
+ cleanupManifest(worktreePath);
2040
2174
  const prUrls = [];
2041
2175
  try {
2042
2176
  const repoInfo = await getRepoInfo(worktreePath);
@@ -2044,7 +2178,7 @@ ${result.output}
2044
2178
  {
2045
2179
  owner: repoInfo.owner,
2046
2180
  repo: repoInfo.repo,
2047
- head: branchName,
2181
+ head: effectiveBranch,
2048
2182
  base: defaultBranch,
2049
2183
  title: prTitle,
2050
2184
  body: buildPrBody(issue, result.providerUsed)
@@ -2060,8 +2194,98 @@ ${result.output}
2060
2194
  ok(`Session ${session} complete for ${issue.id}`);
2061
2195
  return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
2062
2196
  }
2197
+ async function runWorktreeMultiRepoSession(config2, issue, logFile, session, models) {
2198
+ const workspace = resolve5(config2.workspace);
2199
+ cleanupManifest(workspace);
2200
+ const prompt = buildWorktreeMultiRepoPrompt(issue, config2);
2201
+ log(`Multi-repo worktree session for ${issue.id} (agent selects repo and branch name)`);
2202
+ log(`Implementing (agent selects repo)... (log: ${logFile})`);
2203
+ initLogFile(logFile);
2204
+ const result = await runWithFallback(models, prompt, {
2205
+ logFile,
2206
+ cwd: workspace,
2207
+ guardrailsDir: workspace,
2208
+ issueId: issue.id,
2209
+ overseer: config2.overseer
2210
+ });
2211
+ try {
2212
+ appendFileSync6(
2213
+ logFile,
2214
+ `
2215
+ ${"=".repeat(80)}
2216
+ Provider used: ${result.providerUsed}
2217
+ Full output:
2218
+ ${result.output}
2219
+ `
2220
+ );
2221
+ } catch {
2222
+ }
2223
+ if (!result.success) {
2224
+ error(`Session ${session} failed for ${issue.id}. Check ${logFile}`);
2225
+ cleanupManifest(workspace);
2226
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2227
+ }
2228
+ const manifest = readLisaManifest(workspace);
2229
+ if (!manifest?.repoPath || !manifest.branch) {
2230
+ error(
2231
+ `Agent did not produce a valid .lisa-manifest.json (requires repoPath + branch) for ${issue.id}. Aborting.`
2232
+ );
2233
+ cleanupManifest(workspace);
2234
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2235
+ }
2236
+ ok(`Provider chose repo: ${manifest.repoPath}, branch: ${manifest.branch}`);
2237
+ const worktreePath = join7(manifest.repoPath, ".worktrees", manifest.branch);
2238
+ const baseBranch = resolveBaseBranch(config2, manifest.repoPath);
2239
+ const hasWorktree = existsSync6(worktreePath);
2240
+ const effectiveCwd = hasWorktree ? worktreePath : manifest.repoPath;
2241
+ if (!hasWorktree) {
2242
+ warn(`Worktree not found at ${worktreePath} \u2014 using repo root for git operations`);
2243
+ }
2244
+ const testsPassed = await runTestValidation(effectiveCwd);
2245
+ if (!testsPassed) {
2246
+ error(`Tests failed for ${issue.id}. Blocking PR creation.`);
2247
+ if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2248
+ cleanupManifest(workspace);
2249
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2250
+ }
2251
+ try {
2252
+ await execa3("git", ["push", "-u", "origin", manifest.branch], { cwd: effectiveCwd });
2253
+ } catch (err) {
2254
+ error(
2255
+ `Failed to push branch to remote: ${err instanceof Error ? err.message : String(err)}`
2256
+ );
2257
+ if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2258
+ cleanupManifest(workspace);
2259
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2260
+ }
2261
+ const prTitle = manifest.prTitle ?? issue.title;
2262
+ const prUrls = [];
2263
+ try {
2264
+ const repoInfo = await getRepoInfo(effectiveCwd);
2265
+ const pr = await createPullRequest(
2266
+ {
2267
+ owner: repoInfo.owner,
2268
+ repo: repoInfo.repo,
2269
+ head: manifest.branch,
2270
+ base: baseBranch,
2271
+ title: prTitle,
2272
+ body: buildPrBody(issue, result.providerUsed)
2273
+ },
2274
+ config2.github
2275
+ );
2276
+ ok(`PR created: ${pr.html_url}`);
2277
+ prUrls.push(pr.html_url);
2278
+ } catch (err) {
2279
+ error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
2280
+ }
2281
+ cleanupManifest(workspace);
2282
+ if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2283
+ ok(`Session ${session} complete for ${issue.id}`);
2284
+ return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
2285
+ }
2063
2286
  async function runBranchSession(config2, issue, logFile, session, models) {
2064
2287
  const workspace = resolve5(config2.workspace);
2288
+ cleanupManifest(workspace);
2065
2289
  const testRunner = detectTestRunner(workspace);
2066
2290
  if (testRunner) {
2067
2291
  log(`Detected test runner: ${testRunner}`);
@@ -2118,20 +2342,27 @@ ${result.output}
2118
2342
  const testsPassed = await runTestValidation(workspace);
2119
2343
  if (!testsPassed) {
2120
2344
  error(`Tests failed for ${issue.id}. Blocking PR creation.`);
2345
+ cleanupManifest(workspace);
2121
2346
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2122
2347
  }
2123
- const detected = await detectFeatureBranches(
2124
- config2.repos,
2125
- issue.id,
2126
- workspace,
2127
- config2.base_branch
2128
- );
2348
+ const manifest = readLisaManifest(workspace);
2349
+ let detected;
2350
+ if (manifest?.repoPath && manifest.branch) {
2351
+ ok(`Using manifest: repo=${manifest.repoPath}, branch=${manifest.branch}`);
2352
+ detected = [{ repoPath: manifest.repoPath, branch: manifest.branch }];
2353
+ } else {
2354
+ if (manifest) {
2355
+ warn(`Manifest found but missing repoPath or branch \u2014 falling back to detection`);
2356
+ }
2357
+ detected = await detectFeatureBranches(config2.repos, issue.id, workspace, config2.base_branch);
2358
+ }
2359
+ cleanupManifest(workspace);
2129
2360
  if (detected.length === 0) {
2130
2361
  error(`Could not detect feature branch for ${issue.id} \u2014 skipping PR creation`);
2131
2362
  ok(`Session ${session} complete for ${issue.id}`);
2132
2363
  return { success: true, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2133
2364
  }
2134
- const prTitle = readPrTitle(workspace) ?? issue.title;
2365
+ const prTitle = manifest?.prTitle ?? readPrTitle(workspace) ?? issue.title;
2135
2366
  cleanupPrTitle(workspace);
2136
2367
  const prUrls = [];
2137
2368
  for (const { repoPath, branch } of detected) {
@@ -2290,8 +2521,8 @@ var status = defineCommand({
2290
2521
  console.log(` In progress: ${pc2.bold(config2.source_config.in_progress)}`);
2291
2522
  console.log(` Done: ${pc2.bold(config2.source_config.done)}`);
2292
2523
  console.log(` Logs: ${pc2.dim(config2.logs.dir)}`);
2293
- const { readdirSync: readdirSync2, existsSync: existsSync7 } = await import("fs");
2294
- if (existsSync7(config2.logs.dir)) {
2524
+ const { readdirSync: readdirSync2, existsSync: existsSync8 } = await import("fs");
2525
+ if (existsSync8(config2.logs.dir)) {
2295
2526
  const logs = readdirSync2(config2.logs.dir).filter((f) => f.endsWith(".log"));
2296
2527
  console.log(`
2297
2528
  ${pc2.cyan("Sessions:")} ${logs.length} log file(s) found`);
@@ -2526,12 +2757,12 @@ async function detectGitHubMethod() {
2526
2757
  }
2527
2758
  async function detectGitRepos() {
2528
2759
  const cwd = process.cwd();
2529
- if (existsSync6(join8(cwd, ".git"))) {
2760
+ if (existsSync7(join8(cwd, ".git"))) {
2530
2761
  clack.log.info(`Detected git repository in current directory.`);
2531
2762
  return [];
2532
2763
  }
2533
2764
  const entries = readdirSync(cwd, { withFileTypes: true });
2534
- const gitDirs = entries.filter((e) => e.isDirectory() && existsSync6(join8(cwd, e.name, ".git"))).map((e) => e.name);
2765
+ const gitDirs = entries.filter((e) => e.isDirectory() && existsSync7(join8(cwd, e.name, ".git"))).map((e) => e.name);
2535
2766
  if (gitDirs.length === 0) {
2536
2767
  return [];
2537
2768
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Deterministic autonomous issue resolver — structured AI agent loop for Linear/Trello",
5
5
  "license": "MIT",
6
6
  "type": "module",