@tarcisiopgs/lisa 1.27.0 → 1.28.1

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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Lisa
2
2
 
3
3
  <p align="center">
4
- <strong>Label an issue. Walk away. Come back to a PR.</strong>
4
+ <strong>Plan issues. Run agents. Get PRs.</strong>
5
5
  </p>
6
6
 
7
7
  <p align="center">
@@ -15,23 +15,23 @@
15
15
  <img src="assets/demo.gif" alt="Lisa demo" />
16
16
  </p>
17
17
 
18
- Lisa connects your issue tracker to an AI coding agent and delivers pull requests — autonomously. Tag an issue with a label, Lisa picks it up, implements it, opens a PR, and updates your board. No babysitting.
18
+ Lisa connects your issue tracker to an AI coding agent and delivers pull requests — autonomously. Describe a goal, Lisa decomposes it into issues, picks them up, implements each one, opens PRs, and updates your board. No babysitting.
19
19
 
20
20
  ## Quickstart
21
21
 
22
22
  ```bash
23
23
  npm install -g @tarcisiopgs/lisa
24
24
  lisa init # interactive setup wizard
25
- lisa run
25
+ lisa # start the agent loop
26
26
  ```
27
27
 
28
28
  ## How It Works
29
29
 
30
30
  ```
31
- Fetch issue ActivateBuild context → Implement → Push → Open PR → Update board → Next
31
+ PlanCreate issues Fetch → Implement → Push → Open PR → Update board → Next
32
32
  ```
33
33
 
34
- Lisa picks the highest-priority labeled issue, moves it to "In Progress", sends a structured prompt to the AI agent, and monitors execution. The agent works in an isolated git worktree, implements the change, runs tests, and commits. Lisa pushes, opens a PR, moves the ticket to "In Review", and picks up the next one.
34
+ Lisa starts and shows a Kanban board. If the queue is empty, press `n` to plan — describe a goal and the AI decomposes it into atomic issues, created directly in your tracker. Press `r` to start processing. Lisa picks the highest-priority labeled issue, moves it to "In Progress", sends a structured prompt to the AI agent, and monitors execution. The agent works in an isolated git worktree, implements the change, runs tests, and commits. Lisa pushes, opens a PR, moves the ticket to "In Review", and picks up the next one.
35
35
 
36
36
  If something fails — pre-push hooks, quota limits, stuck processes — Lisa handles it: retries with error context, falls back to the next model, or kills and moves on.
37
37
 
@@ -39,10 +39,11 @@ If something fails — pre-push hooks, quota limits, stuck processes — Lisa ha
39
39
 
40
40
  - **7 issue trackers** — Linear, GitHub Issues, GitLab Issues, Jira, Trello, Plane, Shortcut
41
41
  - **8 AI agents** — Claude Code, Gemini CLI, GitHub Copilot CLI, Cursor Agent, Aider, Goose, OpenCode, Codex
42
+ - **AI planning** — describe a goal, the AI decomposes it into issues with dependencies, created in your tracker
42
43
  - **Concurrent execution** — process multiple issues in parallel, each in its own worktree
43
44
  - **Multi-repo** — plans across repos, creates one PR per repo in the correct order
44
45
  - **Model fallback** — chain models; transient errors (429, quota, timeout) auto-switch to the next
45
- - **Real-time TUI** — Kanban board with live provider output, keyboard controls, PR merge detection
46
+ - **Real-time TUI** — Kanban board with live provider output, plan mode, PR merge detection
46
47
  - **Self-healing** — orphan recovery on startup, push failure retry, stuck process detection
47
48
  - **Guardrails** — past failures are injected into future prompts to avoid repeating mistakes
48
49
  - **Project context** — auto-generates `.lisa/context.md` with your stack, conventions, and constraints
@@ -82,18 +83,21 @@ provider_options:
82
83
  ## Commands
83
84
 
