@tarcisiopgs/lisa 1.0.3 → 1.2.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 +114 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -638,9 +638,11 @@ ${readmeBlock}${hookBlock}
638
638
  {
639
639
  "repoPath": "<absolute path to the chosen repo>",
640
640
  "branch": "<your English branch name>",
641
- "prTitle": "<PR title in English, conventional commit format>"
641
+ "prTitle": "<PR title in English, conventional commit format>",
642
+ "prBody": "<English summary of what was implemented and why, 2-5 sentences>"
642
643
  }
643
644
  \`\`\`
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.
644
646
  Do NOT commit this file.
645
647
 
646
648
  ## Rules
@@ -699,8 +701,9 @@ ${testBlock}${readmeBlock}${hookBlock}
699
701
 
700
702
  4. **Write manifest**: Create \`.lisa-manifest.json\` in the **current directory** with JSON:
701
703
  \`\`\`json
702
- {"branch": "<final English branch name>", "prTitle": "<English PR title, conventional commit format>"}
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>"}
703
705
  \`\`\`
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.
704
707
  Do NOT commit this file.
705
708
 
706
709
  ## Rules
@@ -768,8 +771,9 @@ ${testBlock}${readmeBlock}${hookBlock}
768
771
 
769
772
  6. **Write manifest**: Before finishing, create \`${manifestPath}\` with JSON:
770
773
  \`\`\`json
771
- {"repoPath": "<absolute path to this repo>", "branch": "<branch name>", "prTitle": "<English PR title, conventional commit format>"}
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>"}
772
775
  \`\`\`
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.
773
777
  Do NOT commit this file.
774
778
 
775
779
  ## Rules
@@ -1715,6 +1719,50 @@ function createSource(name) {
1715
1719
  return factory();
1716
1720
  }
1717
1721
 
1722
+ // src/terminal.ts
1723
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1724
+ var SPINNER_INTERVAL_MS = 80;
1725
+ var spinnerTimer = null;
1726
+ var spinnerFrame = 0;
1727
+ function isTTY() {
1728
+ return process.stdout.isTTY === true;
1729
+ }
1730
+ function writeOSC(title) {
1731
+ process.stdout.write(`\x1B]0;${title}\x07`);
1732
+ }
1733
+ function setTitle(title) {
1734
+ if (!isTTY()) return;
1735
+ writeOSC(title);
1736
+ }
1737
+ function startSpinner(message) {
1738
+ if (!isTTY()) return;
1739
+ stopSpinner();
1740
+ spinnerFrame = 0;
1741
+ writeOSC(`${SPINNER_FRAMES[0]} Lisa \u2014 ${message}`);
1742
+ spinnerTimer = setInterval(() => {
1743
+ spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
1744
+ writeOSC(`${SPINNER_FRAMES[spinnerFrame]} Lisa \u2014 ${message}`);
1745
+ }, SPINNER_INTERVAL_MS);
1746
+ }
1747
+ function stopSpinner(message) {
1748
+ if (spinnerTimer) {
1749
+ clearInterval(spinnerTimer);
1750
+ spinnerTimer = null;
1751
+ }
1752
+ if (!isTTY()) return;
1753
+ if (message) {
1754
+ writeOSC(message);
1755
+ }
1756
+ }
1757
+ function notify() {
1758
+ if (!isTTY()) return;
1759
+ process.stdout.write("\x07");
1760
+ }
1761
+ function resetTitle() {
1762
+ if (!isTTY()) return;
1763
+ writeOSC("");
1764
+ }
1765
+
1718
1766
  // src/worktree.ts
1719
1767
  import { appendFileSync as appendFileSync5, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1720
1768
  import { join as join6, resolve as resolve4 } from "path";
@@ -1851,10 +1899,15 @@ function resolveModels(config2) {
1851
1899
  if (config2.models && config2.models.length > 0) return config2.models;
1852
1900
  return [config2.provider];
1853
1901
  }
1854
- function buildPrBody(issue, providerUsed) {
1855
- return `Closes ${issue.url}
1856
-
1857
- Implemented by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerUsed}**.`;
1902
+ function buildPrBody(providerUsed, description) {
1903
+ const lines = [];
1904
+ if (description) {
1905
+ lines.push(description, "");
1906
+ }
1907
+ lines.push(
1908
+ `Implemented by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerUsed}**.`
1909
+ );
1910
+ return lines.join("\n");
1858
1911
  }
1859
1912
  var PR_TITLE_FILE = ".pr-title";
1860
1913
  function readPrTitle(cwd) {
@@ -1946,6 +1999,8 @@ function installSignalHandlers() {
1946
1999
  process.exit(1);
1947
2000
  }
1948
2001
  shuttingDown = true;
2002
+ stopSpinner();
2003
+ resetTitle();
1949
2004
  warn(`Received ${signal}. Reverting active issue...`);
1950
2005
  if (activeCleanup) {
1951
2006
  const { issueId, previousStatus, source } = activeCleanup;
@@ -2022,12 +2077,14 @@ async function runLoop(config2, opts) {
2022
2077
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
2023
2078
  const logFile = resolve5(config2.logs.dir, `session_${session}_${timestamp2}.log`);
2024
2079
  divider(session);
2080
+ startSpinner("fetching issue...");
2025
2081
  if (opts.issueId) {
2026
2082
  log(`Fetching issue '${opts.issueId}' from ${config2.source}...`);
2027
2083
  } else {
2028
2084
  log(`Fetching next '${config2.source_config.label}' issue from ${config2.source}...`);
2029
2085
  }
2030
2086
  if (opts.dryRun) {
2087
+ stopSpinner();
2031
2088
  if (opts.issueId) {
2032
2089
  log(`[dry-run] Would fetch issue '${opts.issueId}' from ${config2.source}`);
2033
2090
  } else {
@@ -2044,11 +2101,14 @@ async function runLoop(config2, opts) {
2044
2101
  try {
2045
2102
  issue = opts.issueId ? await source.fetchIssueById(opts.issueId) : await source.fetchNextIssue(config2.source_config);
2046
2103
  } catch (err) {
2104
+ stopSpinner();
2047
2105
  error(`Failed to fetch issues: ${err instanceof Error ? err.message : String(err)}`);
2048
2106
  if (opts.once) break;
2107
+ setTitle("Lisa \u2014 cooling down...");
2049
2108
  await sleep(config2.loop.cooldown * 1e3);
2050
2109
  continue;
2051
2110
  }
2111
+ stopSpinner();
2052
2112
  if (!issue) {
2053
2113
  if (opts.issueId) {
2054
2114
  error(`Issue '${opts.issueId}' not found.`);
@@ -2058,6 +2118,7 @@ async function runLoop(config2, opts) {
2058
2118
  break;
2059
2119
  }
2060
2120
  ok(`Picked up: ${issue.id} \u2014 ${issue.title}`);
2121
+ setTitle(`Lisa \u2014 ${issue.id}`);
2061
2122
  const previousStatus = config2.source_config.pick_from;
2062
2123
  try {
2063
2124
  const inProgress = config2.source_config.in_progress;
@@ -2071,6 +2132,7 @@ async function runLoop(config2, opts) {
2071
2132
  try {
2072
2133
  sessionResult = config2.workflow === "worktree" ? await runWorktreeSession(config2, issue, logFile, session, models) : await runBranchSession(config2, issue, logFile, session, models);
2073
2134
  } catch (err) {
2135
+ stopSpinner();
2074
2136
  error(
2075
2137
  `Unhandled error in session for ${issue.id}: ${err instanceof Error ? err.message : String(err)}`
2076
2138
  );
@@ -2083,8 +2145,10 @@ async function runLoop(config2, opts) {
2083
2145
  );
2084
2146
  }
2085
2147
  activeCleanup = null;
2148
+ notify();
2086
2149
  if (opts.once) break;
2087
2150
  log(`Cooling down ${config2.loop.cooldown}s before next issue...`);
2151
+ setTitle("Lisa \u2014 cooling down...");
2088
2152
  await sleep(config2.loop.cooldown * 1e3);
2089
2153
  continue;
2090
2154
  }
@@ -2100,11 +2164,13 @@ async function runLoop(config2, opts) {
2100
2164
  );
2101
2165
  }
2102
2166
  activeCleanup = null;
2167
+ notify();
2103
2168
  if (opts.once) {
2104
2169
  log("Single iteration mode. Exiting.");
2105
2170
  break;
2106
2171
  }
2107
2172
  log(`Cooling down ${config2.loop.cooldown}s before next issue...`);
2173
+ setTitle("Lisa \u2014 cooling down...");
2108
2174
  await sleep(config2.loop.cooldown * 1e3);
2109
2175
  continue;
2110
2176
  }
@@ -2122,11 +2188,13 @@ async function runLoop(config2, opts) {
2122
2188
  );
2123
2189
  }
2124
2190
  activeCleanup = null;
2191
+ notify();
2125
2192
  if (opts.once) {
2126
2193
  log("Single iteration mode. Exiting.");
2127
2194
  break;
2128
2195
  }
2129
2196
  log(`Cooling down ${config2.loop.cooldown}s before next issue...`);
2197
+ setTitle("Lisa \u2014 cooling down...");
2130
2198
  await sleep(config2.loop.cooldown * 1e3);
2131
2199
  continue;
2132
2200
  }
@@ -2150,13 +2218,17 @@ async function runLoop(config2, opts) {
2150
2218
  error(`Failed to complete issue: ${err instanceof Error ? err.message : String(err)}`);
2151
2219
  }
2152
2220
  activeCleanup = null;
2221
+ stopSpinner(`\u2713 Lisa \u2014 ${issue.id} \u2014 PR created`);
2222
+ notify();
2153
2223
  if (opts.once) {
2154
2224
  log("Single iteration mode. Exiting.");
2155
2225
  break;
2156
2226
  }
2157
2227
  log(`Cooling down ${config2.loop.cooldown}s before next issue...`);
2228
+ setTitle("Lisa \u2014 cooling down...");
2158
2229
  await sleep(config2.loop.cooldown * 1e3);
2159
2230
  }
2231
+ resetTitle();
2160
2232
  ok(`lisa finished. ${session} session(s) run.`);
2161
2233
  }
2162
2234
  function logAttemptHistory(result) {
@@ -2205,11 +2277,13 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
2205
2277
  const repoPath = determineRepoPath(config2.repos, issue, workspace) ?? workspace;
2206
2278
  const defaultBranch = resolveBaseBranch(config2, repoPath);
2207
2279
  const branchName = generateBranchName(issue.id, issue.title);
2280
+ startSpinner(`${issue.id} \u2014 creating worktree...`);
2208
2281
  log(`Creating worktree for ${branchName} (base: ${defaultBranch})...`);
2209
2282
  let worktreePath;
2210
2283
  try {
2211
2284
  worktreePath = await createWorktree(repoPath, branchName, defaultBranch);
2212
2285
  } catch (err) {
2286
+ stopSpinner();
2213
2287
  error(`Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`);
2214
2288
  return {
2215
2289
  success: false,
@@ -2224,10 +2298,13 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
2224
2298
  }
2225
2299
  };
2226
2300
  }
2301
+ stopSpinner();
2227
2302
  ok(`Worktree created at ${worktreePath}`);
2228
2303
  const repo = findRepoConfig(config2, issue);
2229
2304
  if (repo?.lifecycle) {
2305
+ startSpinner(`${issue.id} \u2014 starting resources...`);
2230
2306
  const started = await startResources(repo, worktreePath);
2307
+ stopSpinner();
2231
2308
  if (!started) {
2232
2309
  error(`Lifecycle startup failed for ${issue.id}. Aborting session.`);
2233
2310
  await cleanupWorktree(repoPath, worktreePath);
@@ -2250,6 +2327,7 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
2250
2327
  log(`Detected test runner: ${testRunner}`);
2251
2328
  }
2252
2329
  const prompt = buildImplementPrompt(issue, config2, testRunner);
2330
+ startSpinner(`${issue.id} \u2014 implementing...`);
2253
2331
  log(`Implementing in worktree... (log: ${logFile})`);
2254
2332
  initLogFile(logFile);
2255
2333
  const result = await runWithFallback(models, prompt, {
@@ -2259,6 +2337,7 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
2259
2337
  issueId: issue.id,
2260
2338
  overseer: config2.overseer
2261
2339
  });
2340
+ stopSpinner();
2262
2341
  try {
2263
2342
  appendFileSync6(
2264
2343
  logFile,
@@ -2279,7 +2358,9 @@ ${result.output}
2279
2358
  await cleanupWorktree(repoPath, worktreePath);
2280
2359
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2281
2360
  }
2361
+ startSpinner(`${issue.id} \u2014 validating tests...`);
2282
2362
  const testsPassed = await runTestValidation(worktreePath);
2363
+ stopSpinner();
2283
2364
  if (!testsPassed) {
2284
2365
  error(`Tests failed for ${issue.id}. Blocking PR creation.`);
2285
2366
  await cleanupWorktree(repoPath, worktreePath);
@@ -2299,6 +2380,7 @@ ${result.output}
2299
2380
  );
2300
2381
  }
2301
2382
  }
2383
+ startSpinner(`${issue.id} \u2014 pushing...`);
2302
2384
  const pushResult = await pushWithRecovery({
2303
2385
  branch: effectiveBranch,
2304
2386
  cwd: worktreePath,
@@ -2308,13 +2390,16 @@ ${result.output}
2308
2390
  issueId: issue.id,
2309
2391
  overseer: config2.overseer
2310
2392
  });
2393
+ stopSpinner();
2311
2394
  if (!pushResult.success) {
2312
2395
  error(`Failed to push branch to remote: ${pushResult.error}`);
2313
2396
  cleanupManifest(worktreePath);
2314
2397
  await cleanupWorktree(repoPath, worktreePath);
2315
2398
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2316
2399
  }
2400
+ startSpinner(`${issue.id} \u2014 creating PR...`);
2317
2401
  const prTitle = manifest?.prTitle ?? readPrTitle(worktreePath) ?? issue.title;
2402
+ const prBody = manifest?.prBody;
2318
2403
  cleanupPrTitle(worktreePath);
2319
2404
  cleanupManifest(worktreePath);
2320
2405
  const prUrls = [];
@@ -2327,7 +2412,7 @@ ${result.output}
2327
2412
  head: effectiveBranch,
2328
2413
  base: defaultBranch,
2329
2414
  title: prTitle,
2330
- body: buildPrBody(issue, result.providerUsed)
2415
+ body: buildPrBody(result.providerUsed, prBody)
2331
2416
  },
2332
2417
  config2.github
2333
2418
  );
@@ -2336,6 +2421,7 @@ ${result.output}
2336
2421
  } catch (err) {
2337
2422
  error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
2338
2423
  }
2424
+ stopSpinner();
2339
2425
  await cleanupWorktree(repoPath, worktreePath);
2340
2426
  ok(`Session ${session} complete for ${issue.id}`);
2341
2427
  return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
@@ -2344,6 +2430,7 @@ async function runWorktreeMultiRepoSession(config2, issue, logFile, session, mod
2344
2430
  const workspace = resolve5(config2.workspace);
2345
2431
  cleanupManifest(workspace);
2346
2432
  const prompt = buildWorktreeMultiRepoPrompt(issue, config2);
2433
+ startSpinner(`${issue.id} \u2014 implementing...`);
2347
2434
  log(`Multi-repo worktree session for ${issue.id} (agent selects repo and branch name)`);
2348
2435
  log(`Implementing (agent selects repo)... (log: ${logFile})`);
2349
2436
  initLogFile(logFile);
@@ -2354,6 +2441,7 @@ async function runWorktreeMultiRepoSession(config2, issue, logFile, session, mod
2354
2441
  issueId: issue.id,
2355
2442
  overseer: config2.overseer
2356
2443
  });
2444
+ stopSpinner();
2357
2445
  try {
2358
2446
  appendFileSync6(
2359
2447
  logFile,
@@ -2387,13 +2475,16 @@ ${result.output}
2387
2475
  if (!hasWorktree) {
2388
2476
  warn(`Worktree not found at ${worktreePath} \u2014 using repo root for git operations`);
2389
2477
  }
2478
+ startSpinner(`${issue.id} \u2014 validating tests...`);
2390
2479
  const testsPassed = await runTestValidation(effectiveCwd);
2480
+ stopSpinner();
2391
2481
  if (!testsPassed) {
2392
2482
  error(`Tests failed for ${issue.id}. Blocking PR creation.`);
2393
2483
  if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2394
2484
  cleanupManifest(workspace);
2395
2485
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2396
2486
  }
2487
+ startSpinner(`${issue.id} \u2014 pushing...`);
2397
2488
  const pushResult = await pushWithRecovery({
2398
2489
  branch: manifest.branch,
2399
2490
  cwd: effectiveCwd,
@@ -2403,13 +2494,16 @@ ${result.output}
2403
2494
  issueId: issue.id,
2404
2495
  overseer: config2.overseer
2405
2496
  });
2497
+ stopSpinner();
2406
2498
  if (!pushResult.success) {
2407
2499
  error(`Failed to push branch to remote: ${pushResult.error}`);
2408
2500
  if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2409
2501
  cleanupManifest(workspace);
2410
2502
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2411
2503
  }
2504
+ startSpinner(`${issue.id} \u2014 creating PR...`);
2412
2505
  const prTitle = manifest.prTitle ?? issue.title;
2506
+ const prBody = manifest.prBody;
2413
2507
  const prUrls = [];
2414
2508
  try {
2415
2509
  const repoInfo = await getRepoInfo(effectiveCwd);
@@ -2420,7 +2514,7 @@ ${result.output}
2420
2514
  head: manifest.branch,
2421
2515
  base: baseBranch,
2422
2516
  title: prTitle,
2423
- body: buildPrBody(issue, result.providerUsed)
2517
+ body: buildPrBody(result.providerUsed, prBody)
2424
2518
  },
2425
2519
  config2.github
2426
2520
  );
@@ -2429,6 +2523,7 @@ ${result.output}
2429
2523
  } catch (err) {
2430
2524
  error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
2431
2525
  }
2526
+ stopSpinner();
2432
2527
  cleanupManifest(workspace);
2433
2528
  if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
2434
2529
  ok(`Session ${session} complete for ${issue.id}`);
@@ -2444,8 +2539,10 @@ async function runBranchSession(config2, issue, logFile, session, models) {
2444
2539
  const prompt = buildImplementPrompt(issue, config2, testRunner);
2445
2540
  const repo = findRepoConfig(config2, issue);
2446
2541
  if (repo?.lifecycle) {
2542
+ startSpinner(`${issue.id} \u2014 starting resources...`);
2447
2543
  const cwd = resolve5(workspace, repo.path);
2448
2544
  const started = await startResources(repo, cwd);
2545
+ stopSpinner();
2449
2546
  if (!started) {
2450
2547
  error(`Lifecycle startup failed for ${issue.id}. Aborting session.`);
2451
2548
  return {
@@ -2462,6 +2559,7 @@ async function runBranchSession(config2, issue, logFile, session, models) {
2462
2559
  };
2463
2560
  }
2464
2561
  }
2562
+ startSpinner(`${issue.id} \u2014 implementing...`);
2465
2563
  log(`Implementing... (log: ${logFile})`);
2466
2564
  initLogFile(logFile);
2467
2565
  const result = await runWithFallback(models, prompt, {
@@ -2471,6 +2569,7 @@ async function runBranchSession(config2, issue, logFile, session, models) {
2471
2569
  issueId: issue.id,
2472
2570
  overseer: config2.overseer
2473
2571
  });
2572
+ stopSpinner();
2474
2573
  try {
2475
2574
  appendFileSync6(
2476
2575
  logFile,
@@ -2490,7 +2589,9 @@ ${result.output}
2490
2589
  error(`Session ${session} failed for ${issue.id}. Check ${logFile}`);
2491
2590
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2492
2591
  }
2592
+ startSpinner(`${issue.id} \u2014 validating tests...`);
2493
2593
  const testsPassed = await runTestValidation(workspace);
2594
+ stopSpinner();
2494
2595
  if (!testsPassed) {
2495
2596
  error(`Tests failed for ${issue.id}. Blocking PR creation.`);
2496
2597
  cleanupManifest(workspace);
@@ -2513,7 +2614,9 @@ ${result.output}
2513
2614
  ok(`Session ${session} complete for ${issue.id}`);
2514
2615
  return { success: true, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2515
2616
  }
2617
+ startSpinner(`${issue.id} \u2014 creating PR...`);
2516
2618
  const prTitle = manifest?.prTitle ?? readPrTitle(workspace) ?? issue.title;
2619
+ const prBody = manifest?.prBody;
2517
2620
  cleanupPrTitle(workspace);
2518
2621
  const prUrls = [];
2519
2622
  for (const { repoPath, branch } of detected) {
@@ -2528,7 +2631,7 @@ ${result.output}
2528
2631
  head: branch,
2529
2632
  base: baseBranch,
2530
2633
  title: prTitle,
2531
- body: buildPrBody(issue, result.providerUsed)
2634
+ body: buildPrBody(result.providerUsed, prBody)
2532
2635
  },
2533
2636
  config2.github
2534
2637
  );
@@ -2538,6 +2641,7 @@ ${result.output}
2538
2641
  error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
2539
2642
  }
2540
2643
  }
2644
+ stopSpinner();
2541
2645
  ok(`Session ${session} complete for ${issue.id}`);
2542
2646
  return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
2543
2647
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "Deterministic autonomous issue resolver — structured AI agent loop for Linear/Trello",
5
5
  "license": "MIT",
6
6
  "type": "module",