@tarcisiopgs/lisa 1.14.2 → 1.15.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.
@@ -640,6 +640,7 @@ function useKanbanState(bellEnabled) {
640
640
  const [cards, setCards] = useState([]);
641
641
  const [isEmpty, setIsEmpty] = useState(false);
642
642
  const [isWatching, setIsWatching] = useState(false);
643
+ const [isWatchPrompt, setIsWatchPrompt] = useState(false);
643
644
  const [workComplete, setWorkComplete] = useState(
644
645
  null
645
646
  );
@@ -763,10 +764,17 @@ function useKanbanState(bellEnabled) {
763
764
  const onComplete = (data) => setWorkComplete(data);
764
765
  const onWatching = () => setIsWatching(true);
765
766
  const onWatchResume = () => setIsWatching(false);
767
+ const onWatchPrompt = () => {
768
+ setIsWatchPrompt(true);
769
+ setIsWatching(false);
770
+ };
771
+ const onWatchPromptResolved = () => setIsWatchPrompt(false);
766
772
  kanbanEmitter.on("work:empty", onEmpty);
767
773
  kanbanEmitter.on("work:complete", onComplete);
768
774
  kanbanEmitter.on("work:watching", onWatching);
769
775
  kanbanEmitter.on("work:watch-resume", onWatchResume);
776
+ kanbanEmitter.on("work:watch-prompt", onWatchPrompt);
777
+ kanbanEmitter.on("work:watch-prompt-resolved", onWatchPromptResolved);
770
778
  const cleanupBell = registerBellListeners(bellEnabled);
771
779
  return () => {
772
780
  kanbanEmitter.off("issue:queued", onQueued);
@@ -784,13 +792,15 @@ function useKanbanState(bellEnabled) {
784
792
  kanbanEmitter.off("work:complete", onComplete);
785
793
  kanbanEmitter.off("work:watching", onWatching);
786
794
  kanbanEmitter.off("work:watch-resume", onWatchResume);
795
+ kanbanEmitter.off("work:watch-prompt", onWatchPrompt);
796
+ kanbanEmitter.off("work:watch-prompt-resolved", onWatchPromptResolved);
787
797
  cleanupBell();
788
798
  for (const issueId of activePolls.keys()) {
789
799
  stopMergePolling(issueId);
790
800
  }
791
801
  };
792
802
  }, [bellEnabled]);
793
- return { cards, isEmpty, isWatching, workComplete, modelInUse };
803
+ return { cards, isEmpty, isWatching, isWatchPrompt, workComplete, modelInUse };
794
804
  }
795
805
 
796
806
  export {
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  ok,
33
33
  setOutputMode,
34
34
  warn
35
- } from "./chunk-Z6CNJAZF.js";
35
+ } from "./chunk-WZIPTRJL.js";
36
36
  import {
37
37
  notify,
38
38
  resetTitle,
@@ -363,6 +363,18 @@ function determineRepoPath(repos, issue2, workspace) {
363
363
  const first = repos[0];
364
364
  return first ? join(workspace, first.path) : void 0;
365
365
  }
366
+ async function hasCodeChanges(repoPath, baseBranch) {
367
+ try {
368
+ const { stdout } = await execa("git", ["diff", "--stat", `${baseBranch}..HEAD`], {
369
+ cwd: repoPath,
370
+ reject: false
371
+ });
372
+ const trimmed = stdout.trim();
373
+ return trimmed.length > 0;
374
+ } catch {
375
+ return false;
376
+ }
377
+ }
366
378
 
367
379
  // src/providers/aider.ts
368
380
  import { execSync } from "child_process";
@@ -1263,7 +1275,8 @@ var ELIGIBLE_ERROR_PATTERNS = [
1263
1275
  /command not found/i,
1264
1276
  /lisa-overseer/i,
1265
1277
  /named models unavailable/i,
1266
- /free plans can only use/i
1278
+ /free plans can only use/i,
1279
+ /empty commit/i
1267
1280
  ];
1268
1281
  function isEligibleForFallback(output) {
1269
1282
  return ELIGIBLE_ERROR_PATTERNS.some((pattern) => pattern.test(output));
@@ -3686,6 +3699,7 @@ var userKilledSet = /* @__PURE__ */ new Set();
3686
3699
  var userSkippedSet = /* @__PURE__ */ new Set();
3687
3700
  var _shuttingDown = false;
3688
3701
  var _loopPaused = false;
3702
+ var _userQuitFromWatchPrompt = false;
3689
3703
  function isShuttingDown() {
3690
3704
  return _shuttingDown;
3691
3705
  }
@@ -3695,6 +3709,9 @@ function setShuttingDown(value) {
3695
3709
  function isLoopPaused() {
3696
3710
  return _loopPaused;
3697
3711
  }
3712
+ function hasUserQuitFromWatchPrompt() {
3713
+ return _userQuitFromWatchPrompt;
3714
+ }
3698
3715
  function killProviderForIssue(issueId) {
3699
3716
  const pid = activeProviderPids.get(issueId);
3700
3717
  if (!pid) return;
@@ -3792,6 +3809,10 @@ function setupEventListeners() {
3792
3809
  }
3793
3810
  }
3794
3811
  });
3812
+ kanbanEmitter.on("loop:quit", () => {
3813
+ _userQuitFromWatchPrompt = true;
3814
+ setShuttingDown(true);
3815
+ });
3795
3816
  }
