@tarcisiopgs/lisa 1.14.1 → 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.
- package/README.md +89 -484
- package/dist/{chunk-Z6CNJAZF.js → chunk-WZIPTRJL.js} +11 -1
- package/dist/index.js +137 -4
- package/dist/{kanban-VSEZ7NUK.js → kanban-ECJSRP4C.js} +48 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,10 +12,6 @@
|
|
|
12
12
|
<img src="assets/demo.gif?v=2" alt="Lisa demo" width="800" />
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
<p align="center">
|
|
16
|
-
Lisa is an autonomous issue resolver that turns your backlog into pull requests — no babysitting required.
|
|
17
|
-
</p>
|
|
18
|
-
|
|
19
15
|
<p align="center">
|
|
20
16
|
<a href="https://www.npmjs.com/package/@tarcisiopgs/lisa"><img src="https://img.shields.io/npm/v/@tarcisiopgs/lisa.svg" alt="npm version" /></a>
|
|
21
17
|
<a href="https://www.npmjs.com/package/@tarcisiopgs/lisa"><img src="https://img.shields.io/npm/dm/@tarcisiopgs/lisa.svg" alt="npm downloads" /></a>
|
|
@@ -29,36 +25,17 @@
|
|
|
29
25
|
|
|
30
26
|
```bash
|
|
31
27
|
npm install -g @tarcisiopgs/lisa
|
|
32
|
-
lisa init
|
|
33
|
-
lisa run
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
That's it. Lisa picks up the next labeled issue, implements it, pushes a branch, opens a pull request, and moves the ticket to "In Review" — all without you touching it.
|
|
37
|
-
|
|
38
|
-
`lisa init` offers pre-defined templates for the most common source + provider combinations (GitHub Issues + Claude, Linear + Claude, Jira + Claude, and more). Select a template to pre-fill sensible defaults, or choose "Configure manually" for full control.
|
|
39
|
-
|
|
40
|
-
## Try it safely first
|
|
41
|
-
|
|
42
|
-
Before letting Lisa touch real issues, verify your configuration with `--dry-run`. No issues will be fetched, no code will be written, no PRs will be created.
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
lisa run --once --dry-run
|
|
28
|
+
lisa init # interactive setup — picks your source + provider
|
|
29
|
+
lisa run # start resolving issues
|
|
46
30
|
```
|
|
47
31
|
|
|
48
|
-
|
|
32
|
+
Verify your setup first with `lisa run --once --dry-run` — no issues will be fetched, no code written, no PRs created.
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
[dry-run] Would fetch issue from linear (Engineering/Web App)
|
|
52
|
-
[dry-run] Workflow mode: worktree
|
|
53
|
-
[dry-run] Models priority: claude/claude-sonnet-4-6
|
|
54
|
-
[dry-run] Then implement, push, create PR, and update issue status
|
|
55
|
-
```
|
|
34
|
+
Lisa picks up the next labeled issue, implements it, pushes a branch, opens a pull request, and moves the ticket to "In Review" — all without you touching it.
|
|
56
35
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
## What Lisa Does
|
|
36
|
+
---
|
|
60
37
|
|
|
61
|
-
|
|
38
|
+
## How It Works
|
|
62
39
|
|
|
63
40
|
```
|
|
64
41
|
┌─────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌────┐ ┌────────┐
|
|
@@ -66,140 +43,29 @@ Lisa follows a deterministic pipeline:
|
|
|
66
43
|
└─────────┘ └──────────┘ └───────────┘ └──────────┘ └────┘ └────────┘
|
|
67
44
|
```
|
|
68
45
|
|
|
69
|
-
1. **Fetch** — Pulls the next issue
|
|
70
|
-
2. **
|
|
71
|
-
3. **
|
|
72
|
-
4. **
|
|
73
|
-
5. **PR** — Pushes the branch and creates a pull request (or merge request) referencing the original issue.
|
|
74
|
-
6. **Update** — Moves the issue to the `done` status and removes the pickup label.
|
|
75
|
-
7. **Next** — Picks the next issue. When there are no more matching issues, Lisa stops.
|
|
76
|
-
|
|
77
|
-
### What makes it different
|
|
78
|
-
|
|
79
|
-
- **Deterministic, not hopeful** — Each issue follows a structured pipeline with clear stages. No blind retries, no speculative loops.
|
|
80
|
-
- **Token efficiency** — Each issue gets one focused prompt. No wasted retries, no idle polling.
|
|
81
|
-
- **Multi-repo awareness** — Plans across multiple repos, executes in the correct order, creates one PR per repo.
|
|
82
|
-
- **Model fallback** — Configure a chain of models. Transient errors (429, quota, timeout) trigger the next model automatically.
|
|
83
|
-
- **Workflow integration** — Issues move through your board in real time. Your team always knows what's being worked on.
|
|
84
|
-
- **Self-healing** — Orphan issues stuck in "In Progress" are recovered on startup. Pre-push failures trigger the agent to fix and retry.
|
|
85
|
-
- **Guardrails** — Past failures are logged and injected into future prompts so the agent avoids repeating mistakes.
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
## Writing Issues
|
|
90
|
-
|
|
91
|
-
Issue quality is the single biggest factor in PR quality. Lisa validates issues before accepting them — vague tickets without clear criteria are skipped automatically (labelled `needs-spec`). A well-structured issue gives the AI agent everything it needs to deliver a clean implementation on the first try.
|
|
92
|
-
|
|
93
|
-
### What Lisa looks for
|
|
94
|
-
|
|
95
|
-
At minimum, the issue description must contain **acceptance criteria**. Lisa detects them by looking for:
|
|
96
|
-
|
|
97
|
-
- Markdown checklists (`- [ ]`)
|
|
98
|
-
- Keywords: `acceptance criteria`, `expected`, `should`, `deve`, `critérios`
|
|
99
|
-
|
|
100
|
-
Issues without these are rejected. Beyond validation, the agent also consumes **relevant files**, **technical constraints**, and **stack information** when present in the description — so including them leads to better results.
|
|
101
|
-
|
|
102
|
-
### Feature
|
|
103
|
-
|
|
104
|
-
```markdown
|
|
105
|
-
Title: Add rate limiting to /api/users endpoint
|
|
106
|
-
|
|
107
|
-
Description:
|
|
108
|
-
Implement rate limiting on the `/api/users` endpoint to prevent abuse.
|
|
109
|
-
|
|
110
|
-
Relevant files:
|
|
111
|
-
- src/routes/users.ts
|
|
112
|
-
- src/middleware/auth.ts
|
|
113
|
-
|
|
114
|
-
Acceptance criteria:
|
|
115
|
-
- [ ] Requests exceeding 100/min per IP return HTTP 429
|
|
116
|
-
- [ ] Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining) included in all responses
|
|
117
|
-
- [ ] Rate limit state stored in Redis (use existing connection from src/lib/redis.ts)
|
|
118
|
-
- [ ] Existing tests still pass
|
|
119
|
-
|
|
120
|
-
Stack: Express, Redis
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Bug fix
|
|
124
|
-
|
|
125
|
-
```markdown
|
|
126
|
-
Title: Login redirect loop when session expires
|
|
127
|
-
|
|
128
|
-
Description:
|
|
129
|
-
Users report being stuck in an infinite redirect loop after their session expires.
|
|
130
|
-
The issue is in the session middleware — it redirects to /login, but /login also
|
|
131
|
-
checks for a valid session and redirects back.
|
|
132
|
-
|
|
133
|
-
Steps to reproduce:
|
|
134
|
-
1. Log in
|
|
135
|
-
2. Wait for session to expire (or manually clear the session cookie)
|
|
136
|
-
3. Navigate to any protected route
|
|
137
|
-
|
|
138
|
-
Relevant files:
|
|
139
|
-
- src/middleware/session.ts
|
|
140
|
-
- src/routes/auth.ts
|
|
141
|
-
|
|
142
|
-
Expected behavior:
|
|
143
|
-
- [ ] Expired sessions redirect to /login without a loop
|
|
144
|
-
- [ ] /login route should not require an active session
|
|
145
|
-
- [ ] After login, user is redirected back to the originally requested page
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Refactor
|
|
149
|
-
|
|
150
|
-
```markdown
|
|
151
|
-
Title: Extract email sending into a dedicated service
|
|
152
|
-
|
|
153
|
-
Description:
|
|
154
|
-
Email sending logic is duplicated across 3 controllers. Extract it into a
|
|
155
|
-
shared service.
|
|
156
|
-
|
|
157
|
-
Relevant files:
|
|
158
|
-
- src/controllers/auth.ts (lines 45-67: welcome email)
|
|
159
|
-
- src/controllers/orders.ts (lines 120-145: order confirmation)
|
|
160
|
-
- src/controllers/support.ts (lines 30-55: ticket confirmation)
|
|
161
|
-
|
|
162
|
-
Acceptance criteria:
|
|
163
|
-
- [ ] New EmailService class in src/services/email.ts
|
|
164
|
-
- [ ] All 3 controllers use the shared service instead of inline email logic
|
|
165
|
-
- [ ] Existing tests still pass
|
|
166
|
-
- [ ] No new dependencies added
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### What makes a good issue
|
|
170
|
-
|
|
171
|
-
| Do | Don't |
|
|
172
|
-
|---|---|
|
|
173
|
-
| List specific files the agent should read | Say "fix the bug" with no context |
|
|
174
|
-
| Include acceptance criteria as a checklist | Describe the desired outcome vaguely |
|
|
175
|
-
| Mention the stack or libraries to use | Assume the agent knows your preferences |
|
|
176
|
-
| Describe expected behavior for bug fixes | Provide only steps to reproduce |
|
|
177
|
-
| Scope to a single deliverable | Bundle multiple unrelated changes |
|
|
46
|
+
1. **Fetch and activate** — Pulls the next priority-sorted issue matching your configured label and moves it to `in_progress`.
|
|
47
|
+
2. **Implement** — Builds a structured prompt with full issue context and sends it to the AI agent. The agent works in a worktree or branch, implements the change, runs tests, and commits.
|
|
48
|
+
3. **Push and PR** — Pushes the branch, creates a pull request referencing the original issue. If pre-push hooks fail, re-invokes the agent with the error output and retries.
|
|
49
|
+
4. **Update and loop** — Moves the issue to `done`, removes the pickup label, and picks the next issue. Stops when the queue is empty.
|
|
178
50
|
|
|
179
51
|
---
|
|
180
52
|
|
|
181
53
|
## Issue Sources
|
|
182
54
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
|
186
|
-
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
|
192
|
-
| Plane | `plane` | Open-source project management (cloud and self-hosted) |
|
|
193
|
-
| Shortcut | `shortcut` | Story-based tracking (formerly Clubhouse) |
|
|
194
|
-
|
|
195
|
-
Each source supports label filtering, priority ordering, and status transitions out of the box. See [Source-Specific Fields](#source-specific-fields) for configuration details.
|
|
55
|
+
| Source | Key |
|
|
56
|
+
|--------|-----|
|
|
57
|
+
| Linear | `linear` |
|
|
58
|
+
| GitHub Issues | `github-issues` |
|
|
59
|
+
| Jira | `jira` |
|
|
60
|
+
| GitLab Issues | `gitlab-issues` |
|
|
61
|
+
| Trello | `trello` |
|
|
62
|
+
| Plane | `plane` |
|
|
63
|
+
| Shortcut | `shortcut` |
|
|
196
64
|
|
|
197
65
|
---
|
|
198
66
|
|
|
199
67
|
## AI Providers
|
|
200
68
|
|
|
201
|
-
Lisa supports **8 AI coding agents** as implementation backends:
|
|
202
|
-
|
|
203
69
|
| Provider | Key | Command |
|
|
204
70
|
|----------|-----|---------|
|
|
205
71
|
| Claude Code | `claude` | `claude` |
|
|
@@ -213,13 +79,9 @@ Lisa supports **8 AI coding agents** as implementation backends:
|
|
|
213
79
|
|
|
214
80
|
At least one provider must be installed and available in your PATH.
|
|
215
81
|
|
|
216
|
-
> **Cursor Free plan** — `lisa init` automatically detects Free accounts and restricts model selection to `auto` only. On paid plans, available models are fetched live from `cursor --list-models` and filtered to a curated top-tier list (`composer-1.5`, `opus-4.6`, `sonnet-4.6`, `gpt-5.3-codex`, etc.).
|
|
217
|
-
|
|
218
|
-
> **OpenCode** — `lisa init` fetches available models from `opencode models` and filters them based on which API keys are present in your environment (`ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `OPENAI_API_KEY`, `GITHUB_TOKEN`, `GROQ_API_KEY`, `MISTRAL_API_KEY`, `DEEPSEEK_API_KEY`). Only models from providers you have credentials for are shown.
|
|
219
|
-
|
|
220
82
|
### Fallback Chain
|
|
221
83
|
|
|
222
|
-
Configure multiple models — Lisa tries each in order. Transient errors (429, quota, timeout, network) trigger the next model
|
|
84
|
+
Configure multiple models — Lisa tries each in order. Transient errors (429, quota, timeout, network) trigger the next model automatically.
|
|
223
85
|
|
|
224
86
|
```yaml
|
|
225
87
|
provider: claude
|
|
@@ -231,137 +93,62 @@ models:
|
|
|
231
93
|
|
|
232
94
|
---
|
|
233
95
|
|
|
234
|
-
## PR
|
|
96
|
+
## PR Platforms
|
|
235
97
|
|
|
236
|
-
|
|
98
|
+
| Platform | Key | Auth |
|
|
99
|
+
|----------|-----|------|
|
|
100
|
+
| GitHub CLI | `cli` | `gh auth login` |
|
|
101
|
+
| GitHub API | `token` | `GITHUB_TOKEN` |
|
|
102
|
+
| GitLab | `gitlab` | `GITLAB_TOKEN` |
|
|
103
|
+
| Bitbucket | `bitbucket` | `BITBUCKET_TOKEN` + `BITBUCKET_USERNAME` |
|
|
237
104
|
|
|
238
|
-
|
|
239
|
-
|----------|-----|------|--------|
|
|
240
|
-
| GitHub CLI | `cli` | `gh auth login` | Pull Request |
|
|
241
|
-
| GitHub API | `token` | `GITHUB_TOKEN` | Pull Request |
|
|
242
|
-
| GitLab | `gitlab` | `GITLAB_TOKEN` | Merge Request |
|
|
243
|
-
| Bitbucket | `bitbucket` | `BITBUCKET_TOKEN` + `BITBUCKET_USERNAME` | Pull Request |
|
|
105
|
+
---
|
|
244
106
|
|
|
245
|
-
|
|
107
|
+
## Environment Variables
|
|
246
108
|
|
|
247
|
-
|
|
248
|
-
platform: gitlab # or: cli, token, bitbucket
|
|
249
|
-
```
|
|
109
|
+
Set the tokens for your chosen source and PR platform:
|
|
250
110
|
|
|
251
111
|
```bash
|
|
252
|
-
|
|
112
|
+
# PR platform
|
|
113
|
+
GITHUB_TOKEN # GitHub (platform: cli or token)
|
|
114
|
+
GITLAB_TOKEN # GitLab (platform: gitlab)
|
|
115
|
+
BITBUCKET_TOKEN # Bitbucket (platform: bitbucket)
|
|
116
|
+
BITBUCKET_USERNAME # Bitbucket username
|
|
117
|
+
|
|
118
|
+
# Issue sources
|
|
119
|
+
LINEAR_API_KEY # source: linear
|
|
120
|
+
TRELLO_API_KEY # source: trello
|
|
121
|
+
TRELLO_TOKEN
|
|
122
|
+
SHORTCUT_API_TOKEN # source: shortcut
|
|
123
|
+
PLANE_API_TOKEN # source: plane
|
|
124
|
+
PLANE_BASE_URL # optional, defaults to https://api.plane.so
|
|
125
|
+
GITLAB_TOKEN # source: gitlab-issues
|
|
126
|
+
GITLAB_BASE_URL # optional, defaults to https://gitlab.com
|
|
127
|
+
GITHUB_TOKEN # source: github-issues
|
|
128
|
+
JIRA_BASE_URL # source: jira
|
|
129
|
+
JIRA_EMAIL
|
|
130
|
+
JIRA_API_TOKEN
|
|
253
131
|
```
|
|
254
132
|
|
|
255
|
-
Each platform appends a `🤖 Resolved by lisa` attribution comment to the PR/MR after creation.
|
|
256
|
-
|
|
257
133
|
---
|
|
258
134
|
|
|
259
|
-
## Install
|
|
260
|
-
|
|
261
|
-
```bash
|
|
262
|
-
npm install -g @tarcisiopgs/lisa
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
## Environment Variables
|
|
266
|
-
|
|
267
|
-
```bash
|
|
268
|
-
# PR/MR delivery — set one based on your platform
|
|
269
|
-
export GITHUB_TOKEN="" # GitHub — or have `gh` CLI authenticated (platform: cli or token)
|
|
270
|
-
export GITLAB_TOKEN="" # GitLab MR creation (platform: gitlab)
|
|
271
|
-
export BITBUCKET_TOKEN="" # Bitbucket PR creation (platform: bitbucket)
|
|
272
|
-
export BITBUCKET_USERNAME="" # Bitbucket username (required with BITBUCKET_TOKEN)
|
|
273
|
-
|
|
274
|
-
# Required when source = linear
|
|
275
|
-
export LINEAR_API_KEY=""
|
|
276
|
-
|
|
277
|
-
# Required when source = trello
|
|
278
|
-
export TRELLO_API_KEY=""
|
|
279
|
-
export TRELLO_TOKEN=""
|
|
280
|
-
|
|
281
|
-
# Required when source = plane
|
|
282
|
-
export PLANE_API_TOKEN=""
|
|
283
|
-
export PLANE_BASE_URL="" # optional; defaults to https://api.plane.so
|
|
284
|
-
export PLANE_WORKSPACE="" # optional; fallback when team is not set in config
|
|
285
|
-
|
|
286
|
-
# Required when source = shortcut
|
|
287
|
-
export SHORTCUT_API_TOKEN=""
|
|
288
|
-
|
|
289
|
-
# Required when source = gitlab-issues or github = gitlab
|
|
290
|
-
export GITLAB_TOKEN=""
|
|
291
|
-
export GITLAB_BASE_URL="" # optional; defaults to https://gitlab.com
|
|
292
|
-
|
|
293
|
-
# Required when source = github-issues
|
|
294
|
-
export GITHUB_TOKEN="" # same token used for PR creation
|
|
295
|
-
|
|
296
|
-
# Required when source = jira
|
|
297
|
-
export JIRA_BASE_URL="" # e.g. https://yourcompany.atlassian.net
|
|
298
|
-
export JIRA_EMAIL="" # Atlassian account email
|
|
299
|
-
export JIRA_API_TOKEN="" # Atlassian API token
|
|
300
|
-
```
|
|
301
|
-
|
|
302
135
|
## Commands
|
|
303
136
|
|
|
304
137
|
| Command | Description |
|
|
305
138
|
|---------|-------------|
|
|
306
139
|
| `lisa run` | Run the agent loop |
|
|
307
140
|
| `lisa run --once` | Process a single issue |
|
|
308
|
-
| `lisa run --once --dry-run` |
|
|
309
|
-
| `lisa run --watch`
|
|
141
|
+
| `lisa run --once --dry-run` | Preview config without executing |
|
|
142
|
+
| `lisa run --watch` | Poll for new issues every 60s after queue empties |
|
|
310
143
|
| `lisa run --issue ID` | Process a specific issue by identifier or URL |
|
|
311
|
-
| `lisa run --
|
|
312
|
-
| `lisa
|
|
313
|
-
| `lisa
|
|
314
|
-
| `lisa run --provider NAME` | Override AI provider |
|
|
315
|
-
| `lisa run --source NAME` | Override issue source |
|
|
316
|
-
| `lisa run --label NAME` | Override label filter |
|
|
317
|
-
| `lisa run --platform METHOD` | Override PR platform (`cli`, `token`, `gitlab`, or `bitbucket`) |
|
|
318
|
-
| `lisa run --no-bell` | Disable terminal bell on issue completion/failure |
|
|
319
|
-
| `lisa run --lifecycle MODE` | Lifecycle mode: `auto` (default), `skip`, or `validate-only` |
|
|
320
|
-
| `lisa run --lifecycle-timeout N` | Startup timeout per resource in seconds (default: 30) |
|
|
321
|
-
| `lisa run --demo` | Run animated demo with fake issues to preview the TUI |
|
|
322
|
-
| `lisa init` | Create `.lisa/config.yaml` interactively (offers pre-defined templates) |
|
|
323
|
-
| `lisa config` | Edit config interactively |
|
|
324
|
-
| `lisa config --show` | Print current config as JSON |
|
|
325
|
-
| `lisa config --set key=value` | Set a single config value |
|
|
144
|
+
| `lisa run --concurrency N` | Process N issues in parallel (each in its own worktree) |
|
|
145
|
+
| `lisa init` | Create `.lisa/config.yaml` interactively |
|
|
146
|
+
| `lisa config --show` | Print current config |
|
|
326
147
|
| `lisa status` | Show session stats |
|
|
327
|
-
| `lisa issue get <id>` | Fetch full issue details as JSON (for use inside worktrees) |
|
|
328
|
-
| `lisa issue done <id> --pr-url <url>` | Complete issue, attach PR, update status, remove label |
|
|
329
|
-
| `lisa feedback --pr <url> --issue <id>` | Inject PR review comments from a closed-without-merge PR into guardrails |
|
|
330
|
-
|
|
331
|
-
## TUI
|
|
332
|
-
|
|
333
|
-
When running in an interactive terminal, `lisa run` renders a real-time Kanban board:
|
|
334
148
|
|
|
335
|
-
|
|
336
|
-
┌──────────────────────────┐ ┌───────────────────────────┐ ┌───────────────────────────┐
|
|
337
|
-
│ ▶ BACKLOG [3] │ │ ▶ IN PROGRESS [1] │ │ ▶ IN REVIEW [2] │
|
|
338
|
-
│ │ │ │ │ │
|
|
339
|
-
│ ┌────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
|
|
340
|
-
│ │ ENG-42 │ │ │ │ ● ENG-38 │ │ │ │ ✓ ENG-35 │ │
|
|
341
|
-
│ │ Add dark mode │ │ │ │ Fix login redirect │ │ │ │ Update dependencies │ │
|
|
342
|
-
│ │ ready │ │ │ │ ~1 running │ │ │ │ PR created │ │
|
|
343
|
-
│ └────────────────────┘ │ │ └─────────────────────┘ │ │ └─────────────────────┘ │
|
|
344
|
-
└──────────────────────────┘ └───────────────────────────┘ └───────────────────────────┘
|
|
345
|
-
```
|
|
149
|
+
Run `lisa run --help` for all available flags.
|
|
346
150
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
### Keyboard shortcuts
|
|
350
|
-
|
|
351
|
-
| Key | Action |
|
|
352
|
-
|-----|--------|
|
|
353
|
-
| `←` / `→` | Move between columns |
|
|
354
|
-
| `Tab` / `Shift+Tab` | Move between columns (alternative) |
|
|
355
|
-
| `↑` / `↓` | Navigate cards / scroll output |
|
|
356
|
-
| `Enter` | Open issue detail view (streams provider output) |
|
|
357
|
-
| `o` | Open PR in browser (when viewing a completed card with a PR) |
|
|
358
|
-
| `Esc` | Close detail view, return to board |
|
|
359
|
-
| `k` | Kill the selected in-progress issue |
|
|
360
|
-
| `s` | Skip the selected in-progress issue |
|
|
361
|
-
| `p` | Pause / resume all active providers |
|
|
362
|
-
| `q` | Quit |
|
|
363
|
-
|
|
364
|
-
The sidebar legend updates contextually: board shortcuts when browsing the Kanban, scroll and back hints when viewing issue detail. The terminal tab title also updates in real time: it shows a spinner with the active issue ID while work is in progress, and a checkmark when done.
|
|
151
|
+
---
|
|
365
152
|
|
|
366
153
|
## Configuration
|
|
367
154
|
|
|
@@ -370,7 +157,7 @@ Config lives in `.lisa/config.yaml`. Run `lisa init` to create it interactively.
|
|
|
370
157
|
```yaml
|
|
371
158
|
provider: claude
|
|
372
159
|
source: linear
|
|
373
|
-
workflow: worktree
|
|
160
|
+
workflow: worktree # "worktree" (isolated) or "branch" (in-place)
|
|
374
161
|
|
|
375
162
|
source_config:
|
|
376
163
|
team: Engineering
|
|
@@ -380,257 +167,75 @@ source_config:
|
|
|
380
167
|
in_progress: In Progress
|
|
381
168
|
done: In Review
|
|
382
169
|
|
|
383
|
-
platform: cli
|
|
170
|
+
platform: cli # "cli", "token", "gitlab", or "bitbucket"
|
|
384
171
|
workspace: .
|
|
385
172
|
base_branch: main
|
|
386
173
|
|
|
174
|
+
# Multi-repo (optional)
|
|
387
175
|
repos:
|
|
388
176
|
- name: my-api
|
|
389
177
|
path: ./api
|
|
390
178
|
base_branch: main
|
|
391
|
-
match: "[API]"
|
|
179
|
+
match: "[API]" # route issues by title prefix
|
|
392
180
|
- name: my-app
|
|
393
181
|
path: ./app
|
|
394
182
|
base_branch: main
|
|
395
183
|
|
|
396
184
|
loop:
|
|
397
|
-
cooldown: 10
|
|
398
|
-
max_sessions: 0
|
|
399
|
-
concurrency: 1 # issues processed in parallel (default: 1; >1 forces worktree mode)
|
|
400
|
-
|
|
401
|
-
logs:
|
|
402
|
-
dir: .lisa/logs
|
|
185
|
+
cooldown: 10 # seconds between issues
|
|
186
|
+
max_sessions: 0 # 0 = unlimited
|
|
403
187
|
|
|
404
188
|
# Optional — kill stuck providers
|
|
405
189
|
overseer:
|
|
406
190
|
enabled: true
|
|
407
|
-
check_interval: 30
|
|
408
|
-
stuck_threshold: 300
|
|
191
|
+
check_interval: 30 # seconds between git status checks
|
|
192
|
+
stuck_threshold: 300 # seconds without changes before killing
|
|
409
193
|
|
|
410
|
-
# Optional —
|
|
194
|
+
# Optional — skip issues without acceptance criteria
|
|
411
195
|
validation:
|
|
412
|
-
require_acceptance_criteria: true
|
|
413
|
-
|
|
414
|
-
# Optional — control infrastructure lifecycle
|
|
415
|
-
lifecycle:
|
|
416
|
-
mode: auto # "auto" (default), "skip", or "validate-only"
|
|
417
|
-
timeout: 30 # seconds to wait per resource (default: 30)
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
### Source-Specific Fields
|
|
421
|
-
|
|
422
|
-
| Field | Linear | Trello | Plane | Shortcut | GitLab Issues | GitHub Issues | Jira |
|
|
423
|
-
|-------|--------|--------|-------|----------|---------------|---------------|------|
|
|
424
|
-
| `team` | Team name | Board name | Workspace slug | Group name (optional) | Project path (`namespace/project`) or numeric ID | `owner/repo` | Project key (e.g. `ENG`) |
|
|
425
|
-
| `project` | Project name | — | Project identifier or UUID | — | — | — | — |
|
|
426
|
-
| `pick_from` | Status to pick issues from | List to pick cards from | State name to pick issues from | Workflow state to pick stories from | — | — | Status to pick issues from |
|
|
427
|
-
| `label` | Label to filter issues | Label to filter cards | Label to filter issues | Label to filter stories | Label to filter issues | Label to filter issues | Label to filter issues |
|
|
428
|
-
| `remove_label` | Label removed after pickup (optional; defaults to `label`) | Same | Same | Same | Same | Same | Same |
|
|
429
|
-
| `in_progress` | In-progress status | In-progress column | In-progress state name | In-progress workflow state | Label to apply on activate | Label to apply on activate | In-progress status name |
|
|
430
|
-
| `done` | Destination status after PR | Destination column after PR | Done state name | Done workflow state | Closes the issue | Closes the issue | Destination status after PR |
|
|
431
|
-
|
|
432
|
-
`label` can also be a list — Lisa picks any issue that has **all** of the specified labels. `remove_label` controls which label is removed after pickup, useful when you want to keep some labels intact. Example:
|
|
433
|
-
|
|
434
|
-
```yaml
|
|
435
|
-
source_config:
|
|
436
|
-
label: [ready, api] # pick issues labelled both "ready" and "api"
|
|
437
|
-
remove_label: ready # only remove "ready" after pickup; "api" is preserved
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
Plane example:
|
|
441
|
-
|
|
442
|
-
```yaml
|
|
443
|
-
source: plane
|
|
444
|
-
source_config:
|
|
445
|
-
team: my-workspace # workspace slug (or set PLANE_WORKSPACE env var)
|
|
446
|
-
project: DEV # project identifier or UUID
|
|
447
|
-
label: ready # issues with this label are picked up
|
|
448
|
-
pick_from: Todo # state to fetch issues from
|
|
449
|
-
in_progress: In Progress # state set when Lisa starts working
|
|
450
|
-
done: Done # state set after PR is created
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
Shortcut example:
|
|
454
|
-
|
|
455
|
-
```yaml
|
|
456
|
-
source: shortcut
|
|
457
|
-
source_config:
|
|
458
|
-
label: ready # stories with this label are picked up
|
|
459
|
-
pick_from: Ready for Development # workflow state to fetch stories from
|
|
460
|
-
in_progress: In Progress # state set when Lisa starts working
|
|
461
|
-
done: Done # state set after PR is created
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
GitLab Issues example:
|
|
465
|
-
|
|
466
|
-
```yaml
|
|
467
|
-
source: gitlab-issues
|
|
468
|
-
source_config:
|
|
469
|
-
team: my-org/my-repo # namespace/project path or numeric project ID
|
|
470
|
-
label: ready # issues with this label are picked up
|
|
471
|
-
in_progress: in-progress # label applied when Lisa starts working
|
|
472
|
-
done: "" # issue is closed after PR (value unused)
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
GitHub Issues example:
|
|
476
|
-
|
|
477
|
-
```yaml
|
|
478
|
-
source: github-issues
|
|
479
|
-
source_config:
|
|
480
|
-
team: my-org/my-repo # owner/repo
|
|
481
|
-
label: ready # issues with this label are picked up
|
|
482
|
-
in_progress: in-progress # label applied when Lisa starts working
|
|
483
|
-
done: "" # issue is closed after PR (value unused)
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
Jira example:
|
|
487
|
-
|
|
488
|
-
```yaml
|
|
489
|
-
source: jira
|
|
490
|
-
source_config:
|
|
491
|
-
team: ENG # Jira project key
|
|
492
|
-
label: lisa # label to filter issues
|
|
493
|
-
pick_from: Backlog # status to pick issues from
|
|
494
|
-
in_progress: In Progress # status applied when Lisa starts working
|
|
495
|
-
done: In Review # status applied after PR is created
|
|
196
|
+
require_acceptance_criteria: true
|
|
496
197
|
```
|
|
497
198
|
|
|
498
199
|
### Workflow Modes
|
|
499
200
|
|
|
500
|
-
**Branch** — The
|
|
501
|
-
|
|
502
|
-
**Worktree** — Lisa creates an isolated [git worktree](https://git-scm.com/docs/git-worktree) for each issue under `.worktrees/`. The agent works in the worktree without touching your main checkout. After the PR is created, the worktree is cleaned up automatically. Ideal when you want to keep working in the repo while Lisa resolves issues in the background.
|
|
503
|
-
|
|
504
|
-
**Multi-repo worktree** — When multiple repos are configured, Lisa runs a two-phase flow: a planning agent produces a `.lisa-plan.json` with ordered steps, then Lisa executes each step sequentially — one worktree and one PR per repo. Cross-repo context (branch names, PR URLs) is passed to each subsequent step.
|
|
505
|
-
|
|
506
|
-
### Concurrent Execution
|
|
507
|
-
|
|
508
|
-
Process multiple issues in parallel with `--concurrency`:
|
|
509
|
-
|
|
510
|
-
```bash
|
|
511
|
-
lisa run --concurrency 3 # or -c 3
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
Each issue runs in its own isolated worktree with an independent provider process. When `--concurrency` is greater than 1, worktree mode is enforced automatically. The default is 1 (sequential, backward-compatible).
|
|
201
|
+
**Branch** — The agent creates a branch in your current checkout. Simple setup, works everywhere.
|
|
515
202
|
|
|
516
|
-
|
|
517
|
-
- **Shared infrastructure** — Docker containers and lifecycle resources are started once and shared across worktrees.
|
|
518
|
-
- **Guardrails** — Writes to the guardrails file are serialized to prevent concurrent corruption.
|
|
519
|
-
- **TUI** — Multiple cards appear in the In Progress column simultaneously. `[k]` and `[s]` target the selected card; `[p]` pauses all active providers.
|
|
520
|
-
- **Fallback** — Each issue has its own independent fallback chain.
|
|
521
|
-
- **Slot filling** — When an issue completes, the freed slot is immediately filled with the next available issue.
|
|
203
|
+
**Worktree** — Lisa creates an isolated git worktree per issue under `.worktrees/`. Your main checkout stays untouched. Cleaned up automatically after the PR is created.
|
|
522
204
|
|
|
523
|
-
|
|
205
|
+
When `--concurrency` is greater than 1, worktree mode is enforced automatically.
|
|
524
206
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
Lisa detects blocked issues using the `completedBlockerIds` from your issue tracker (Linear blockers, GitHub milestone links, etc.) and skips issues whose blockers are not yet completed. Once a blocker is resolved, the dependent issue becomes eligible and will be picked up in a future loop.
|
|
528
|
-
|
|
529
|
-
### Recovery Mechanisms
|
|
530
|
-
|
|
531
|
-
- **Orphan recovery** — On startup, Lisa scans for issues stuck in `in_progress` from interrupted runs and reverts them to `pick_from`.
|
|
532
|
-
- **Push recovery** — If `git push` fails due to pre-push hooks (linter, typecheck, tests), Lisa re-invokes the agent with the error output and retries the push.
|
|
533
|
-
- **Signal handling** — SIGINT/SIGTERM gracefully revert the active issue to its previous status before exiting.
|
|
534
|
-
- **Guardrails** — Failed sessions and rejected PR reviews are logged and injected into future prompts so the agent avoids repeating the same mistakes.
|
|
535
|
-
- **PR feedback injection** — When a PR created by Lisa is closed without merge, review comments are automatically captured and injected as guardrails for the next run of the same issue. Use `lisa feedback --pr <url> --issue <id>` to inject feedback manually.
|
|
536
|
-
|
|
537
|
-
### Overseer
|
|
538
|
-
|
|
539
|
-
When enabled, the overseer periodically checks `git status` in the working directory. If no changes are detected within `stuck_threshold` seconds, the provider process is killed and the error is eligible for fallback to the next model.
|
|
540
|
-
|
|
541
|
-
### Issue Spec Validation
|
|
542
|
-
|
|
543
|
-
Before starting work on an issue, Lisa validates that it has a minimum spec. Issues that fail validation are skipped, labelled `needs-spec`, and have their `ready` label removed — so they never block the queue.
|
|
544
|
-
|
|
545
|
-
An issue passes validation if its description is non-empty and contains at least one of:
|
|
546
|
-
|
|
547
|
-
- A Markdown checklist item (`- [ ]`)
|
|
548
|
-
- The phrase `acceptance criteria` or `critérios`
|
|
549
|
-
- The words `expected`, `should`, or `deve`
|
|
550
|
-
|
|
551
|
-
To disable spec validation (e.g. for quick one-off issues):
|
|
552
|
-
|
|
553
|
-
```yaml
|
|
554
|
-
validation:
|
|
555
|
-
require_acceptance_criteria: false
|
|
556
|
-
```
|
|
207
|
+
---
|
|
557
208
|
|
|
558
|
-
|
|
209
|
+
## Writing Issues
|
|
559
210
|
|
|
560
|
-
|
|
211
|
+
Issue quality is the single biggest factor in PR quality. Lisa validates issues before accepting them — vague tickets without clear criteria are skipped and labelled `needs-spec`.
|
|
561
212
|
|
|
562
|
-
|
|
213
|
+
Issues must contain acceptance criteria: markdown checklists (`- [ ]`) or keywords like `acceptance criteria`, `expected`, `should`.
|
|
563
214
|
|
|
564
|
-
|
|
215
|
+
### Example
|
|
565
216
|
|
|
566
|
-
|
|
217
|
+
```markdown
|
|
218
|
+
Title: Add rate limiting to /api/users endpoint
|
|
567
219
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
| Prisma | `npx prisma generate`, `npx prisma db push` |
|
|
571
|
-
| Drizzle | `npx drizzle-kit push` |
|
|
572
|
-
| TypeORM | `npx typeorm migration:run` |
|
|
573
|
-
| Sequelize | `npx sequelize-cli db:migrate` |
|
|
220
|
+
Description:
|
|
221
|
+
Implement rate limiting on the `/api/users` endpoint to prevent abuse.
|
|
574
222
|
|
|
575
|
-
|
|
223
|
+
Relevant files:
|
|
224
|
+
- src/routes/users.ts
|
|
225
|
+
- src/middleware/auth.ts
|
|
576
226
|
|
|
577
|
-
|
|
227
|
+
Acceptance criteria:
|
|
228
|
+
- [ ] Requests exceeding 100/min per IP return HTTP 429
|
|
229
|
+
- [ ] Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining) included in all responses
|
|
230
|
+
- [ ] Rate limit state stored in Redis (use existing connection from src/lib/redis.ts)
|
|
231
|
+
- [ ] Existing tests still pass
|
|
578
232
|
|
|
579
|
-
|
|
580
|
-
repos:
|
|
581
|
-
- path: api
|
|
582
|
-
# lifecycle config is resolved by auto-discovery (docker-compose.yml)
|
|
583
|
-
# port_range and port_env_var can be set via manual config override
|
|
233
|
+
Stack: Express, Redis
|
|
584
234
|
```
|
|
585
235
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
| Field | Type | Description |
|
|
589
|
-
|---|---|---|
|
|
590
|
-
| `check_port` | `number` | Preferred (base) port |
|
|
591
|
-
| `port_range` | `number` (optional) | How many ports to scan from `check_port` |
|
|
592
|
-
| `port_env_var` | `string` (optional) | Env var name to inject the allocated port (e.g. `DATABASE_PORT`) |
|
|
593
|
-
|
|
594
|
-
If no free port is found within the range, Lisa logs a clear error and aborts the session.
|
|
595
|
-
|
|
596
|
-
**Lifecycle modes** control how Lisa handles infrastructure:
|
|
236
|
+
Including **relevant files**, **technical constraints**, and **stack information** in the description leads to better results.
|
|
597
237
|
|
|
598
|
-
|
|
599
|
-
|------|----------|
|
|
600
|
-
| `auto` (default) | Discover, start, and tear down infrastructure automatically |
|
|
601
|
-
| `skip` | Skip all infrastructure management — assume services are already running |
|
|
602
|
-
| `validate-only` | Check that required ports are available but don't start or stop services |
|
|
603
|
-
|
|
604
|
-
### Auto-Detection
|
|
605
|
-
|
|
606
|
-
Lisa auto-detects `vitest` or `jest` from `package.json` dependencies and injects the correct test command into the agent prompt. It also detects the package manager from lockfiles (`bun.lockb`/`bun.lock` → `bun`, `pnpm-lock.yaml` → `pnpm`, `yarn.lock` → `yarn`, otherwise `npm`).
|
|
607
|
-
|
|
608
|
-
### Project Context Analysis
|
|
609
|
-
|
|
610
|
-
Before building the prompt for the AI agent, Lisa automatically analyzes the project and injects rich context into the prompt:
|
|
611
|
-
|
|
612
|
-
- **Quality scripts** — detects `lint`, `typecheck`, `check`, `format`, `test`, `build`, `ci` scripts from `package.json`
|
|
613
|
-
- **Test patterns** — discovers test file location (colocated vs separate directory), style (describe/it vs top-level test), mocking libraries (`vi.mock`, `jest.mock`, fixtures), and includes example code from the first test file found
|
|
614
|
-
- **Code tools** — reads configuration from Biome (`biome.json`), ESLint (`.eslintrc.*`, `eslint.config.*`), and Prettier (`.prettierrc.*`, `prettier.config.*`)
|
|
615
|
-
- **Project structure** — generates a tree view of immediate subdirectories and their children
|
|
616
|
-
- **Environment type** — classifies the project as CLI, Mobile (React Native/Flutter), Web, Server, Library, or Unknown
|
|
617
|
-
|
|
618
|
-
This context is formatted as a `## Project Context` section in the implementation prompt, giving the agent immediate understanding of the codebase conventions without exploration.
|
|
619
|
-
|
|
620
|
-
### API Client Generator Detection
|
|
621
|
-
|
|
622
|
-
Lisa detects API client generators in the project and enriches the agent prompt with generation details — input spec location, output directory, and the command to regenerate clients:
|
|
623
|
-
|
|
624
|
-
| Generator | Config Files |
|
|
625
|
-
|-----------|-------------|
|
|
626
|
-
| Orval | `orval.config.ts/js`, `.orvalrc`, `.orvalrc.json/js/ts` |
|
|
627
|
-
| Kubb | `kubb.config.ts/js/mjs` |
|
|
628
|
-
| hey-api | `openapi-ts.config.ts/js/mjs` |
|
|
629
|
-
| openapi-generator | `openapitools.json`, `.openapi-generator/` |
|
|
630
|
-
| swagger-codegen | `swagger-codegen-config.json` |
|
|
631
|
-
| openapi-typescript | detected from `package.json` dependencies |
|
|
632
|
-
|
|
633
|
-
When a generator is detected, the agent receives the config file path, whether the spec is a live URL or local file, the output directory, and the `package.json` script (or `npx` command) to run the generator.
|
|
238
|
+
---
|
|
634
239
|
|
|
635
240
|
## License
|
|
636
241
|
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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, {
|
|
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
|
|
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,
|