@tarcisiopgs/lisa 1.26.2 → 1.28.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.
package/README.md CHANGED
@@ -45,6 +45,7 @@ If something fails — pre-push hooks, quota limits, stuck processes — Lisa ha
45
45
  - **Real-time TUI** — Kanban board with live provider output, keyboard controls, PR merge detection
46
46
  - **Self-healing** — orphan recovery on startup, push failure retry, stuck process detection
47
47
  - **Guardrails** — past failures are injected into future prompts to avoid repeating mistakes
48
+ - **AI planning** — `lisa plan` decomposes goals into atomic issues with dependencies, creates them in your tracker
48
49
  - **Project context** — auto-generates `.lisa/context.md` with your stack, conventions, and constraints
49
50
 
50
51
  ## Providers
@@ -89,12 +90,18 @@ lisa run --watch # poll for new issues after queue empties
89
90
  lisa run --concurrency 3 # process 3 issues in parallel
90
91
  lisa run --issue INT-42 # process a specific issue
91
92
  lisa run --limit 5 # stop after 5 issues
93
+ lisa plan "Add rate limiting" # decompose goal into issues via AI
94
+ lisa plan --issue EPIC-123 # decompose existing issue into sub-issues
95
+ lisa plan --continue # resume interrupted plan
92
96
  lisa init # create .lisa/config.yaml interactively
93
97
  lisa status # show session stats
98
+ lisa doctor # diagnose setup issues (config, provider, env, git)
94
99
  lisa context refresh # regenerate project context
95
100
  lisa feedback --pr URL # inject PR review feedback into guardrails
96
101
  ```
97
102
 
103
+ Append `--json` to any command for machine-readable output. Use `--verbose` / `--quiet` to control log verbosity.
104
+
98
105
  ## Configuration
99
106
 
100
107
  Config lives in `.lisa/config.yaml`. Run `lisa init` to create it interactively.
@@ -235,15 +242,24 @@ Acceptance criteria:
235
242
 
236
243
  ## TUI
237
244
 
238
- The real-time Kanban board shows issue progress, streams provider output, and detects PR merges.
245
+ The real-time Kanban board shows issue progress, streams provider output, and detects PR merges. The sidebar legend updates contextually — only the shortcuts active in the current view are shown.
246
+
247
+ **Board view**
239
248
 
240
249
  | Key | Action | Key | Action |
241
250
  |-----|--------|-----|--------|
242
251
  | `←` `→` | Switch columns | `p` | Pause / resume provider |
243
- | `↑` `↓` | Navigate cards | `k` | Kill current issue |
244
- | `↵` | Open detail view | `s` | Skip current issue |
245
- | `Esc` | Back to board | `o` | Open PR in browser |
246
- | `q` | Quit | | |
252
+ | `1` `2` `3` | Jump to column | `k` | Kill current issue |
253
+ | `↑` `↓` | Navigate cards | `s` | Skip current issue |
254
+ | `↵` | Open detail view | `q` | Quit |
255
+
256
+ **Detail view**
257
+
258
+ | Key | Action |
259
+ |-----|--------|
260
+ | `↑` `↓` | Scroll output log |
261
+ | `o` | Open PR in browser |
262
+ | `Esc` | Back to board |
247
263
 
248
264
  ## License
249
265
 
@@ -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
+ };
@@ -81,7 +81,7 @@ ${newEntryText}`;
81
81
 
82
82
  ${rotated.join("\n\n")}`;
83
83
  }
84
- writeFileSync(path, content, "utf-8");
84
+ writeFileSync(path, content, { encoding: "utf-8", mode: 384 });
85
85
  }
86
86
  function appendRawEntry(dir, entryText) {
87
87
  writeLock = writeLock.then(() => appendRawEntrySync(dir, entryText)).catch(() => {
@@ -108,7 +108,7 @@ ${entryText}`;
108
108
 
