@tarcisiopgs/lisa 1.2.1 → 1.3.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 (2) hide show
  1. package/dist/index.js +563 -153
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -498,6 +498,21 @@ function registerCleanup() {
498
498
  });
499
499
  }
500
500
 
501
+ // src/pr-body.ts
502
+ function sanitizePrBody(raw) {
503
+ let text2 = raw.trim();
504
+ if (!text2) return "";
505
+ text2 = text2.replace(/<[^>]*>/g, "");
506
+ text2 = text2.replace(/^(\s*)\* /gm, "$1- ");
507
+ if (!text2.includes("\n")) {
508
+ const sentences = text2.match(/[^.!?]+[.!?]+/g);
509
+ if (sentences && sentences.length > 1) {
510
+ text2 = sentences.map((s) => `- ${s.trim()}`).join("\n");
511
+ }
512
+ }
513
+ return text2.trim();
514
+ }
515
+
501
516
  // src/prompt.ts
502
517
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
503
518
  import { join, resolve as resolve3 } from "path";
@@ -564,98 +579,17 @@ Do NOT update README.md for:
564
579
  If an update is needed, keep the existing README style and structure. Include the README change in the same commit as the implementation.
565
580
  `;
566
581
  }
567
- function buildWorktreeMultiRepoPrompt(issue, config2) {
568
- const workspace = resolve3(config2.workspace);
569
- const repoBlock = config2.repos.map((r) => {
570
- const absPath = resolve3(workspace, r.path);
571
- return [
572
- `- **${r.name}**: \`${absPath}\``,
573
- ` - Base branch: \`${r.base_branch}\``,
574
- ` - Worktrees dir: \`${join(absPath, ".worktrees")}\``
575
- ].join("\n");
576
- }).join("\n\n");
577
- const readmeBlock = buildReadmeInstructions();
578
- const hookBlock = buildPreCommitHookInstructions();
579
- const manifestPath = join(workspace, ".lisa-manifest.json");
580
- return `You are an autonomous implementation agent working in a multi-repository workspace.
581
- Your job is to determine the correct repository, create an English-named branch, implement the issue, commit, and write a manifest file.
582
-
583
- You are in the workspace: \`${workspace}\`
584
-
585
- ## Issue
586
-
587
- - **ID:** ${issue.id}
588
- - **Title:** ${issue.title}
589
- - **URL:** ${issue.url}
590
-
591
- ### Description
592
-
593
- ${issue.description}
594
-
595
- ## Available Repositories
596
-
597
- ${repoBlock}
598
-
599
- ## Instructions
600
-
601
- 1. **Identify the correct repository**: Read the issue title and description carefully.
602
- Determine which single repository above is the right target. Consider:
603
- - File paths or module names mentioned in the description
604
- - Technologies and frameworks referenced
605
- - The nature of the change (e.g., API endpoint \u2192 api repo, UI component \u2192 frontend repo)
606
-
607
- 2. **Choose an English branch name**: Create a slug in English following:
608
- \`feat/${issue.id.toLowerCase()}-short-english-description\`
609
- The description part MUST be in English regardless of the issue title language.
610
- Example: for "${issue.id} Implementar rate limiting na API" \u2192 \`feat/${issue.id.toLowerCase()}-add-rate-limiting-to-api\`
611
-
612
- 3. **Set up the worktree**: In the chosen repo, run:
613
- \`\`\`
614
- git fetch origin <base_branch>
615
- git worktree add -b <your-english-branch> <repoPath>/.worktrees/<your-english-branch> origin/<base_branch>
616
- cd <repoPath>/.worktrees/<your-english-branch>
582
+ function buildPrBodyInstructions() {
583
+ return `The \`prBody\` MUST follow this exact markdown structure:
617
584
  \`\`\`
618
-
619
- 4. **Implement**: Work inside the worktree. Follow the issue description exactly:
620
- - Read all relevant files listed in the description first (if present)
621
- - Follow the implementation instructions exactly
622
- - Verify each acceptance criteria (if present)
623
- - Respect any stack or technical constraints (if present)
624
- ${readmeBlock}${hookBlock}
625
- 5. **Validate**: Run the project's linter/typecheck/tests if available:
626
- - Check \`package.json\` for lint, typecheck, check, or test scripts.
627
- - Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`, \`npm run test\`).
628
- - Fix any errors before proceeding.
629
-
630
- 6. **Commit (do NOT push)**: Make atomic commits with conventional commit messages.
631
- Do NOT run \`git push\` \u2014 the caller handles pushing.
632
- **IMPORTANT \u2014 Language rules:**
633
- - All commit messages MUST be in English.
634
- - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
635
-
636
- 7. **Write the manifest**: After committing, create \`${manifestPath}\` with JSON:
637
- \`\`\`json
638
- {
639
- "repoPath": "<absolute path to the chosen repo>",
640
- "branch": "<your English branch name>",
641
- "prTitle": "<PR title in English, conventional commit format>",
642
- "prBody": "<markdown-formatted English summary>"
643
- }
585
+ - **What**: one-line summary of the change
586
+ - **Why**: motivation or issue context
587
+ - **Key changes**:
588
+ - \`src/foo.ts\` \u2014 added X functionality
589
+ - \`src/bar.ts\` \u2014 refactored Y to support Z
590
+ - **Testing**: what was validated (e.g. "all unit tests pass", "manually tested endpoint")
644
591
  \`\`\`
645
- The \`prBody\` MUST use markdown formatting. Use bullet points (\`-\`) to list key changes, and optionally bold (\`**text**\`) for emphasis. Do NOT write a wall of text \u2014 structure the summary as a bulleted list. Describe WHAT was changed and WHY, mentioning key files modified, new behavior added, or bugs fixed. Write in English.
646
- Do NOT commit this file.
647
-
648
- ## Rules
649
-
650
- - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
651
- - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
652
- - Do NOT push \u2014 the caller handles that.
653
- - Do NOT create pull requests \u2014 the caller handles that.
654
- - Do NOT update the issue tracker \u2014 the caller handles that.
655
- - Do NOT install new dependencies unless the issue explicitly requires it.
656
- - If you get stuck or the issue is unclear, STOP and explain why.
657
- - One issue only. Do not pick up additional issues.
658
- - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
592
+ Write in English. Do NOT write a wall of text \u2014 structure the summary using the template above.`;
659
593
  }