84
85
  ```bash
85
- lisa run # start the agent loop
86
- lisa run --once # process a single issue
87
- lisa run --once --dry-run # preview config without executing
88
- lisa run --watch # poll for new issues after queue empties
89
- lisa run --concurrency 3 # process 3 issues in parallel
90
- lisa run --issue INT-42 # process a specific issue
91
- lisa run --limit 5 # stop after 5 issues
92
- lisa init # create .lisa/config.yaml interactively
93
- lisa status # show session stats
94
- lisa doctor # diagnose setup issues (config, provider, env, git)
95
- lisa context refresh # regenerate project context
96
- lisa feedback --pr URL # inject PR review feedback into guardrails
86
+ lisa # start the agent loop (Kanban TUI)
87
+ lisa --once # process a single issue
88
+ lisa --once --dry-run # preview config without executing
89
+ lisa --watch # poll for new issues after queue empties
90
+ lisa -c 3 # process 3 issues in parallel
91
+ lisa --issue INT-42 # process a specific issue
92
+ lisa --limit 5 # stop after 5 issues
93
+ lisa plan "Add rate limiting" # decompose goal into issues via AI (CLI mode)
94
+ lisa plan --issue EPIC-123 # decompose existing issue into sub-issues
95
+ lisa plan --continue # resume interrupted plan
96
+ lisa init # create .lisa/config.yaml interactively
97
+ lisa status # show session stats
98
+ lisa doctor # diagnose setup issues (config, provider, env, git)
99
+ lisa context refresh # regenerate project context
100
+ lisa feedback --pr URL # inject PR review feedback into guardrails
97
101
  ```
98
102
 
99
103
  Append `--json` to any command for machine-readable output. Use `--verbose` / `--quiet` to control log verbosity.
@@ -238,16 +242,16 @@ Acceptance criteria:
238
242
 
239
243
  ## TUI
240
244
 
241
- The real-time Kanban board shows issue progress, streams provider output, and detects PR merges. The sidebar legend updates contextuallyonly the shortcuts active in the current view are shown.
245
+ The real-time Kanban board shows issue progress, streams provider output, and detects PR merges. When the queue is empty, Lisa enters idle mode plan new issues with `n`, then start processing with `r`.
242
246
 
243
247
  **Board view**
244
248
 
245
249
  | Key | Action | Key | Action |
246
250
  |-----|--------|-----|--------|
247
- | `←` `→` | Switch columns | `p` | Pause / resume provider |
248
- | `1` `2` `3` | Jump to column | `k` | Kill current issue |
249
- | `↑` `↓` | Navigate cards | `s` | Skip current issue |
250
- | `↵` | Open detail view | `q` | Quit |
251
+ | `←` `→` | Switch columns | `k` | Kill current issue |
252
+ | `↑` `↓` | Navigate cards | `n` | Open plan mode |
253
+ | `↵` | Open detail view | `r` | Run (from idle) |
254
+ | `p` | Pause / resume | `q` | Quit |
251
255
 
252
256
  **Detail view**
253
257
 
@@ -257,6 +261,16 @@ The real-time Kanban board shows issue progress, streams provider output, and de
257
261
  | `o` | Open PR in browser |
258
262
  | `Esc` | Back to board |
259
263
 
264
+ **Plan mode**
265
+
266
+ | Key | Action |
267
+ |-----|--------|
268
+ | `↵` | Send message / view detail |
269
+ | `e` | Edit issue in $EDITOR |
270
+ | `d` | Delete issue |
271
+ | `a` | Approve and create issues |
272
+ | `Esc` | Cancel / back |
273
+
260
274
  ## License
261
275
 