3796
3817
 
3797
3818
  // src/loop/helpers.ts
@@ -6003,6 +6024,34 @@ ${result.output}
6003
6024
  cleanupManifest(workspace, issue2.id);
6004
6025
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
6005
6026
  }
6027
+ const hasChanges = await hasCodeChanges(repoPath, _defaultBranch);
6028
+ if (!hasChanges) {
6029
+ error(
6030
+ `Provider reported success but no code changes detected. Treating as failure for ${issue2.id}.`
6031
+ );
6032
+ cleanupManifest(workspace, issue2.id);
6033
+ const emptyCommitResult = {
6034
+ success: false,
6035
+ output: "Provider reported success but no code changes detected",
6036
+ duration: result.duration,
6037
+ providerUsed: result.providerUsed,
6038
+ attempts: [
6039
+ {
6040
+ provider: result.providerUsed,
6041
+ model: "",
6042
+ success: false,
6043
+ error: "Eligible error (empty commit)",
6044
+ duration: result.duration
6045
+ }
6046
+ ]
6047
+ };
6048
+ return {
6049
+ success: false,
6050
+ providerUsed: result.providerUsed,
6051
+ prUrls: [],
6052
+ fallback: emptyCommitResult
6053
+ };
6054
+ }
6006
6055
  const manifest = readLisaManifest(workspace, issue2.id);
6007
6056
  cleanupManifest(workspace, issue2.id);
6008
6057
  let prUrl = manifest?.prUrl;
@@ -6131,6 +6180,34 @@ ${result.output}
6131
6180
  await cleanupWorktree(repoPath, worktreePath);
6132
6181
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
6133
6182
  }
6183
+ const hasChanges = await hasCodeChanges(worktreePath, baseBranch);
6184
+ if (!hasChanges) {
6185
+ error(
6186
+ `Provider reported success but no code changes detected. Treating as failure for ${issue2.id}.`
6187
+ );
6188
+ await cleanupWorktree(repoPath, worktreePath);
6189
+ const emptyCommitResult = {
6190
+ success: false,
6191
+ output: "Provider reported success but no code changes detected",
6192
+ duration: result.duration,
6193
+ providerUsed: result.providerUsed,
6194
+ attempts: [
6195
+ {
6196
+ provider: result.providerUsed,
6197
+ model: "",
6198
+ success: false,
6199
+ error: "Eligible error (empty commit)",
6200
+ duration: result.duration
6201
+ }
6202
+ ]
6203
+ };
6204
+ return {
6205
+ success: false,
6206
+ providerUsed: result.providerUsed,
6207
+ prUrls: [],
6208
+ fallback: emptyCommitResult
6209
+ };
6210
+ }
6134
6211
  const manifest = readManifestFile(manifestPath);
6135
6212
  let prUrl = manifest?.prUrl;
