@tarcisiopgs/lisa 1.2.0 → 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 +567 -155
  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": "<English summary of what was implemented and why, 2-5 sentences>"
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\` should describe WHAT was changed and WHY, not just repeat the title. Mention 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);
@@ -701,9 +635,9 @@ ${testBlock}${readmeBlock}${hookBlock}
701
635
 
702
636
  4. **Write manifest**: Create \`.lisa-manifest.json\` in the **current directory** with JSON:
703
637
  \`\`\`json
704
- {"branch": "<final English branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<English summary of what was implemented and why, 2-5 sentences>"}
638
+ {"branch": "<final English branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<markdown-formatted English summary>"}
705
639
  \`\`\`
706
- The \`prBody\` should describe WHAT was changed and WHY, not just repeat the title. Mention 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
@@ -771,9 +705,9 @@ ${testBlock}${readmeBlock}${hookBlock}
771
705
 
772
706
  6. **Write manifest**: Before finishing, create \`${manifestPath}\` with JSON:
773
707
  \`\`\`json
774
- {"repoPath": "<absolute path to this repo>", "branch": "<branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<English summary of what was implemented and why, 2-5 sentences>"}
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\` should describe WHAT was changed and WHY, not just repeat the title. Mention 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,9 +2031,14 @@ function resolveModels(config2) {
1902
2031
  function buildPrBody(providerUsed, description) {
1903
2032
  const lines = [];
1904
2033
  if (description) {
1905
- lines.push(description, "");
2034
+ const sanitized = sanitizePrBody(description);
2035
+ if (sanitized) {
2036
+ lines.push("## Summary", "", sanitized, "");
2037
+ }
1906
2038
  }
1907
2039
  lines.push(
2040
+ "---",
2041
+ "",
1908
2042
  `Implemented by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerUsed}**.`
1909
2043
  );
1910
2044
  return lines.join("\n");
@@ -1924,6 +2058,22 @@ function cleanupPrTitle(cwd) {
1924
2058
  } catch {
1925
2059
  }
1926
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
+ }
1927
2077
  var MANIFEST_FILE = ".lisa-manifest.json";
1928
2078
  function readLisaManifest(dir) {
1929
2079
  const manifestPath = join7(dir, MANIFEST_FILE);
@@ -2269,6 +2419,24 @@ async function runTestValidation(cwd) {
2269
2419
  return false;
2270
2420
  }
2271
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
+ }
2272
2440
  async function runWorktreeSession(config2, issue, logFile, session, models) {
2273
2441
  if (config2.repos.length > 1) {
2274
2442
  return runWorktreeMultiRepoSession(config2, issue, logFile, session, models);
@@ -2276,6 +2444,140 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
2276
2444
  const workspace = resolve5(config2.workspace);
2277
2445
  const repoPath = determineRepoPath(config2.repos, issue, workspace) ?? workspace;
2278
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) {
2279
2581
  const branchName = generateBranchName(issue.id, issue.title);
2280
2582
  startSpinner(`${issue.id} \u2014 creating worktree...`);
2281
2583
  log(`Creating worktree for ${branchName} (base: ${defaultBranch})...`);
@@ -2429,12 +2731,12 @@ ${result.output}
2429
2731
  async function runWorktreeMultiRepoSession(config2, issue, logFile, session, models) {
2430
2732
  const workspace = resolve5(config2.workspace);
2431
2733
  cleanupManifest(workspace);
2432
- const prompt = buildWorktreeMultiRepoPrompt(issue, config2);
2433
- startSpinner(`${issue.id} \u2014 implementing...`);
2434
- log(`Multi-repo worktree session for ${issue.id} (agent selects repo and branch name)`);
2435
- 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}`);
2436
2737
  initLogFile(logFile);