660
594
  function buildWorktreePrompt(issue, testRunner) {
661
595
  const testBlock = buildTestInstructions(testRunner ?? null);
@@ -703,7 +637,7 @@ ${testBlock}${readmeBlock}${hookBlock}
703
637
  \`\`\`json
704
638
  {"branch": "<final English branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<markdown-formatted English summary>"}
705
639
  \`\`\`
706
- The \`prBody\` MUST use markdown formatting. Use bullet points (\`-\`) to list key changes, and optionally bold (\`**text**\`) for emphasis. Do NOT write a wall of text \u2014 structure the summary as a bulleted list. Describe WHAT was changed and WHY, mentioning key files modified, new behavior added, or bugs fixed. Write in English.
640
+ ${buildPrBodyInstructions()}
707
641
  Do NOT commit this file.
708
642
 
709
643
  ## Rules
@@ -773,7 +707,7 @@ ${testBlock}${readmeBlock}${hookBlock}
773
707
  \`\`\`json
774
708
  {"repoPath": "<absolute path to this repo>", "branch": "<branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<markdown-formatted English summary>"}
775
709
  \`\`\`
776
- The \`prBody\` MUST use markdown formatting. Use bullet points (\`-\`) to list key changes, and optionally bold (\`**text**\`) for emphasis. Do NOT write a wall of text \u2014 structure the summary as a bulleted list. Describe WHAT was changed and WHY, mentioning key files modified, new behavior added, or bugs fixed. Write in English.
710
+ ${buildPrBodyInstructions()}
777
711
  Do NOT commit this file.
778
712
 
779
713
  ## Rules
@@ -814,6 +748,198 @@ ${hookErrors}
814
748
 
815
749
  Focus only on fixing the hook errors. Do not make unrelated changes.`;
816
750
  }