6136
6213
  if (!prUrl) {
@@ -6269,6 +6346,18 @@ async function runConcurrentLoop(config2, source, models, workspace, opts) {
6269
6346
  if (!issue2) {
6270
6347
  if (opts.watch) {
6271
6348
  if (activeWorkers.size === 0) {
6349
+ if (completedCount > 0) {
6350
+ ok(`All issues resolved. Prompting user to continue watching...`);
6351
+ kanbanEmitter.emit("work:watch-prompt");
6352
+ setTitle("Lisa \u2014 all resolved");
6353
+ await waitIfPaused();
6354
+ if (hasUserQuitFromWatchPrompt() || isShuttingDown()) {
6355
+ noMoreIssues = true;
6356
+ break;
6357
+ }
6358
+ kanbanEmitter.emit("work:watch-prompt-resumed");
6359
+ ok(`Resuming watch mode (polling every ${WATCH_POLL_INTERVAL_MS / 1e3}s)...`);
6360
+ }
6272
6361
  ok(
6273
6362
  `No issues ready. Watching for new issues (polling every ${WATCH_POLL_INTERVAL_MS / 1e3}s)...`
6274
6363
  );
@@ -6461,6 +6550,33 @@ ${result.output}
6461
6550
  error(`Session ${session} failed for ${issue2.id}. Check ${logFile}`);
6462
6551
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
6463
6552
  }
6553
+ const hasChanges = await hasCodeChanges(workspace, config2.base_branch);
6554
+ if (!hasChanges) {
6555
+ error(
6556
+ `Provider reported success but no code changes detected. Treating as failure for ${issue2.id}.`
6557
+ );
6558
+ const emptyCommitResult = {
6559
+ success: false,
6560
+ output: "Provider reported success but no code changes detected",
6561
+ duration: result.duration,
6562
+ providerUsed: result.providerUsed,
6563
+ attempts: [
6564
+ {
6565
+ provider: result.providerUsed,
6566
+ model: "",
6567
+ success: false,
6568
+ error: "Eligible error (empty commit)",
6569
+ duration: result.duration
6570
+ }
6571
+ ]
6572
+ };
6573
+ return {
6574
+ success: false,
6575
+ providerUsed: result.providerUsed,
6576
+ prUrls: [],
6577
+ fallback: emptyCommitResult
6578
+ };
6579
+ }
6464
6580
  const manifest = readManifestFile(manifestPath);
6465
6581
  try {
6466
6582
  unlinkSync11(manifestPath);
@@ -6556,6 +6672,23 @@ async function runSequentialLoop(config2, source, models, workspace, opts) {
6556
6672
  break;
6557
6673
  }
6558
6674
  if (opts.watch) {
6675
+ if (completedCount > 0) {
6676
+ ok(`All issues resolved. Prompting user to continue watching...`);
6677
+ kanbanEmitter.emit("work:watch-prompt");
6678
+ setTitle("Lisa \u2014 all resolved");
6679
+ await waitIfPaused();
6680
+ if (hasUserQuitFromWatchPrompt() || isShuttingDown()) {
6681
+ break;
6682
+ }
6683
+ kanbanEmitter.emit("work:watch-prompt-resumed");
6684
+ ok(`Resuming watch mode (polling every ${WATCH_POLL_INTERVAL_MS / 1e3}s)...`);
6685
+ kanbanEmitter.emit("work:watching");
6686
+ setTitle("Lisa \u2014 watching...");
6687
+ await sleep2(WATCH_POLL_INTERVAL_MS);
6688
+ kanbanEmitter.emit("work:watch-resume");
6689
+ session--;
6690
+ continue;
6691
+ }
6559
6692
  ok(
6560
6693
  `No issues ready. Watching for new issues (polling every ${WATCH_POLL_INTERVAL_MS / 1e3}s)...`
6561
6694
  );
@@ -6903,7 +7036,7 @@ var run = defineCommand5({
6903
7036
  if (isTTY) {
6904
7037
  const { render } = await import("ink");
6905
7038
  const { createElement } = await import("react");
6906
- const { KanbanApp } = await import("./kanban-VSEZ7NUK.js");
7039
+ const { KanbanApp } = await import("./kanban-ECJSRP4C.js");
6907
7040
  const demoConfig = {
6908
7041
  provider: "claude",
6909
7042
  source: "linear",
@@ -6972,7 +7105,7 @@ Add them to your ${shell} and run: source ${shell}`));
6972
7105
  if (isTTY) {
6973
7106
  const { render } = await import("ink");
6974
7107
  const { createElement } = await import("react");
6975
- const { KanbanApp } = await import("./kanban-VSEZ7NUK.js");
7108
+ const { KanbanApp } = await import("./kanban-ECJSRP4C.js");
6976
7109
  render(createElement(KanbanApp, { config: merged }), { exitOnCtrlC: false });
6977
7110
  }
6978
7111
  await runLoop(merged, {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  kanbanEmitter,
4
4
  useKanbanState
5
- } from "./chunk-Z6CNJAZF.js";
5
+ } from "./chunk-WZIPTRJL.js";
6
6
  import {
7
7
  resetTitle,
8
8
  startSpinner,
@@ -56,21 +56,6 @@ function wrapTitle(title, maxWidth) {
56
56
  const line2 = remaining.length > maxWidth ? `${remaining.slice(0, maxWidth - 1)}\u2026` : remaining;
57
57
  return [line1, line2];
58
58
  }
59
- function getLastOutputLine(outputLog, maxWidth) {
60
- if (!outputLog) return "";
61
- const ansiPattern = /\x1B(?:\[[0-?]*[ -/]*[@-~]|\].*?(?:\x07|\x1B\\))/g;
62
- const stripped = outputLog.replace(ansiPattern, "");
63
- const lines = stripped.split(/\r?\n/).map((line) => {
64
- const parts = line.split("\r");
65
- return (parts[parts.length - 1] ?? "").trim();
66
- }).filter((line) => line.length > 0);
67
- if (lines.length === 0) return "";
68
- const lastLine = lines[lines.length - 1] ?? "";
69
- if (lastLine.length > maxWidth) {
70
- return `${lastLine.slice(0, maxWidth - 1)}\u2026`;
71
- }
72
- return lastLine;
73
- }
74
59
  function Card({
75
60
  card,
76
61
  isSelected = false,
@@ -134,9 +119,7 @@ function Card({
134
119
  ] }),
135
120
  /* @__PURE__ */ jsx(Text, { bold: isSelected, dimColor: !isSelected, children: stripDoubleWidth(titleLine1).padEnd(cardWidth) }),
136
121
  /* @__PURE__ */ jsx(Text, { bold: isSelected, dimColor: !isSelected, children: stripDoubleWidth(titleLine2).padEnd(cardWidth) }),
137
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: stripDoubleWidth(
138
- card.column === "in_progress" ? getLastOutputLine(card.outputLog, cardWidth) : ""
139
- ).padEnd(cardWidth) }),
122
+ /* @__PURE__ */ jsx(Text, { children: " ".repeat(cardWidth) }),
140
123
  card.column === "in_progress" ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", marginTop: 0, children: [
141
124
  isPausedInProgress ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u23F8" }) : /* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
142
125
  /* @__PURE__ */ jsx(Text, { color: isPausedInProgress ? "gray" : "yellow", dimColor: isPausedInProgress, children: elapsedMs !== null ? ` ${formatElapsed(elapsedMs)}` : "" })
@@ -220,7 +203,6 @@ function Column({
220
203
  const hiddenBelow = Math.max(0, sortedCards.length - scrollOffset - visibleCount);
221
204
  const borderColor = isFocused ? "yellow" : "gray";
222
205
  const headerColor = isFocused ? "yellow" : "white";
223
- const runningCount = cards.filter((c) => c.column === "in_progress").length;
224
206
  const errorCount = cards.filter((c) => c.hasError).length;
225
207
  return /* @__PURE__ */ jsxs2(
226
208
  Box2,
@@ -241,7 +223,6 @@ function Column({
241
223
  ] }),
242
224
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
243
225
  errorCount > 0 && /* @__PURE__ */ jsx2(Text2, { color: "red", bold: true, children: `\u2716${errorCount} ` }),
244
- runningCount > 0 && /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: `\u25CF${runningCount} ` }),
245
226
  /* @__PURE__ */ jsx2(Text2, { color: headerColor, children: `[${cards.length}]` })
246
227
  ] })
247
228
  ] }),
@@ -268,7 +249,7 @@ function Column({
268
249
  }
269
250
 
270
251
  // src/ui/board.tsx
271
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
252
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
272
253
  function formatDuration(ms) {
273
254
  const totalSeconds = Math.floor(ms / 1e3);
274
255
  const minutes = Math.floor(totalSeconds / 60);
@@ -281,6 +262,7 @@ function Board({
281
262
  labels,
282
263
  isEmpty,
283
264
  isWatching = false,
265
+ isWatchPrompt = false,
284
266
  workComplete,
285
267
  activeColIndex = 0,
286
268
  activeCardIndex = 0,
@@ -303,7 +285,36 @@ function Board({
303
285
  /* @__PURE__ */ jsx3(Box3, { height: 1 }),
304
286
  /* @__PURE__ */ jsx3(Text3, { color: "white", dimColor: true, children: "Polling every 60s for new issues with the ready label." }),
305
287
  /* @__PURE__ */ jsx3(Box3, { height: 1 }),
306
- /* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Press Ctrl+C to stop" })
288
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Press [q] to quit" })
289
+ ]
290
+ }
291
+ ) });
292
+ }
293
+ if (isWatchPrompt) {
294
+ return /* @__PURE__ */ jsx3(Box3, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs3(
295
+ Box3,
296
+ {
297
+ flexDirection: "column",
298
+ borderStyle: "single",
299
+ borderColor: "green",
300
+ paddingX: 3,
301
+ paddingY: 1,
302
+ children: [
303
+ workComplete && /* @__PURE__ */ jsxs3(Fragment, { children: [
304
+ /* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: `\u25C8 ${workComplete.total} issue${workComplete.total !== 1 ? "s" : ""} resolved` }),
305
+ /* @__PURE__ */ jsx3(Box3, { height: 1 })
306
+ ] }),
307
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "\u25CE CONTINUE WATCHING?" }),
308
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
309
+ /* @__PURE__ */ jsx3(Text3, { color: "white", dimColor: true, children: "All issues have been processed." }),
310
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
311
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
312
+ "[",
313
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "w" }),
314
+ "] Watch / [",
315
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "q" }),
316
+ "] Quit"
317
+ ] })
307
318
  ]
308
319
  }
309
320
  ) });
@@ -655,7 +666,7 @@ function Sidebar({
655
666
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
656
667
  function KanbanApp({ config }) {
657
668
  const { exit } = useApp();
658
- const { cards, isEmpty, isWatching, workComplete, modelInUse } = useKanbanState(
669
+ const { cards, isEmpty, isWatching, isWatchPrompt, workComplete, modelInUse } = useKanbanState(
659
670
  config.bell ?? true
660
671
  );
661
672
  const { rows } = useTerminalSize();
@@ -712,6 +723,18 @@ function KanbanApp({ config }) {
712
723
  setActiveCardIndex(newCardIndex);
713
724
  }, [cards, selectedCardId, activeView]);
714
725
  useInput2((input, key) => {
726
+ if (isWatchPrompt) {
727
+ if (input === "w") {
728
+ kanbanEmitter.emit("work:watch-prompt-resolved");
729
+ kanbanEmitter.emit("loop:resume");
730
+ return;
731
+ }
732
+ if (input === "q") {
733
+ kanbanEmitter.emit("loop:quit");
734
+ return;
735
+ }
736
+ return;
737
+ }
715
738
  if (input === "q") {
716
739
  process.emit("SIGINT");
717
740
  return;
@@ -810,6 +833,7 @@ function KanbanApp({ config }) {
810
833
  labels,
811
834
  isEmpty,
812
835
  isWatching,
836
+ isWatchPrompt,
813
837
  workComplete,
814
838
  activeColIndex,
815
839
  activeCardIndex,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.14.2",
3
+ "version": "1.15.0",
4
4
  "description": "Autonomous issue resolver",
5
5
  "keywords": [
6
6
  "loop",