262
276
  [MIT](LICENSE)
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/git/github.ts
4
+ import { execa } from "execa";
5
+
6
+ // src/git/pr-body.ts
7
+ var PROVIDER_ATTRIBUTION_RE = /claude\.ai|claude\s+code|\banthropic\b|gemini\s+cli|\bgoogle\s+gemini\b|openai\s+codex|\bopenai\b|\bgoose\b|\baider\b|github\s+copilot|cursor\s+agent|\bopencode\b|\blisa\b/i;
8
+ var AI_COAUTHOR_RE = /co-authored-by:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)/i;
9
+ function stripProviderAttribution(body) {
10
+ let result = body;
11
+ while (true) {
12
+ const sepIndex = result.lastIndexOf("\n---");
13
+ if (sepIndex === -1) break;
14
+ const section = result.slice(sepIndex);
15
+ if (PROVIDER_ATTRIBUTION_RE.test(section) || AI_COAUTHOR_RE.test(section)) {
16
+ result = result.slice(0, sepIndex).trimEnd();
17
+ } else {
18
+ break;
19
+ }
20
+ }
21
+ result = result.replace(
22
+ /\n+Co-Authored-By:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)[^\n]*/gi,
23
+ ""
24
+ );
25
+ return result.trimEnd();
26
+ }
27
+
28
+ // src/git/github.ts
29
+ async function isGhCliAvailable() {
30
+ try {
31
+ await execa("gh", ["auth", "status"]);
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ var PROVIDER_DISPLAY_NAMES = {
38
+ claude: "Claude Code",
39
+ gemini: "Gemini CLI",
40
+ opencode: "OpenCode",
41
+ copilot: "GitHub Copilot CLI",
42
+ cursor: "Cursor Agent",
43
+ goose: "Goose",
44
+ aider: "Aider",
45
+ codex: "OpenAI Codex"
46
+ };
47
+ function formatProviderName(providerUsed) {
48
+ const providerKey = providerUsed.split("/")[0] ?? providerUsed;
49
+ return PROVIDER_DISPLAY_NAMES[providerKey] ?? providerKey;
50
+ }
51
+ async function deleteProviderComments(prUrl) {
52
+ try {
53
+ const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
54
+ if (!match) return;
55
+ const [, owner, repo, prNumber] = match;
56
+ const { stdout } = await execa("gh", [
57
+ "api",
58
+ "--paginate",
59
+ "--jq",
60
+ ".[]",
61
+ `/repos/${owner}/${repo}/issues/${prNumber}/comments`
62
+ ]);
63
+ const comments = stdout.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
64
+ for (const comment of comments) {
65
+ if (PROVIDER_ATTRIBUTION_RE.test(comment.body)) {
66
+ try {
67
+ await execa("gh", [
68
+ "api",
69
+ "--method",
70
+ "DELETE",
71
+ `/repos/${owner}/${repo}/issues/comments/${comment.id}`
72
+ ]);
73
+ } catch {
74
+ }
75
+ }
76
+ }
77
+ } catch {
78
+ }
79
+ }
80
+ async function appendPrAttribution(prUrl, providerUsed) {
81
+ await deleteProviderComments(prUrl);
82
+ try {
83
+ const { stdout: bodyJson } = await execa("gh", ["pr", "view", prUrl, "--json", "body"]);
84
+ const { body } = JSON.parse(bodyJson);
85
+ const providerName = formatProviderName(providerUsed);
86
+ const attribution = `
87
+
88
+ ---
89
+ \u{1F916} Resolved by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerName}**`;
90
+ const newBody = stripProviderAttribution(body ?? "") + attribution;
91
+ await execa("gh", ["pr", "edit", prUrl, "--body", newBody]);
92
+ } catch {
93
+ }
94
+ }
95
+ async function appendPrBody(prUrl, content) {
96
+ try {
97
+ const { stdout: bodyJson } = await execa("gh", ["pr", "view", prUrl, "--json", "body"]);
98
+ const { body } = JSON.parse(bodyJson);
99
+ const newBody = (body ?? "") + content;
100
+ await execa("gh", ["pr", "edit", prUrl, "--body", newBody]);
101
+ } catch {
102
+ }
103
+ }
104
+
105
+ // src/errors.ts
106
+ function formatError(err) {
107
+ return err instanceof Error ? err.message : String(err);
108
+ }
109
+
110
+ export {
111
+ stripProviderAttribution,
112
+ isGhCliAvailable,
113
+ appendPrAttribution,
114
+ appendPrBody,
115
+ formatError
116
+ };
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ formatError,
4
+ isGhCliAvailable
5
+ } from "./chunk-2TW2MJXF.js";
2
6
 
3
7
  // src/cli/detection.ts
4
8
  import { execSync } from "child_process";
@@ -6,115 +10,6 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
6
10
  import { tmpdir } from "os";
7
11
  import { join, resolve as resolvePath } from "path";
8
12
  import * as clack from "@clack/prompts";
9
-
10
- // src/errors.ts
11
- function formatError(err) {
12
- return err instanceof Error ? err.message : String(err);
13
- }
14
-
15
- // src/git/github.ts
16
- import { execa } from "execa";
17
-
18
- // src/git/pr-body.ts
19
- var PROVIDER_ATTRIBUTION_RE = /claude\.ai|claude\s+code|\banthropic\b|gemini\s+cli|\bgoogle\s+gemini\b|openai\s+codex|\bopenai\b|\bgoose\b|\baider\b|github\s+copilot|cursor\s+agent|\bopencode\b|\blisa\b/i;
20
- var AI_COAUTHOR_RE = /co-authored-by:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)/i;
21
- function stripProviderAttribution(body) {
22
- let result = body;
23
- while (true) {
24
- const sepIndex = result.lastIndexOf("\n---");
25
- if (sepIndex === -1) break;
26
- const section = result.slice(sepIndex);
27
- if (PROVIDER_ATTRIBUTION_RE.test(section) || AI_COAUTHOR_RE.test(section)) {
28
- result = result.slice(0, sepIndex).trimEnd();
29
- } else {
30
- break;
31
- }
32
- }
33
- result = result.replace(
34
- /\n+Co-Authored-By:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)[^\n]*/gi,
35
- ""
36
- );
37
- return result.trimEnd();
38
- }
39
-
40
- // src/git/github.ts
41
- async function isGhCliAvailable() {
42
- try {
43
- await execa("gh", ["auth", "status"]);
44
- return true;
45
- } catch {
46
- return false;
47
- }
48
- }
49
- var PROVIDER_DISPLAY_NAMES = {
50
- claude: "Claude Code",
51
- gemini: "Gemini CLI",
52
- opencode: "OpenCode",
53
- copilot: "GitHub Copilot CLI",
54
- cursor: "Cursor Agent",
55
- goose: "Goose",
56
- aider: "Aider",
57
- codex: "OpenAI Codex"
58
- };
59
- function formatProviderName(providerUsed) {
60
- const providerKey = providerUsed.split("/")[0] ?? providerUsed;
61
- return PROVIDER_DISPLAY_NAMES[providerKey] ?? providerKey;
62
- }
63
- async function deleteProviderComments(prUrl) {
64
- try {
65
- const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
66
- if (!match) return;
67
- const [, owner, repo, prNumber] = match;
68
- const { stdout } = await execa("gh", [
69
- "api",
70
- "--paginate",
71
- "--jq",
72
- ".[]",
73
- `/repos/${owner}/${repo}/issues/${prNumber}/comments`
74
- ]);
75
- const comments = stdout.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
76
- for (const comment of comments) {
77
- if (PROVIDER_ATTRIBUTION_RE.test(comment.body)) {
78
- try {
79
- await execa("gh", [
80
- "api",
81
- "--method",
82
- "DELETE",
83
- `/repos/${owner}/${repo}/issues/comments/${comment.id}`
84
- ]);
85
- } catch {
86
- }
87
- }
88
- }
89
- } catch {
90
- }
91
- }
92
- async function appendPrAttribution(prUrl, providerUsed) {
93
- await deleteProviderComments(prUrl);
94
- try {
95
- const { stdout: bodyJson } = await execa("gh", ["pr", "view", prUrl, "--json", "body"]);
96
- const { body } = JSON.parse(bodyJson);
97
- const providerName = formatProviderName(providerUsed);
98
- const attribution = `
99
-
100
- ---
101
- \u{1F916} Resolved by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerName}**`;
102
- const newBody = stripProviderAttribution(body ?? "") + attribution;
103
- await execa("gh", ["pr", "edit", prUrl, "--body", newBody]);
104
- } catch {
105
- }
106
- }
107
- async function appendPrBody(prUrl, content) {
108
- try {
109
- const { stdout: bodyJson } = await execa("gh", ["pr", "view", prUrl, "--json", "body"]);
110
- const { body } = JSON.parse(bodyJson);
111
- const newBody = (body ?? "") + content;
112
- await execa("gh", ["pr", "edit", prUrl, "--body", newBody]);
113
- } catch {
114
- }
115
- }
116
-
117
- // src/cli/detection.ts
118
13
  function getVersion() {
119
14
  try {
120
15
  const pkgPath = resolvePath(new URL(".", import.meta.url).pathname, "../package.json");
@@ -353,11 +248,6 @@ async function getMissingEnvVars(source) {
353
248
  }
354
249
 
355
250
  export {
356
- stripProviderAttribution,
357
- isGhCliAvailable,
358
- appendPrAttribution,
359
- appendPrBody,
360
- formatError,
361
251
  getVersion,
362
252
  isCursorFreePlan,
363
253
  fetchCursorModels,
@@ -3,13 +3,6 @@ import {
3
3
  notify
4
4
  } from "./chunk-72CYGBT4.js";
5
5
 
6
- // src/ui/state.ts
7
- import { EventEmitter } from "events";
8
- import { useEffect, useState } from "react";
9
-
10
- // src/sources/github-issues.ts
11
- import { execa } from "execa";
12
-
13
6
  // src/output/logger.ts
14
7
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
15
8
  import { dirname } from "path";
@@ -100,6 +93,13 @@ function updateNotice(update) {
100
93
  `));
101
94
  }
102
95
 
96
+ // src/ui/state.ts
97
+ import { EventEmitter } from "events";
98
+ import { useEffect, useState } from "react";
99
+
100
+ // src/sources/github-issues.ts
101
+ import { execa } from "execa";
102
+
103
103
  // src/sources/base.ts
104
104
  var REQUEST_TIMEOUT_MS = 3e4;
105
105
  function normalizeLabels(config) {
@@ -425,6 +425,16 @@ var GitHubIssuesSource = class {
425
425
  } catch {
426
426
  }
427
427
  }
428
+ async createIssue(opts, config) {
429
+ const { owner, repo } = parseOwnerRepo(config.scope);
430
+ const labels = Array.isArray(opts.label) ? opts.label : [opts.label];
431
+ const issue = await api().post(`/repos/${owner}/${repo}/issues`, {
432
+ title: opts.title,
433
+ body: opts.description,
434
+ labels
435
+ });
436
+ return makeIssueId(owner, repo, issue.number);
437
+ }
428
438
  };
429
439
 
430
440
  // src/sources/gitlab-issues.ts
@@ -642,6 +652,30 @@ var GitLabIssuesSource = class {
642
652
  labels: filtered.join(",")
643
653
  });
644
654
  }
655
+ async createIssue(opts, config) {
656
+ const encodedProject = parseGitLabProject(config.scope);
657
+ const labels = Array.isArray(opts.label) ? opts.label : [opts.label];
658
+ const issue = await api2().post(`/projects/${encodedProject}/issues`, {
659
+ title: opts.title,
660
+ description: opts.description,
661
+ labels: labels.join(","),
662
+ ...opts.order !== void 0 && { weight: opts.order }
663
+ });
664
+ return makeIssueId2(config.scope, issue.iid);
665
+ }
666
+ async linkDependency(issueId, dependsOnId) {
667
+ const source = splitIssueId(issueId);
668
+ const target = splitIssueId(dependsOnId);
669
+ const encodedProject = parseGitLabProject(source.project);
670
+ const projectInfo = await api2().get(
671
+ `/projects/${parseGitLabProject(target.project)}`
672
+ );
673
+ await api2().post(`/projects/${encodedProject}/issues/${source.iid}/links`, {
674
+ target_project_id: projectInfo.id,
675
+ target_issue_iid: Number(target.iid),
676
+ link_type: "is_blocked_by"
677
+ });
678
+ }
645
679
  };
646
680
  function parseGitLabProject(input) {
647
681
  if (/^\d+$/.test(input)) return input;
@@ -874,6 +908,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
874
908
  const onModelChanged = (model) => setModelInUse(model);
875
909
  kanbanEmitter.on("provider:model-changed", onModelChanged);
876
910
  const onEmpty = () => setIsEmpty(true);
911
+ const onResumed = () => setIsEmpty(false);
877
912
  const onComplete = (data) => setWorkComplete(data);
878
913
  const onWatching = () => setIsWatching(true);
879
914
  const onWatchResume = () => setIsWatching(false);
@@ -883,6 +918,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
883
918
  };
884
919
  const onWatchPromptResolved = () => setIsWatchPrompt(false);
885
920
  kanbanEmitter.on("work:empty", onEmpty);
921
+ kanbanEmitter.on("work:resumed", onResumed);
886
922
  kanbanEmitter.on("work:complete", onComplete);
887
923
  kanbanEmitter.on("work:watching", onWatching);
888
924
  kanbanEmitter.on("work:watch-resume", onWatchResume);
@@ -909,6 +945,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
909
945
  kanbanEmitter.off("issue:output", onOutput);
910
946
  kanbanEmitter.off("provider:model-changed", onModelChanged);
911
947
  kanbanEmitter.off("work:empty", onEmpty);
948
+ kanbanEmitter.off("work:resumed", onResumed);
912
949
  kanbanEmitter.off("work:complete", onComplete);
913
950
  kanbanEmitter.off("work:watching", onWatching);
914
951
  kanbanEmitter.off("work:watch-resume", onWatchResume);