751
+ function buildNativeWorktreePrompt(issue, repoPath, testRunner) {
752
+ const testBlock = buildTestInstructions(testRunner ?? null);
753
+ const readmeBlock = buildReadmeInstructions();
754
+ const hookBlock = buildPreCommitHookInstructions();
755
+ const prBodyBlock = buildPrBodyInstructions();
756
+ const manifestLocation = repoPath ? `\`${join(repoPath, ".lisa-manifest.json")}\`` : "`.lisa-manifest.json` in the **current directory**";
757
+ return `You are an autonomous implementation agent. Your job is to implement a single
758
+ issue, validate it, and commit.
759
+
760
+ You are working inside a git worktree that was automatically created for this task.
761
+ Work on the current branch \u2014 it was created for you.
762
+
763
+ ## Issue
764
+
765
+ - **ID:** ${issue.id}
766
+ - **Title:** ${issue.title}
767
+ - **URL:** ${issue.url}
768
+
769
+ ### Description
770
+
771
+ ${issue.description}
772
+
773
+ ## Instructions
774
+
775
+ 1. **Implement**: Follow the issue description exactly:
776
+ - Read all relevant files listed in the description first (if present)
777
+ - Follow the implementation instructions exactly
778
+ - Verify each acceptance criteria (if present)
779
+ - Respect any stack or technical constraints (if present)
780
+ ${testBlock}${readmeBlock}${hookBlock}
781
+ 2. **Validate**: Run the project's linter/typecheck/tests if available:
782
+ - Check \`package.json\` (or equivalent) for lint, typecheck, check, or test scripts.
783
+ - Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`).
784
+ - Fix any errors before proceeding.
785
+
786
+ 3. **Commit**: Make atomic commits with conventional commit messages.
787
+ **Branch name must be in English.** If the current branch name contains non-English words,
788
+ rename it: \`git branch -m <current-name> feat/${issue.id.toLowerCase()}-short-english-slug\`
789
+ Do NOT push \u2014 the caller handles pushing.
790
+ **IMPORTANT \u2014 Language rules:**
791
+ - All commit messages MUST be in English.
792
+ - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
793
+
794
+ 4. **Write manifest**: Create ${manifestLocation} with JSON:
795
+ \`\`\`json
796
+ {"branch": "<final English branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<markdown-formatted English summary>"}
797
+ \`\`\`
798
+ ${prBodyBlock}
799
+ Do NOT commit this file.
800
+
801
+ ## Rules
802
+
803
+ - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
804
+ - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
805
+ - Do NOT push \u2014 the caller handles that.
806
+ - Do NOT install new dependencies unless the issue explicitly requires it.
807
+ - If you get stuck or the issue is unclear, STOP and explain why.
808
+ - One issue only. Do not pick up additional issues.
809
+ - If the repo has a CLAUDE.md, read it first and follow its conventions.
810
+ - Do NOT create pull requests \u2014 the caller handles that.
811
+ - Do NOT update the issue tracker \u2014 the caller handles that.`;
812
+ }
813
+ function buildPlanningPrompt(issue, config2) {
814
+ const workspace = resolve3(config2.workspace);
815
+ const repoBlock = config2.repos.map((r) => {
816
+ const absPath = resolve3(workspace, r.path);
817
+ return `- **${r.name}**: \`${absPath}\` (base branch: \`${r.base_branch}\`)`;
818
+ }).join("\n");
819
+ const planPath = join(workspace, ".lisa-plan.json");
820
+ return `You are an issue analysis agent. Your job is to read the issue below, determine which repositories are affected, and produce an execution plan.
821
+
822
+ **Do NOT implement anything.** Only analyze the issue and produce the plan file.
823
+
824
+ ## Issue
825
+
826
+ - **ID:** ${issue.id}
827
+ - **Title:** ${issue.title}
828
+ - **URL:** ${issue.url}
829
+
830
+ ### Description
831
+
832
+ ${issue.description}
833
+
834
+ ## Available Repositories
835
+
836
+ ${repoBlock}
837
+
838
+ ## Instructions
839
+
840
+ 1. **Analyze the issue**: Read the title and description carefully. Determine which repositories above are affected by this change.
841
+ Consider:
842
+ - File paths or module names mentioned in the description
843
+ - Technologies and frameworks referenced
844
+ - Dependencies between repos (e.g., backend API changes needed before frontend can consume them)
845
+
846
+ 2. **Determine execution order**: If multiple repos are affected, decide the order. Repos that produce APIs, schemas, or shared libraries should come first. Repos that consume them should come later.
847
+
848
+ 3. **Write the plan**: Create \`${planPath}\` with JSON:
849
+ \`\`\`json
850
+ {
851
+ "steps": [
852
+ { "repoPath": "<absolute path to repo>", "scope": "<what to implement in this repo>", "order": 1 },
853
+ { "repoPath": "<absolute path to repo>", "scope": "<what to implement in this repo>", "order": 2 }
854
+ ]
855
+ }
856
+ \`\`\`
857
+
858
+ ## Rules
859
+
860
+ - Only include repos that are actually affected by the issue. Do NOT include repos that don't need changes.
861
+ - The \`scope\` field should be a concise English description of what needs to be done in that specific repo.
862
+ - Order matters: lower order numbers execute first.
863
+ - Do NOT implement anything. Do NOT create branches, write code, or commit.
864
+ - Do NOT push, create pull requests, or update the issue tracker.
865
+ - If only one repo is affected, the plan should have a single step.`;
866
+ }
867
+ function buildScopedImplementPrompt(issue, step, previousResults, testRunner) {
868
+ const testBlock = buildTestInstructions(testRunner ?? null);
869
+ const readmeBlock = buildReadmeInstructions();
870
+ const hookBlock = buildPreCommitHookInstructions();
871
+ const prBodyBlock = buildPrBodyInstructions();
872
+ const previousBlock = previousResults.length > 0 ? `
873
+ ## Previous Steps
874
+
875
+ The following repos have already been implemented as part of this issue:
876
+
877
+ ${previousResults.map((r) => `- **${r.repoPath}**: branch \`${r.branch}\`${r.prUrl ? ` \u2014 PR: ${r.prUrl}` : ""}`).join("\n")}
878
+
879
+ Use this context if the current step depends on changes from previous steps.
880
+ ` : "";
881
+ return `You are an autonomous implementation agent. Your job is to implement a specific part of an issue in a single repository.
882
+
883
+ You are working inside a git worktree that was automatically created for this task.
884
+ Work on the current branch \u2014 it was created for you.
885
+
886
+ ## Issue
887
+
888
+ - **ID:** ${issue.id}
889
+ - **Title:** ${issue.title}
890
+ - **URL:** ${issue.url}
891
+
892
+ ### Description
893
+
894
+ ${issue.description}
895
+
896
+ ## Your Scope
897
+
898
+ You are responsible for **this specific part** of the issue:
899
+
900
+ > ${step.scope}
901
+
902
+ Focus only on this scope. Do NOT implement changes outside this scope.
903
+ ${previousBlock}
904
+ ## Instructions
905
+
906
+ 1. **Implement**: Follow the scope above. Read the full issue description for context, but only implement what is described in "Your Scope":
907
+ - Read all relevant files first
908
+ - Follow the implementation instructions exactly
909
+ - Verify each acceptance criteria relevant to your scope
910
+ ${testBlock}${readmeBlock}${hookBlock}
911
+ 2. **Validate**: Run the project's linter/typecheck/tests if available:
912
+ - Check \`package.json\` (or equivalent) for lint, typecheck, check, or test scripts.
913
+ - Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`).
914
+ - Fix any errors before proceeding.
915
+
916
+ 3. **Commit**: Make atomic commits with conventional commit messages.
917
+ **Branch name must be in English.** If the current branch name contains non-English words,
918
+ rename it: \`git branch -m <current-name> feat/${issue.id.toLowerCase()}-short-english-slug\`
919
+ Do NOT push \u2014 the caller handles pushing.
920
+ **IMPORTANT \u2014 Language rules:**
921
+ - All commit messages MUST be in English.
922
+ - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
923
+
924
+ 4. **Write manifest**: Create \`.lisa-manifest.json\` in the **current directory** with JSON:
925
+ \`\`\`json
926
+ {"branch": "<final English branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<markdown-formatted English summary>"}
927
+ \`\`\`
928
+ ${prBodyBlock}
929
+ Do NOT commit this file.
930
+
931
+ ## Rules
932
+
933
+ - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
934
+ - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
935
+ - Do NOT push \u2014 the caller handles that.
936
+ - Do NOT install new dependencies unless the issue explicitly requires it.
937
+ - If you get stuck or the issue is unclear, STOP and explain why.
938
+ - One scope only. Do not pick up additional work outside your scope.
939
+ - If the repo has a CLAUDE.md, read it first and follow its conventions.
940
+ - Do NOT create pull requests \u2014 the caller handles that.
941
+ - Do NOT update the issue tracker \u2014 the caller handles that.`;
942
+ }
817
943
 