2437
- const result = await runWithFallback(models, prompt, {
2738
+ const planPrompt = buildPlanningPrompt(issue, config2);
2739
+ const planResult = await runWithFallback(models, planPrompt, {
2438
2740
  logFile,
2439
2741
  cwd: workspace,
2440
2742
  guardrailsDir: workspace,
@@ -2447,87 +2749,197 @@ async function runWorktreeMultiRepoSession(config2, issue, logFile, session, mod
2447
2749
  logFile,
2448
2750
  `
2449
2751
  ${"=".repeat(80)}
2450
- Provider used: ${result.providerUsed}
2451
- Full output:
2452
- ${result.output}
2752
+ Planning phase \u2014 provider: ${planResult.providerUsed}
2753
+ ${planResult.output}
2453
2754
  `
2454
2755
  );
2455
2756
  } catch {
2456
2757
  }
2457
- if (!result.success) {
2458
- error(`Session ${session} failed for ${issue.id}. Check ${logFile}`);
2459
- cleanupManifest(workspace);
2460
- 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
+ };
2461
2767
  }
2462
- const manifest = readLisaManifest(workspace);
2463
- if (!manifest?.repoPath || !manifest.branch) {
2464
- error(
2465
- `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
2466
2800
  );
2467
- cleanupManifest(workspace);
2468
- 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
+ });
2469
2820
  }
2470
- ok(`Provider chose repo: ${manifest.repoPath}, branch: ${manifest.branch}`);
2471
- const worktreePath = join7(manifest.repoPath, ".worktrees", manifest.branch);
2472
- const baseBranch = resolveBaseBranch(config2, manifest.repoPath);
2473
- const hasWorktree = existsSync6(worktreePath);
2474
- const effectiveCwd = hasWorktree ? worktreePath : manifest.repoPath;
2475
- if (!hasWorktree) {
2476
- 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");
2477
2842
  }
2478
- startSpinner(`${issue.id} \u2014 validating tests...`);
2479
- 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);
2480
2886
  stopSpinner();
2481
2887
  if (!testsPassed) {
2482
- error(`Tests failed for ${issue.id}. Blocking PR creation.`);
2483
- if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2484
- cleanupManifest(workspace);
2485
- 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 };
2486
2892
  }
2487
- startSpinner(`${issue.id} \u2014 pushing...`);
2893
+ startSpinner(`${issue.id} step ${stepNum} \u2014 pushing...`);
2488
2894
  const pushResult = await pushWithRecovery({
2489
- branch: manifest.branch,
2490
- cwd: effectiveCwd,
2895
+ branch: effectiveBranch,
2896
+ cwd: worktreePath,
2491
2897
  models,
2492
2898
  logFile,
2493
- guardrailsDir: manifest.repoPath,
2899
+ guardrailsDir: repoPath,
2494
2900
  issueId: issue.id,
2495
2901
  overseer: config2.overseer
2496
2902
  });
2497
2903
  stopSpinner();
2498
2904
  if (!pushResult.success) {
2499
- error(`Failed to push branch to remote: ${pushResult.error}`);
2500
- if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2501
- cleanupManifest(workspace);
2502
- 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 };
2503
2909
  }
2504
- startSpinner(`${issue.id} \u2014 creating PR...`);
2505
- const prTitle = manifest.prTitle ?? issue.title;
2506
- const prBody = manifest.prBody;
2507
- 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;
2508
2915
  try {
2509
- const repoInfo = await getRepoInfo(effectiveCwd);
2916
+ const repoInfo = await getRepoInfo(worktreePath);
2510
2917
  const pr = await createPullRequest(
2511
2918
  {
2512
2919
  owner: repoInfo.owner,
2513
2920
  repo: repoInfo.repo,
2514
- head: manifest.branch,
2515
- base: baseBranch,
2921
+ head: effectiveBranch,
2922
+ base: defaultBranch,
2516
2923
  title: prTitle,
2517
2924
  body: buildPrBody(result.providerUsed, prBody)
2518
2925
  },
2519
2926
  config2.github
2520
2927
  );
2521
2928
  ok(`PR created: ${pr.html_url}`);
2522
- prUrls.push(pr.html_url);
2929
+ prUrl = pr.html_url;
2523
2930
  } catch (err) {
2524
2931
  error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
2525
2932
  }
2526
2933
  stopSpinner();
2527
- cleanupManifest(workspace);
2528
- if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2529
- ok(`Session ${session} complete for ${issue.id}`);
2530
- 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
+ };
2531
2943
  }
2532
2944
  async function runBranchSession(config2, issue, logFile, session, models) {
2533
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.0",
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",