@tarcisiopgs/lisa 1.27.0 → 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,6 +90,9 @@ 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
94
98
  lisa doctor # diagnose setup issues (config, provider, env, git)
@@ -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);