818
944
  // src/guardrails.ts
819
945
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
@@ -986,6 +1112,7 @@ function startOverseer(proc, cwd, config2, getSnapshot = getGitSnapshot) {
986
1112
  // src/providers/claude.ts
987
1113
  var ClaudeProvider = class {
988
1114
  name = "claude";
1115
+ supportsNativeWorktree = true;
989
1116
  async isAvailable() {
990
1117
  try {
991
1118
  execSync("claude --version", { stdio: "ignore" });
@@ -1000,15 +1127,15 @@ var ClaudeProvider = class {
1000
1127
  const promptFile = join3(tmpDir, "prompt.md");
1001
1128
  writeFileSync4(promptFile, prompt, "utf-8");
1002
1129
  try {
1003
- const proc = spawn2(
1004
- "sh",
1005
- ["-c", `claude -p --dangerously-skip-permissions "$(cat '${promptFile}')"`],
1006
- {
1007
- cwd: opts.cwd,
1008
- stdio: ["ignore", "pipe", "pipe"],
1009
- env: { ...process.env, CLAUDECODE: void 0 }
1010
- }
1011
- );
1130
+ const flags = ["-p", "--dangerously-skip-permissions"];
1131
+ if (opts.useNativeWorktree) {
1132
+ flags.push("--worktree");
1133
+ }
1134
+ const proc = spawn2("sh", ["-c", `claude ${flags.join(" ")} "$(cat '${promptFile}')"`], {
1135
+ cwd: opts.cwd,
1136
+ stdio: ["ignore", "pipe", "pipe"],
1137
+ env: { ...process.env, CLAUDECODE: void 0 }
1138
+ });
1012
1139
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1013
1140
  const chunks = [];
1014
1141
  proc.stdout.on("data", (chunk) => {
@@ -1277,6 +1404,7 @@ async function runWithFallback(models, prompt, opts) {
1277
1404
  output: result.output,
1278
1405
  duration: result.duration,
1279
1406
  providerUsed: model,
1407
+ provider,
1280
1408
  attempts
1281
1409
  };
1282
1410
  }
@@ -1302,6 +1430,7 @@ async function runWithFallback(models, prompt, opts) {
1302
1430
  output: result.output,
1303
1431
  duration: result.duration,
1304
1432
  providerUsed: model,
1433
+ provider,
1305
1434
  attempts
1306
1435
  };
1307
1436
  }
@@ -1902,7 +2031,10 @@ function resolveModels(config2) {
1902
2031
  function buildPrBody(providerUsed, description) {
1903
2032
  const lines = [];
1904
2033
  if (description) {
1905
- lines.push("## Summary", "", description, "");
2034
+ const sanitized = sanitizePrBody(description);
2035
+ if (sanitized) {
2036
+ lines.push("## Summary", "", sanitized, "");
2037
+ }
1906
2038
  }
1907
2039
  lines.push(
1908
2040
  "---",
@@ -1926,6 +2058,22 @@ function cleanupPrTitle(cwd) {
1926
2058
  } catch {
1927
2059
  }
1928
2060
  }
2061
+ var PLAN_FILE = ".lisa-plan.json";
2062
+ function readLisaPlan(dir) {
2063
+ const planPath = join7(dir, PLAN_FILE);
2064
+ if (!existsSync6(planPath)) return null;
2065
+ try {
2066
+ return JSON.parse(readFileSync5(planPath, "utf-8").trim());
2067
+ } catch {
2068
+ return null;
2069
+ }
2070
+ }
2071
+ function cleanupPlan(dir) {
2072
+ try {
2073
+ unlinkSync4(join7(dir, PLAN_FILE));
2074
+ } catch {
2075
+ }
2076
+ }
1929
2077
  var MANIFEST_FILE = ".lisa-manifest.json";
1930
2078
  function readLisaManifest(dir) {
1931
2079
  const manifestPath = join7(dir, MANIFEST_FILE);
@@ -2271,6 +2419,24 @@ async function runTestValidation(cwd) {
2271
2419
  return false;
2272
2420
  }
2273
2421
  }
2422
+ async function findWorktreeForBranch(repoRoot, branch) {
2423
+ try {
2424
+ const { stdout } = await execa3("git", ["worktree", "list", "--porcelain"], { cwd: repoRoot });
2425
+ const lines = stdout.split("\n");
2426
+ let currentPath = null;
2427
+ for (const line of lines) {
2428
+ if (line.startsWith("worktree ")) {
2429
+ currentPath = line.slice("worktree ".length);
2430
+ }
2431
+ if (line.startsWith("branch ") && line.endsWith(`/${branch}`)) {
2432
+ return currentPath;
2433
+ }
2434
+ }
2435
+ return null;
2436
+ } catch {
2437
+ return null;
2438
+ }
2439
+ }
2274
2440
  async function runWorktreeSession(config2, issue, logFile, session, models) {
2275
2441
  if (config2.repos.length > 1) {
2276
2442
  return runWorktreeMultiRepoSession(config2, issue, logFile, session, models);
@@ -2278,6 +2444,140 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
2278
2444
  const workspace = resolve5(config2.workspace);
2279
2445
  const repoPath = determineRepoPath(config2.repos, issue, workspace) ?? workspace;
2280
2446
  const defaultBranch = resolveBaseBranch(config2, repoPath);
2447
+ const primaryProvider = createProvider(models[0] ?? "claude");
2448
+ const useNativeWorktree = primaryProvider.supportsNativeWorktree === true;
2449
+ if (useNativeWorktree) {
2450
+ return runNativeWorktreeSession(
2451
+ config2,
2452
+ issue,
2453
+ logFile,
2454
+ session,
2455
+ models,
2456
+ repoPath,
2457
+ defaultBranch
2458
+ );
2459
+ }
2460
+ return runManualWorktreeSession(config2, issue, logFile, session, models, repoPath, defaultBranch);
2461
+ }
2462
+ async function runNativeWorktreeSession(config2, issue, logFile, session, models, repoPath, defaultBranch) {
2463
+ const failResult = (providerUsed, fallback) => ({
2464
+ success: false,
2465
+ providerUsed,
2466
+ prUrls: [],
2467
+ fallback: fallback ?? { success: false, output: "", duration: 0, providerUsed, attempts: [] }
2468
+ });
2469
+ const repo = findRepoConfig(config2, issue);
2470
+ if (repo?.lifecycle) {
2471
+ startSpinner(`${issue.id} \u2014 starting resources...`);
2472
+ const started = await startResources(repo, repoPath);
2473
+ stopSpinner();
2474
+ if (!started) {
2475
+ error(`Lifecycle startup failed for ${issue.id}. Aborting session.`);
2476
+ return failResult(models[0] ?? "claude");
2477
+ }
2478
+ }
2479
+ const testRunner = detectTestRunner(repoPath);
2480
+ if (testRunner) log(`Detected test runner: ${testRunner}`);
2481
+ cleanupManifest(repoPath);
2482
+ const prompt = buildNativeWorktreePrompt(issue, repoPath, testRunner);
2483
+ startSpinner(`${issue.id} \u2014 implementing (native worktree)...`);
2484
+ log(`Implementing with native worktree... (log: ${logFile})`);
2485
+ initLogFile(logFile);
2486
+ const result = await runWithFallback(models, prompt, {
2487
+ logFile,
2488
+ cwd: repoPath,
2489
+ guardrailsDir: repoPath,
2490
+ issueId: issue.id,
2491
+ overseer: config2.overseer,
2492
+ useNativeWorktree: true
2493
+ });
2494
+ stopSpinner();
2495
+ try {
2496
+ appendFileSync6(
2497
+ logFile,
2498
+ `
2499
+ ${"=".repeat(80)}
2500
+ Provider used: ${result.providerUsed}
2501
+ Full output:
2502
+ ${result.output}
2503
+ `
2504
+ );
2505
+ } catch {
2506
+ }
2507
+ if (repo?.lifecycle) await stopResources();
2508
+ if (!result.success) {
2509
+ error(`Session ${session} failed for ${issue.id}. Check ${logFile}`);
2510
+ cleanupManifest(repoPath);
2511
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2512
+ }
2513
+ const manifest = readLisaManifest(repoPath);
2514
+ if (!manifest?.branch) {
2515
+ error(`Agent did not produce a valid .lisa-manifest.json for ${issue.id}. Aborting.`);
2516
+ cleanupManifest(repoPath);
2517
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2518
+ }
2519
+ const effectiveBranch = manifest.branch;
2520
+ ok(`Agent created branch: ${effectiveBranch}`);
2521
+ const worktreePath = await findWorktreeForBranch(repoPath, effectiveBranch);
2522
+ const effectiveCwd = worktreePath ?? repoPath;
2523
+ if (!worktreePath) {
2524
+ warn(`No worktree found for branch ${effectiveBranch} \u2014 using repo root`);
2525
+ }
2526
+ startSpinner(`${issue.id} \u2014 validating tests...`);
2527
+ const testsPassed = await runTestValidation(effectiveCwd);
2528
+ stopSpinner();
2529
+ if (!testsPassed) {
2530
+ error(`Tests failed for ${issue.id}. Blocking PR creation.`);
2531
+ cleanupManifest(repoPath);
2532
+ if (worktreePath) await cleanupWorktree(repoPath, worktreePath);
2533
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2534
+ }
2535
+ startSpinner(`${issue.id} \u2014 pushing...`);
2536
+ const pushResult = await pushWithRecovery({
2537
+ branch: effectiveBranch,
2538
+ cwd: effectiveCwd,
2539
+ models,
2540
+ logFile,
2541
+ guardrailsDir: repoPath,
2542
+ issueId: issue.id,
2543
+ overseer: config2.overseer
2544
+ });
2545
+ stopSpinner();
2546
+ if (!pushResult.success) {
2547
+ error(`Failed to push branch to remote: ${pushResult.error}`);
2548
+ cleanupManifest(repoPath);
2549
+ if (worktreePath) await cleanupWorktree(repoPath, worktreePath);
2550
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2551
+ }
2552
+ startSpinner(`${issue.id} \u2014 creating PR...`);
2553
+ const prTitle = manifest.prTitle ?? issue.title;
2554
+ const prBody = manifest.prBody;
2555
+ cleanupManifest(repoPath);
2556
+ const prUrls = [];
2557
+ try {
2558
+ const repoInfo = await getRepoInfo(effectiveCwd);
2559
+ const pr = await createPullRequest(
2560
+ {
2561
+ owner: repoInfo.owner,
2562
+ repo: repoInfo.repo,
2563
+ head: effectiveBranch,
2564
+ base: defaultBranch,
2565
+ title: prTitle,
2566
+ body: buildPrBody(result.providerUsed, prBody)
2567
+ },
2568
+ config2.github
2569
+ );
2570
+ ok(`PR created: ${pr.html_url}`);
2571
+ prUrls.push(pr.html_url);
2572
+ } catch (err) {
2573
+ error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
2574
+ }
2575
+ stopSpinner();
2576
+ if (worktreePath) await cleanupWorktree(repoPath, worktreePath);
2577
+ ok(`Session ${session} complete for ${issue.id}`);
2578
+ return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
2579
+ }
2580
+ async function runManualWorktreeSession(config2, issue, logFile, session, models, repoPath, defaultBranch) {
2281
2581
  const branchName = generateBranchName(issue.id, issue.title);
2282
2582
  startSpinner(`${issue.id} \u2014 creating worktree...`);
2283
2583
  log(`Creating worktree for ${branchName} (base: ${defaultBranch})...`);
@@ -2431,12 +2731,12 @@ ${result.output}
2431
2731
  async function runWorktreeMultiRepoSession(config2, issue, logFile, session, models) {
2432
2732
  const workspace = resolve5(config2.workspace);
2433
2733
  cleanupManifest(workspace);
2434
- const prompt = buildWorktreeMultiRepoPrompt(issue, config2);
2435
- startSpinner(`${issue.id} \u2014 implementing...`);
2436
- log(`Multi-repo worktree session for ${issue.id} (agent selects repo and branch name)`);
2437
- log(`Implementing (agent selects repo)... (log: ${logFile})`);
2734
+ cleanupPlan(workspace);
2735
+ startSpinner(`${issue.id} \u2014 analyzing issue...`);
2736
+ log(`Multi-repo planning phase for ${issue.id}`);
2438
2737
  initLogFile(logFile);
2439
- const result = await runWithFallback(models, prompt, {
2738
+ const planPrompt = buildPlanningPrompt(issue, config2);
2739
+ const planResult = await runWithFallback(models, planPrompt, {
2440
2740
  logFile,
2441
2741
  cwd: workspace,
2442
2742
  guardrailsDir: workspace,
@@ -2449,87 +2749,197 @@ async function runWorktreeMultiRepoSession(config2, issue, logFile, session, mod
2449
2749
  logFile,
2450
2750
  `
2451
2751
  ${"=".repeat(80)}
2452
- Provider used: ${result.providerUsed}
2453
- Full output:
2454
- ${result.output}
2752
+ Planning phase \u2014 provider: ${planResult.providerUsed}
2753
+ ${planResult.output}
2455
2754
  `
2456
2755
  );
2457
2756
  } catch {
2458
2757
  }
2459
- if (!result.success) {
2460
- error(`Session ${session} failed for ${issue.id}. Check ${logFile}`);
2461
- cleanupManifest(workspace);
2462
- return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2758
+ if (!planResult.success) {
2759
+ error(`Planning phase failed for ${issue.id}. Check ${logFile}`);
2760
+ cleanupPlan(workspace);
2761
+ return {
2762
+ success: false,
2763
+ providerUsed: planResult.providerUsed,
2764
+ prUrls: [],
2765
+ fallback: planResult
2766
+ };
2463
2767
  }
2464
- const manifest = readLisaManifest(workspace);
2465
- if (!manifest?.repoPath || !manifest.branch) {
2466
- error(
2467
- `Agent did not produce a valid .lisa-manifest.json (requires repoPath + branch) for ${issue.id}. Aborting.`
2768
+ const plan = readLisaPlan(workspace);
2769
+ if (!plan?.steps || plan.steps.length === 0) {
2770
+ error(`Agent did not produce a valid .lisa-plan.json for ${issue.id}. Aborting.`);
2771
+ cleanupPlan(workspace);
2772
+ return {
2773
+ success: false,
2774
+ providerUsed: planResult.providerUsed,
2775
+ prUrls: [],
2776
+ fallback: planResult
2777
+ };
2778
+ }
2779
+ const sortedSteps = [...plan.steps].sort((a, b) => a.order - b.order);
2780
+ ok(
2781
+ `Plan produced ${sortedSteps.length} step(s): ${sortedSteps.map((s) => s.repoPath).join(" \u2192 ")}`
2782
+ );
2783
+ cleanupPlan(workspace);
2784
+ const prUrls = [];
2785
+ const previousResults = [];
2786
+ let lastFallback = planResult;
2787
+ let lastProvider = planResult.providerUsed;
2788
+ for (const [i, step] of sortedSteps.entries()) {
2789
+ const stepNum = i + 1;
2790
+ divider(stepNum);
2791
+ log(`Step ${stepNum}/${sortedSteps.length}: ${step.repoPath} \u2014 ${step.scope}`);
2792
+ const stepResult = await runMultiRepoStep(
2793
+ config2,
2794
+ issue,
2795
+ step,
2796
+ previousResults,
2797
+ logFile,
2798
+ models,
2799
+ stepNum
2468
2800
  );
2469
- cleanupManifest(workspace);
2470
- return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2801
+ lastFallback = stepResult.fallback;
2802
+ lastProvider = stepResult.providerUsed;
2803
+ if (!stepResult.success) {
2804
+ error(`Step ${stepNum} failed for ${step.repoPath}. Aborting remaining steps.`);
2805
+ return {
2806
+ success: false,
2807
+ providerUsed: lastProvider,
2808
+ prUrls,
2809
+ fallback: lastFallback
2810
+ };
2811
+ }
2812
+ if (stepResult.prUrl) {
2813
+ prUrls.push(stepResult.prUrl);
2814
+ }
2815
+ previousResults.push({
2816
+ repoPath: step.repoPath,
2817
+ branch: stepResult.branch,
2818
+ prUrl: stepResult.prUrl
2819
+ });
2471
2820
  }
2472
- ok(`Provider chose repo: ${manifest.repoPath}, branch: ${manifest.branch}`);
2473
- const worktreePath = join7(manifest.repoPath, ".worktrees", manifest.branch);
2474
- const baseBranch = resolveBaseBranch(config2, manifest.repoPath);
2475
- const hasWorktree = existsSync6(worktreePath);
2476
- const effectiveCwd = hasWorktree ? worktreePath : manifest.repoPath;
2477
- if (!hasWorktree) {
2478
- warn(`Worktree not found at ${worktreePath} \u2014 using repo root for git operations`);
2821
+ ok(`Session ${session} complete for ${issue.id} \u2014 ${prUrls.length} PR(s) created`);
2822
+ return { success: true, providerUsed: lastProvider, prUrls, fallback: lastFallback };
2823
+ }
2824
+ async function runMultiRepoStep(config2, issue, step, previousResults, logFile, models, stepNum) {
2825
+ const repoPath = step.repoPath;
2826
+ const defaultBranch = resolveBaseBranch(config2, repoPath);
2827
+ const branchName = generateBranchName(issue.id, issue.title);
2828
+ const failResult = (providerUsed, fallback) => ({
2829
+ success: false,
2830
+ providerUsed,
2831
+ branch: branchName,
2832
+ fallback: fallback ?? { success: false, output: "", duration: 0, providerUsed, attempts: [] }
2833
+ });
2834
+ startSpinner(`${issue.id} step ${stepNum} \u2014 creating worktree...`);
2835
+ let worktreePath;
2836
+ try {
2837
+ worktreePath = await createWorktree(repoPath, branchName, defaultBranch);
2838
+ } catch (err) {
2839
+ stopSpinner();
2840
+ error(`Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`);
2841
+ return failResult(models[0] ?? "claude");
2479
2842
  }
2480
- startSpinner(`${issue.id} \u2014 validating tests...`);
2481
- const testsPassed = await runTestValidation(effectiveCwd);
2843
+ stopSpinner();
2844
+ ok(`Worktree created at ${worktreePath}`);
2845
+ const testRunner = detectTestRunner(worktreePath);
2846
+ if (testRunner) log(`Detected test runner: ${testRunner}`);
2847
+ const prompt = buildScopedImplementPrompt(issue, step, previousResults, testRunner);
2848
+ startSpinner(`${issue.id} step ${stepNum} \u2014 implementing...`);
2849
+ const result = await runWithFallback(models, prompt, {
2850
+ logFile,
2851
+ cwd: worktreePath,
2852
+ guardrailsDir: repoPath,
2853
+ issueId: issue.id,
2854
+ overseer: config2.overseer
2855
+ });
2856
+ stopSpinner();
2857
+ try {
2858
+ appendFileSync6(
2859
+ logFile,
2860
+ `
2861
+ ${"=".repeat(80)}
2862
+ Step ${stepNum} \u2014 provider: ${result.providerUsed}
2863
+ ${result.output}
2864
+ `
2865
+ );
2866
+ } catch {
2867
+ }
2868
+ if (!result.success) {
2869
+ error(`Step ${stepNum} implementation failed. Check ${logFile}`);
2870
+ await cleanupWorktree(repoPath, worktreePath);
2871
+ return { ...failResult(result.providerUsed, result), branch: branchName };
2872
+ }
2873
+ const manifest = readLisaManifest(worktreePath);
2874
+ let effectiveBranch = branchName;
2875
+ if (manifest?.branch && manifest.branch !== branchName) {
2876
+ log(`Renaming branch to: ${manifest.branch}`);
2877
+ try {
2878
+ await execa3("git", ["branch", "-m", branchName, manifest.branch], { cwd: worktreePath });
2879
+ effectiveBranch = manifest.branch;
2880
+ } catch (err) {
2881
+ warn(`Branch rename failed: ${err instanceof Error ? err.message : String(err)}`);
2882
+ }
2883
+ }
2884
+ startSpinner(`${issue.id} step ${stepNum} \u2014 validating tests...`);
2885
+ const testsPassed = await runTestValidation(worktreePath);
2482
2886
  stopSpinner();
2483
2887
  if (!testsPassed) {
2484
- error(`Tests failed for ${issue.id}. Blocking PR creation.`);
2485
- if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2486
- cleanupManifest(workspace);
2487
- return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2888
+ error(`Tests failed for step ${stepNum}. Blocking PR creation.`);
2889
+ cleanupManifest(worktreePath);
2890
+ await cleanupWorktree(repoPath, worktreePath);
2891
+ return { ...failResult(result.providerUsed, result), branch: effectiveBranch };
2488
2892
  }
2489
- startSpinner(`${issue.id} \u2014 pushing...`);
2893
+ startSpinner(`${issue.id} step ${stepNum} \u2014 pushing...`);
2490
2894
  const pushResult = await pushWithRecovery({
2491
- branch: manifest.branch,
2492
- cwd: effectiveCwd,
2895
+ branch: effectiveBranch,
2896
+ cwd: worktreePath,
2493
2897
  models,
2494
2898
  logFile,
2495
- guardrailsDir: manifest.repoPath,
2899
+ guardrailsDir: repoPath,
2496
2900
  issueId: issue.id,
2497
2901
  overseer: config2.overseer
2498
2902
  });
2499
2903
  stopSpinner();
2500
2904
  if (!pushResult.success) {
2501
- error(`Failed to push branch to remote: ${pushResult.error}`);
2502
- if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2503
- cleanupManifest(workspace);
2504
- return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2905
+ error(`Failed to push step ${stepNum}: ${pushResult.error}`);
2906
+ cleanupManifest(worktreePath);
2907
+ await cleanupWorktree(repoPath, worktreePath);
2908
+ return { ...failResult(result.providerUsed, result), branch: effectiveBranch };
2505
2909
  }
2506
- startSpinner(`${issue.id} \u2014 creating PR...`);
2507
- const prTitle = manifest.prTitle ?? issue.title;
2508
- const prBody = manifest.prBody;
2509
- const prUrls = [];
2910
+ startSpinner(`${issue.id} step ${stepNum} \u2014 creating PR...`);
2911
+ const prTitle = manifest?.prTitle ?? issue.title;
2912
+ const prBody = manifest?.prBody;
2913
+ cleanupManifest(worktreePath);
2914
+ let prUrl;
2510
2915
  try {
2511
- const repoInfo = await getRepoInfo(effectiveCwd);
2916
+ const repoInfo = await getRepoInfo(worktreePath);
2512
2917
  const pr = await createPullRequest(
2513
2918
  {
2514
2919
  owner: repoInfo.owner,
2515
2920
  repo: repoInfo.repo,
2516
- head: manifest.branch,
2517
- base: baseBranch,
2921
+ head: effectiveBranch,
2922
+ base: defaultBranch,
2518
2923
  title: prTitle,
2519
2924
  body: buildPrBody(result.providerUsed, prBody)
2520
2925
  },
2521
2926
  config2.github
2522
2927
  );
2523
2928
  ok(`PR created: ${pr.html_url}`);
2524
- prUrls.push(pr.html_url);
2929
+ prUrl = pr.html_url;
2525
2930
  } catch (err) {
2526
2931
  error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
2527
2932
  }
2528
2933
  stopSpinner();
2529
- cleanupManifest(workspace);
2530
- if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2531
- ok(`Session ${session} complete for ${issue.id}`);
2532
- return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
2934
+ await cleanupWorktree(repoPath, worktreePath);
2935
+ ok(`Step ${stepNum} complete: ${repoPath}`);
2936
+ return {
2937
+ success: true,
2938
+ providerUsed: result.providerUsed,
2939
+ branch: effectiveBranch,
2940
+ prUrl,
2941
+ fallback: result
2942
+ };
2533
2943
  }
2534
2944
  async function runBranchSession(config2, issue, logFile, session, models) {
2535
2945
  const workspace = resolve5(config2.workspace);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Deterministic autonomous issue resolver — structured AI agent loop for Linear/Trello",
5
5
  "license": "MIT",
6
6
  "type": "module",