109
109
  ${rotated.join("\n\n")}`;
110
110
  }
111
- writeFileSync(path, content, "utf-8");
111
+ writeFileSync(path, content, { encoding: "utf-8", mode: 384 });
112
112
  }
113
113
  function formatEntry(entry) {
114
114
  return [
@@ -1,106 +1,15 @@
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";
5
- import { existsSync, readdirSync, readFileSync } from "fs";
9
+ 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/git/github.ts
11
- import { execa } from "execa";
12
-
13
- // src/git/pr-body.ts
14
- 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;
15
- var AI_COAUTHOR_RE = /co-authored-by:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)/i;
16
- function stripProviderAttribution(body) {
17
- let result = body;
18
- while (true) {
19
- const sepIndex = result.lastIndexOf("\n---");
20
- if (sepIndex === -1) break;
21
- const section = result.slice(sepIndex);
22
- if (PROVIDER_ATTRIBUTION_RE.test(section) || AI_COAUTHOR_RE.test(section)) {
23
- result = result.slice(0, sepIndex).trimEnd();
24
- } else {
25
- break;
26
- }
27
- }
28
- result = result.replace(
29
- /\n+Co-Authored-By:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)[^\n]*/gi,
30
- ""
31
- );
32
- return result.trimEnd();
33
- }
34
-
35
- // src/git/github.ts
36
- async function isGhCliAvailable() {
37
- try {
38
- await execa("gh", ["auth", "status"]);
39
- return true;
40
- } catch {
41
- return false;
42
- }
43
- }
44
- var PROVIDER_DISPLAY_NAMES = {
45
- claude: "Claude Code",
46
- gemini: "Gemini CLI",
47
- opencode: "OpenCode",
48
- copilot: "GitHub Copilot CLI",
49
- cursor: "Cursor Agent",
50
- goose: "Goose",
51
- aider: "Aider",
52
- codex: "OpenAI Codex"
53
- };
54
- function formatProviderName(providerUsed) {
55
- const providerKey = providerUsed.split("/")[0] ?? providerUsed;
56
- return PROVIDER_DISPLAY_NAMES[providerKey] ?? providerKey;
57
- }
58
- async function deleteProviderComments(prUrl) {
59
- try {
60
- const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
61
- if (!match) return;
62
- const [, owner, repo, prNumber] = match;
63
- const { stdout } = await execa("gh", [
64
- "api",
65
- "--paginate",
66
- "--jq",
67
- ".[]",
68
- `/repos/${owner}/${repo}/issues/${prNumber}/comments`
69
- ]);
70
- const comments = stdout.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
71
- for (const comment of comments) {
72
- if (PROVIDER_ATTRIBUTION_RE.test(comment.body)) {
73
- try {
74
- await execa("gh", [
75
- "api",
76
- "--method",
77
- "DELETE",
78
- `/repos/${owner}/${repo}/issues/comments/${comment.id}`
79
- ]);
80
- } catch {
81
- }
82
- }
83
- }
84
- } catch {
85
- }
86
- }
87
- async function appendPrAttribution(prUrl, providerUsed) {
88
- await deleteProviderComments(prUrl);
89
- try {
90
- const { stdout: bodyJson } = await execa("gh", ["pr", "view", prUrl, "--json", "body"]);
91
- const { body } = JSON.parse(bodyJson);
92
- const providerName = formatProviderName(providerUsed);
93
- const attribution = `
94
-
95
- ---
96
- \u{1F916} Resolved by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerName}**`;
97
- const newBody = stripProviderAttribution(body ?? "") + attribution;
98
- await execa("gh", ["pr", "edit", prUrl, "--body", newBody]);
99
- } catch {
100
- }
101
- }
102
-
103
- // src/cli/detection.ts
104
13
  function getVersion() {
105
14
  try {
106
15
  const pkgPath = resolvePath(new URL(".", import.meta.url).pathname, "../package.json");
@@ -112,7 +21,7 @@ function getVersion() {
112
21
  }
113
22
  var CURSOR_FREE_PLAN_ERROR = "Free plans can only use Auto";
114
23
  async function isCursorFreePlan() {
115
- const { mkdtempSync, unlinkSync, writeFileSync } = await import("fs");
24
+ const { mkdtempSync, writeFileSync } = await import("fs");
116
25
  const tmpDir = mkdtempSync(join(tmpdir(), "lisa-cursor-check-"));
117
26
  const promptFile = join(tmpDir, "prompt.txt");
118
27
  writeFileSync(promptFile, "test", "utf-8");
@@ -133,15 +42,11 @@ async function isCursorFreePlan() {
133
42
  });
134
43
  return output.includes(CURSOR_FREE_PLAN_ERROR);
135
44
  } catch (err) {
136
- const errorOutput = err instanceof Error ? err.message : String(err);
45
+ const errorOutput = formatError(err);
137
46
  return errorOutput.includes(CURSOR_FREE_PLAN_ERROR);
138
47
  } finally {
139
48
  try {
140
- unlinkSync(promptFile);
141
- } catch {
142
- }
143
- try {
144
- execSync(`rm -rf ${tmpDir}`, { stdio: "ignore" });
49
+ rmSync(tmpDir, { recursive: true, force: true });
145
50
  } catch {
146
51
  }
147
52
  }
@@ -343,9 +248,6 @@ async function getMissingEnvVars(source) {
343
248
  }
344
249
 
345
250
  export {
346
- stripProviderAttribution,
347
- isGhCliAvailable,
348
- appendPrAttribution,
349
251
  getVersion,
350
252
  isCursorFreePlan,
351
253
  fetchCursorModels,