@tarcisiopgs/lisa 1.28.1 → 1.29.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 +20 -3
- package/dist/{chunk-42NKASE3.js → chunk-LQZQML3A.js} +1 -1
- package/dist/{chunk-FCEUJ7VK.js → chunk-NDNEDFG5.js} +445 -87
- package/dist/{chunk-2TW2MJXF.js → chunk-NMQ6YMBH.js} +7 -1
- package/dist/{chunk-UXVSQQID.js → chunk-YURAUUDI.js} +17 -9
- package/dist/{chunk-KDAXGOFF.js → chunk-ZXZRX5OQ.js} +54 -4
- package/dist/{detection-JT7HSKSX.js → detection-SC5MTPL7.js} +2 -2
- package/dist/index.js +161 -93
- package/dist/{loop-SXI4PQOI.js → loop-6VPQILXY.js} +3 -3
- package/dist/{tui-bridge-DCC4JAPM.js → tui-bridge-C4P7A5EP.js} +42 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ lisa # start the agent loop
|
|
|
31
31
|
Plan → Create issues → Fetch → Implement → Push → Open PR → Update board → Next
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Lisa starts and shows a Kanban board. If the queue is empty, press `n` to plan — describe a goal and the AI decomposes
|
|
34
|
+
Lisa starts and shows a Kanban board. If the queue is empty, press `n` to plan — describe a goal and the AI brainstorms with you (asking clarifying questions), presents its understanding for your confirmation, then decomposes the goal into atomic issues created directly in your tracker. You can review, edit, reorder, delete, or regenerate the plan with feedback before approving. 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,7 +39,8 @@ 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
|
+
- **AI planning** — describe a goal, the AI brainstorms with you, decomposes it into issues with dependencies, created in your tracker
|
|
43
|
+
- **Language-aware** — detects your goal's language (pt/en/es) and generates issues in the same language
|
|
43
44
|
- **Concurrent execution** — process multiple issues in parallel, each in its own worktree
|
|
44
45
|
- **Multi-repo** — plans across repos, creates one PR per repo in the correct order
|
|
45
46
|
- **Model fallback** — chain models; transient errors (429, quota, timeout) auto-switch to the next
|
|
@@ -90,9 +91,11 @@ lisa --watch # poll for new issues after queue empties
|
|
|
90
91
|
lisa -c 3 # process 3 issues in parallel
|
|
91
92
|
lisa --issue INT-42 # process a specific issue
|
|
92
93
|
lisa --limit 5 # stop after 5 issues
|
|
93
|
-
lisa plan "Add rate limiting" # decompose goal into issues via AI
|
|
94
|
+
lisa plan "Add rate limiting" # brainstorm + decompose goal into issues via AI
|
|
94
95
|
lisa plan --issue EPIC-123 # decompose existing issue into sub-issues
|
|
95
96
|
lisa plan --continue # resume interrupted plan
|
|
97
|
+
lisa plan --no-brainstorm "goal" # skip brainstorming, decompose directly
|
|
98
|
+
lisa plan --yes "goal" # skip confirmations (CI/scripts)
|
|
96
99
|
lisa init # create .lisa/config.yaml interactively
|
|
97
100
|
lisa status # show session stats
|
|
98
101
|
lisa doctor # diagnose setup issues (config, provider, env, git)
|
|
@@ -214,6 +217,18 @@ lifecycle:
|
|
|
214
217
|
mode: auto # "auto", "skip" (default), "validate-only"
|
|
215
218
|
timeout: 30
|
|
216
219
|
|
|
220
|
+
proof_of_work:
|
|
221
|
+
enabled: true
|
|
222
|
+
block_on_failure: true # skip PR when validation fails (default: false)
|
|
223
|
+
max_retries: 2 # retry agent on validation failure
|
|
224
|
+
commands:
|
|
225
|
+
- name: lint
|
|
226
|
+
run: pnpm run lint
|
|
227
|
+
- name: typecheck
|
|
228
|
+
run: pnpm run typecheck
|
|
229
|
+
- name: test
|
|
230
|
+
run: pnpm run test
|
|
231
|
+
|
|
217
232
|
validation:
|
|
218
233
|
require_acceptance_criteria: true
|
|
219
234
|
```
|
|
@@ -271,6 +286,8 @@ The real-time Kanban board shows issue progress, streams provider output, and de
|
|
|
271
286
|
| `a` | Approve and create issues |
|
|
272
287
|
| `Esc` | Cancel / back |
|
|
273
288
|
|
|
289
|
+
In CLI mode, the plan wizard also offers **Regenerate with feedback** — describe what to change and the AI regenerates the entire plan incorporating your feedback.
|
|
290
|
+
|
|
274
291
|
## License
|
|
275
292
|
|
|
276
293
|
[MIT](LICENSE)
|
|
@@ -1,67 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
buildContextMdBlock,
|
|
4
|
-
readContext
|
|
5
|
-
|
|
4
|
+
readContext,
|
|
5
|
+
resolveModels,
|
|
6
|
+
runWithFallback
|
|
7
|
+
} from "./chunk-YURAUUDI.js";
|
|
6
8
|
import {
|
|
9
|
+
error,
|
|
10
|
+
log,
|
|
7
11
|
normalizeLabels,
|
|
8
12
|
ok,
|
|
9
13
|
warn
|
|
10
14
|
} from "./chunk-5N4BWHIT.js";
|
|
11
15
|
|
|
12
|
-
// src/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const sorted = [...plan.issues].sort((a, b) => a.order - b.order);
|
|
20
|
-
const createdIds = [];
|
|
21
|
-
const orderToId = /* @__PURE__ */ new Map();
|
|
22
|
-
for (const issue of sorted) {
|
|
23
|
-
let description = issue.description;
|
|
24
|
-
if (issue.dependsOn.length > 0 && !source.linkDependency) {
|
|
25
|
-
const depRefs = issue.dependsOn.map((depOrder) => {
|
|
26
|
-
const depId = orderToId.get(depOrder);
|
|
27
|
-
return depId ? `#${depId}` : `step ${depOrder}`;
|
|
28
|
-
}).join(", ");
|
|
29
|
-
description += `
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
_Depends on: ${depRefs}_`;
|
|
33
|
-
}
|
|
34
|
-
const id = await source.createIssue(
|
|
35
|
-
{
|
|
36
|
-
title: issue.title,
|
|
37
|
-
description,
|
|
38
|
-
status: config.pick_from,
|
|
39
|
-
label: primaryLabel,
|
|
40
|
-
order: issue.order,
|
|
41
|
-
parentId: plan.sourceIssueId
|
|
42
|
-
},
|
|
43
|
-
config
|
|
44
|
-
);
|
|
45
|
-
createdIds.push(id);
|
|
46
|
-
orderToId.set(issue.order, id);
|
|
47
|
-
ok(`${id}: ${issue.title}`);
|
|
48
|
-
if (source.linkDependency && issue.dependsOn.length > 0) {
|
|
49
|
-
for (const depOrder of issue.dependsOn) {
|
|
50
|
-
const depId = orderToId.get(depOrder);
|
|
51
|
-
if (depId) {
|
|
52
|
-
try {
|
|
53
|
-
await source.linkDependency(id, depId);
|
|
54
|
-
} catch (err) {
|
|
55
|
-
warn(
|
|
56
|
-
`Could not link dependency ${id} \u2192 ${depId}: ${err instanceof Error ? err.message : String(err)}`
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
16
|
+
// src/cli/error.ts
|
|
17
|
+
var CliError = class extends Error {
|
|
18
|
+
exitCode;
|
|
19
|
+
constructor(message, exitCode = 1) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "CliError";
|
|
22
|
+
this.exitCode = exitCode;
|
|
62
23
|
}
|
|
63
|
-
|
|
64
|
-
}
|
|
24
|
+
};
|
|
65
25
|
|
|
66
26
|
// src/plan/parser.ts
|
|
67
27
|
function parsePlanResponse(raw) {
|
|
@@ -116,43 +76,296 @@ var PlanParseError = class extends Error {
|
|
|
116
76
|
}
|
|
117
77
|
};
|
|
118
78
|
|
|
119
|
-
// src/plan/
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
79
|
+
// src/plan/structured-output.ts
|
|
80
|
+
function parseStructuredOutput(raw) {
|
|
81
|
+
const cleaned = stripAnsi(raw).trim();
|
|
82
|
+
const parsed = extractStructuredJson(cleaned);
|
|
83
|
+
if (parsed) {
|
|
84
|
+
if (parsed.type === "issues" && Array.isArray(parsed.issues)) {
|
|
85
|
+
try {
|
|
86
|
+
const issues = parsePlanResponse(JSON.stringify({ issues: parsed.issues }));
|
|
87
|
+
return { type: "issues", issues };
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (parsed.type === "summary" && typeof parsed.text === "string") {
|
|
92
|
+
return { type: "summary", text: parsed.text };
|
|
93
|
+
}
|
|
94
|
+
if (parsed.type === "question" && typeof parsed.text === "string") {
|
|
95
|
+
return { type: "question", text: parsed.text };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const issues = parsePlanResponse(cleaned);
|
|
100
|
+
return { type: "issues", issues };
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
return { type: "question", text: extractCleanText(cleaned) };
|
|
124
104
|
}
|
|
125
|
-
function
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
105
|
+
function extractStructuredJson(text2) {
|
|
106
|
+
const fenceMatch = text2.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
107
|
+
if (fenceMatch) {
|
|
108
|
+
const result = tryParseStructured(fenceMatch[1].trim());
|
|
109
|
+
if (result) return result;
|
|
110
|
+
}
|
|
111
|
+
const bracePositions = [];
|
|
112
|
+
for (let i = text2.length - 1; i >= 0; i--) {
|
|
113
|
+
if (text2[i] === "}") {
|
|
114
|
+
bracePositions.push(i);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const endPos of bracePositions) {
|
|
118
|
+
let depth = 0;
|
|
119
|
+
let startPos = -1;
|
|
120
|
+
for (let i = endPos; i >= 0; i--) {
|
|
121
|
+
if (text2[i] === "}") depth++;
|
|
122
|
+
if (text2[i] === "{") depth--;
|
|
123
|
+
if (depth === 0) {
|
|
124
|
+
startPos = i;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (startPos !== -1) {
|
|
129
|
+
const candidate = text2.slice(startPos, endPos + 1);
|
|
130
|
+
const result = tryParseStructured(candidate);
|
|
131
|
+
if (result) return result;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
132
135
|
}
|
|
133
|
-
function
|
|
134
|
-
if (!existsSync(filePath)) return null;
|
|
136
|
+
function tryParseStructured(jsonStr) {
|
|
135
137
|
try {
|
|
136
|
-
|
|
138
|
+
const obj = JSON.parse(jsonStr);
|
|
139
|
+
if (typeof obj === "object" && obj !== null && typeof obj.type === "string") {
|
|
140
|
+
return obj;
|
|
141
|
+
}
|
|
137
142
|
} catch {
|
|
138
|
-
return null;
|
|
139
143
|
}
|
|
144
|
+
return null;
|
|
140
145
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
function stripAnsi(text2) {
|
|
147
|
+
return text2.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
148
|
+
}
|
|
149
|
+
function extractCleanText(text2) {
|
|
150
|
+
if (text2.length < 500) return text2;
|
|
151
|
+
const lines = text2.split("\n").filter((l) => l.trim().length > 0);
|
|
152
|
+
return lines.slice(-10).join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/plan/create.ts
|
|
156
|
+
async function createPlanIssues(source, config, plan) {
|
|
157
|
+
if (!source.createIssue) {
|
|
158
|
+
throw new Error(`Source "${source.name}" does not support createIssue`);
|
|
149
159
|
}
|
|
150
|
-
|
|
160
|
+
const labels = normalizeLabels(config);
|
|
161
|
+
const primaryLabel = labels[0] ?? "";
|
|
162
|
+
const sorted = [...plan.issues].sort((a, b) => a.order - b.order);
|
|
163
|
+
const createdIds = [];
|
|
164
|
+
const orderToId = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const issue of sorted) {
|
|
166
|
+
let description = issue.description;
|
|
167
|
+
if (issue.dependsOn.length > 0 && !source.linkDependency) {
|
|
168
|
+
const depRefs = issue.dependsOn.map((depOrder) => {
|
|
169
|
+
const depId = orderToId.get(depOrder);
|
|
170
|
+
return depId ? `#${depId}` : `step ${depOrder}`;
|
|
171
|
+
}).join(", ");
|
|
172
|
+
description += `
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
_Depends on: ${depRefs}_`;
|
|
176
|
+
}
|
|
177
|
+
const id = await source.createIssue(
|
|
178
|
+
{
|
|
179
|
+
title: issue.title,
|
|
180
|
+
description,
|
|
181
|
+
status: config.pick_from,
|
|
182
|
+
label: primaryLabel,
|
|
183
|
+
order: issue.order,
|
|
184
|
+
parentId: plan.sourceIssueId
|
|
185
|
+
},
|
|
186
|
+
config
|
|
187
|
+
);
|
|
188
|
+
createdIds.push(id);
|
|
189
|
+
orderToId.set(issue.order, id);
|
|
190
|
+
ok(`${id}: ${issue.title}`);
|
|
191
|
+
if (source.linkDependency && issue.dependsOn.length > 0) {
|
|
192
|
+
for (const depOrder of issue.dependsOn) {
|
|
193
|
+
const depId = orderToId.get(depOrder);
|
|
194
|
+
if (depId) {
|
|
195
|
+
try {
|
|
196
|
+
await source.linkDependency(id, depId);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
warn(
|
|
199
|
+
`Could not link dependency ${id} \u2192 ${depId}: ${err instanceof Error ? err.message : String(err)}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return createdIds;
|
|
151
207
|
}
|
|
152
208
|
|
|
153
209
|
// src/plan/prompt.ts
|
|
154
210
|
import { resolve } from "path";
|
|
211
|
+
|
|
212
|
+
// src/plan/language.ts
|
|
213
|
+
var STOP_WORDS = {
|
|
214
|
+
pt: /* @__PURE__ */ new Set([
|
|
215
|
+
"n\xE3o",
|
|
216
|
+
"tamb\xE9m",
|
|
217
|
+
"j\xE1",
|
|
218
|
+
"est\xE1",
|
|
219
|
+
"s\xE3o",
|
|
220
|
+
"nos",
|
|
221
|
+
"das",
|
|
222
|
+
"dos",
|
|
223
|
+
"pelo",
|
|
224
|
+
"pela",
|
|
225
|
+
"uma",
|
|
226
|
+
"nas",
|
|
227
|
+
"aos",
|
|
228
|
+
"essa",
|
|
229
|
+
"esse",
|
|
230
|
+
"isso",
|
|
231
|
+
"aqui",
|
|
232
|
+
"muito",
|
|
233
|
+
"quando",
|
|
234
|
+
"como",
|
|
235
|
+
"mais",
|
|
236
|
+
"ainda",
|
|
237
|
+
"fazer",
|
|
238
|
+
"deve",
|
|
239
|
+
"pode",
|
|
240
|
+
"cada",
|
|
241
|
+
"todos",
|
|
242
|
+
"todas",
|
|
243
|
+
"entre",
|
|
244
|
+
"ap\xF3s",
|
|
245
|
+
"sobre",
|
|
246
|
+
"seus",
|
|
247
|
+
"suas",
|
|
248
|
+
"desta",
|
|
249
|
+
"deste",
|
|
250
|
+
"onde",
|
|
251
|
+
"apenas",
|
|
252
|
+
// Common contractions (preposition + article)
|
|
253
|
+
"na",
|
|
254
|
+
"no",
|
|
255
|
+
"da",
|
|
256
|
+
"do",
|
|
257
|
+
"ao",
|
|
258
|
+
"num",
|
|
259
|
+
"numa",
|
|
260
|
+
"para",
|
|
261
|
+
"com",
|
|
262
|
+
"sem",
|
|
263
|
+
"ou"
|
|
264
|
+
]),
|
|
265
|
+
es: /* @__PURE__ */ new Set([
|
|
266
|
+
"tambi\xE9n",
|
|
267
|
+
"m\xE1s",
|
|
268
|
+
"pero",
|
|
269
|
+
"muy",
|
|
270
|
+
"est\xE1",
|
|
271
|
+
"est\xE1n",
|
|
272
|
+
"puede",
|
|
273
|
+
"todo",
|
|
274
|
+
"esta",
|
|
275
|
+
"este",
|
|
276
|
+
"como",
|
|
277
|
+
"cuando",
|
|
278
|
+
"donde",
|
|
279
|
+
"cada",
|
|
280
|
+
"entre",
|
|
281
|
+
"sobre",
|
|
282
|
+
"despu\xE9s",
|
|
283
|
+
"antes",
|
|
284
|
+
"desde",
|
|
285
|
+
"hasta",
|
|
286
|
+
"seg\xFAn",
|
|
287
|
+
"durante",
|
|
288
|
+
"todos",
|
|
289
|
+
"todas",
|
|
290
|
+
"otro",
|
|
291
|
+
"otra",
|
|
292
|
+
"otros",
|
|
293
|
+
"otras",
|
|
294
|
+
"hacer",
|
|
295
|
+
"debe",
|
|
296
|
+
"aqu\xED",
|
|
297
|
+
"ahora",
|
|
298
|
+
"siempre",
|
|
299
|
+
"nunca"
|
|
300
|
+
]),
|
|
301
|
+
en: /* @__PURE__ */ new Set([
|
|
302
|
+
"the",
|
|
303
|
+
"is",
|
|
304
|
+
"are",
|
|
305
|
+
"was",
|
|
306
|
+
"were",
|
|
307
|
+
"been",
|
|
308
|
+
"being",
|
|
309
|
+
"have",
|
|
310
|
+
"has",
|
|
311
|
+
"had",
|
|
312
|
+
"having",
|
|
313
|
+
"does",
|
|
314
|
+
"did",
|
|
315
|
+
"will",
|
|
316
|
+
"would",
|
|
317
|
+
"could",
|
|
318
|
+
"should",
|
|
319
|
+
"might",
|
|
320
|
+
"shall",
|
|
321
|
+
"this",
|
|
322
|
+
"that",
|
|
323
|
+
"these",
|
|
324
|
+
"those",
|
|
325
|
+
"with",
|
|
326
|
+
"from",
|
|
327
|
+
"into",
|
|
328
|
+
"through",
|
|
329
|
+
"during",
|
|
330
|
+
"before",
|
|
331
|
+
"after",
|
|
332
|
+
"above",
|
|
333
|
+
"below",
|
|
334
|
+
"between",
|
|
335
|
+
"each",
|
|
336
|
+
"every",
|
|
337
|
+
"which",
|
|
338
|
+
"when",
|
|
339
|
+
"where",
|
|
340
|
+
"while"
|
|
341
|
+
])
|
|
342
|
+
};
|
|
343
|
+
var LANGUAGE_NAMES = {
|
|
344
|
+
pt: "Portuguese",
|
|
345
|
+
es: "Spanish",
|
|
346
|
+
en: "English"
|
|
347
|
+
};
|
|
348
|
+
function detectLanguage(text2) {
|
|
349
|
+
const words = text2.toLowerCase().replace(/[^\p{L}\s]/gu, "").split(/\s+/).filter((w) => w.length > 1);
|
|
350
|
+
if (words.length === 0) return "en";
|
|
351
|
+
const scores = { pt: 0, es: 0, en: 0 };
|
|
352
|
+
for (const word of words) {
|
|
353
|
+
for (const [lang, stopWords] of Object.entries(STOP_WORDS)) {
|
|
354
|
+
if (stopWords.has(word)) {
|
|
355
|
+
scores[lang]++;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const best = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
|
|
360
|
+
return best[1] > 0 ? best[0] : "en";
|
|
361
|
+
}
|
|
362
|
+
function languageName(code) {
|
|
363
|
+
return LANGUAGE_NAMES[code] ?? "English";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/plan/prompt.ts
|
|
155
367
|
function buildPlanningPrompt(goal, config, parentIssueDescription) {
|
|
368
|
+
const language = detectLanguage(goal);
|
|
156
369
|
const workspace = resolve(config.workspace);
|
|
157
370
|
const contextMd = readContext(workspace);
|
|
158
371
|
const contextBlock = buildContextMdBlock(contextMd);
|
|
@@ -186,6 +399,10 @@ For each issue, provide:
|
|
|
186
399
|
- **order**: Integer (1-based) \u2014 execution order based on dependencies
|
|
187
400
|
- **dependsOn**: Array of order numbers this issue depends on (empty if independent)
|
|
188
401
|
${config.repos.length > 1 ? "- **repo**: Name of the target repository from the list above (required for multi-repo)\n" : ""}
|
|
402
|
+
## Language
|
|
403
|
+
|
|
404
|
+
Respond in ${languageName(language)}. Generate all issue titles, descriptions, and acceptance criteria in ${languageName(language)}.
|
|
405
|
+
|
|
189
406
|
## Rules
|
|
190
407
|
|
|
191
408
|
1. Each issue MUST be self-contained and completable in a single session
|
|
@@ -203,13 +420,118 @@ Respond with ONLY this JSON structure (no wrapping, no markdown):
|
|
|
203
420
|
{"issues":[{"title":"...","description":"...","acceptanceCriteria":["..."],"relevantFiles":["..."],"order":1,"dependsOn":[]${config.repos.length > 1 ? ',"repo":"..."' : ""}}]}`;
|
|
204
421
|
}
|
|
205
422
|
|
|
423
|
+
// src/plan/persistence.ts
|
|
424
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
425
|
+
import { join } from "path";
|
|
426
|
+
function plansDir(workspace) {
|
|
427
|
+
return join(workspace, ".lisa", "plans");
|
|
428
|
+
}
|
|
429
|
+
function savePlan(workspace, plan) {
|
|
430
|
+
const dir = plansDir(workspace);
|
|
431
|
+
mkdirSync(dir, { recursive: true });
|
|
432
|
+
const filename = `${plan.createdAt.replace(/[:.]/g, "-")}.json`;
|
|
433
|
+
const filePath = join(dir, filename);
|
|
434
|
+
writeFileSync(filePath, JSON.stringify(plan, null, 2));
|
|
435
|
+
return filePath;
|
|
436
|
+
}
|
|
437
|
+
function loadPlan(filePath) {
|
|
438
|
+
if (!existsSync(filePath)) return null;
|
|
439
|
+
try {
|
|
440
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function loadLatestPlan(workspace) {
|
|
446
|
+
const dir = plansDir(workspace);
|
|
447
|
+
if (!existsSync(dir)) return null;
|
|
448
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
449
|
+
for (const file of files) {
|
|
450
|
+
const filePath = join(dir, file);
|
|
451
|
+
const plan = loadPlan(filePath);
|
|
452
|
+
if (plan && plan.status !== "created") return [plan, filePath];
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
206
457
|
// src/plan/wizard.ts
|
|
207
458
|
import { execSync } from "child_process";
|
|
208
|
-
import { mkdtempSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
209
|
-
import { tmpdir } from "os";
|
|
210
|
-
import { join as
|
|
459
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
460
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
461
|
+
import { join as join3 } from "path";
|
|
211
462
|
import * as clack from "@clack/prompts";
|
|
212
463
|
import pc from "picocolors";
|
|
464
|
+
|
|
465
|
+
// src/plan/generate.ts
|
|
466
|
+
import { mkdtempSync } from "fs";
|
|
467
|
+
import { tmpdir } from "os";
|
|
468
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
469
|
+
var MAX_PARSE_RETRIES = 2;
|
|
470
|
+
async function generatePlan(goal, config, opts) {
|
|
471
|
+
let prompt = buildPlanningPrompt(goal, config, opts?.parentDescription);
|
|
472
|
+
if (opts?.feedback) {
|
|
473
|
+
const previousBlock = opts.previousTitles && opts.previousTitles.length > 0 ? `
|
|
474
|
+
The previous plan had ${opts.previousTitles.length} issues: ${opts.previousTitles.join(", ")}` : "";
|
|
475
|
+
prompt += `
|
|
476
|
+
|
|
477
|
+
## Regeneration Feedback
|
|
478
|
+
|
|
479
|
+
The user reviewed the previous plan and wants changes:${previousBlock}
|
|
480
|
+
|
|
481
|
+
User feedback: ${opts.feedback}
|
|
482
|
+
|
|
483
|
+
Regenerate the plan considering this feedback. Output ONLY the JSON structure defined above.`;
|
|
484
|
+
}
|
|
485
|
+
log("Analyzing codebase and decomposing goal...");
|
|
486
|
+
const models = resolveModels(config);
|
|
487
|
+
const logDir = mkdtempSync(join2(tmpdir(), "lisa-plan-"));
|
|
488
|
+
const logFile = join2(logDir, "plan.log");
|
|
489
|
+
const result = await runWithFallback(models, prompt, {
|
|
490
|
+
logFile,
|
|
491
|
+
cwd: resolve2(config.workspace),
|
|
492
|
+
sessionTimeout: 120
|
|
493
|
+
});
|
|
494
|
+
if (!result.success) {
|
|
495
|
+
throw new CliError(`AI provider failed to generate plan: ${result.output.slice(0, 200)}`);
|
|
496
|
+
}
|
|
497
|
+
let lastError = null;
|
|
498
|
+
for (let attempt = 0; attempt <= MAX_PARSE_RETRIES; attempt++) {
|
|
499
|
+
try {
|
|
500
|
+
if (attempt === 0) {
|
|
501
|
+
return parsePlanResponse(result.output);
|
|
502
|
+
}
|
|
503
|
+
const retryPrompt = `${prompt}
|
|
504
|
+
|
|
505
|
+
## Previous Attempt Failed
|
|
506
|
+
|
|
507
|
+
Your previous response could not be parsed: ${lastError.message}
|
|
508
|
+
|
|
509
|
+
Please output ONLY valid JSON with the exact structure specified above.`;
|
|
510
|
+
const retryResult = await runWithFallback(models, retryPrompt, {
|
|
511
|
+
logFile,
|
|
512
|
+
cwd: resolve2(config.workspace),
|
|
513
|
+
sessionTimeout: 120
|
|
514
|
+
});
|
|
515
|
+
if (retryResult.success) {
|
|
516
|
+
return parsePlanResponse(retryResult.output);
|
|
517
|
+
}
|
|
518
|
+
} catch (err) {
|
|
519
|
+
if (err instanceof PlanParseError) {
|
|
520
|
+
lastError = err;
|
|
521
|
+
if (attempt < MAX_PARSE_RETRIES) {
|
|
522
|
+
warn(`Parse attempt ${attempt + 1} failed: ${err.message}. Retrying...`);
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
throw err;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
throw new CliError(
|
|
530
|
+
`Failed to parse AI response after ${MAX_PARSE_RETRIES + 1} attempts: ${lastError?.message}`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/plan/wizard.ts
|
|
213
535
|
async function runPlanWizard(plan, planPath, opts) {
|
|
214
536
|
const workspace = opts.config.workspace;
|
|
215
537
|
while (true) {
|
|
@@ -221,6 +543,10 @@ async function runPlanWizard(plan, planPath, opts) {
|
|
|
221
543
|
{ value: "edit", label: `${pc.yellow("Edit")} \u2014 edit an issue in $EDITOR` },
|
|
222
544
|
{ value: "delete", label: `${pc.red("Delete")} \u2014 remove an issue` },
|
|
223
545
|
{ value: "reorder", label: `${pc.cyan("Reorder")} \u2014 change execution order` },
|
|
546
|
+
{
|
|
547
|
+
value: "regenerate",
|
|
548
|
+
label: `${pc.magenta("Regenerate")} \u2014 regenerate plan with feedback`
|
|
549
|
+
},
|
|
224
550
|
{ value: "cancel", label: `${pc.gray("Cancel")} \u2014 save and exit` }
|
|
225
551
|
]
|
|
226
552
|
});
|
|
@@ -246,6 +572,13 @@ async function runPlanWizard(plan, planPath, opts) {
|
|
|
246
572
|
await reorderIssues(plan, workspace);
|
|
247
573
|
savePlan(workspace, plan);
|
|
248
574
|
}
|
|
575
|
+
if (action === "regenerate") {
|
|
576
|
+
const regenerated = await regeneratePlan(plan, opts);
|
|
577
|
+
if (regenerated) {
|
|
578
|
+
plan.issues = regenerated;
|
|
579
|
+
savePlan(workspace, plan);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
249
582
|
}
|
|
250
583
|
}
|
|
251
584
|
function displayPlan(plan) {
|
|
@@ -280,8 +613,8 @@ async function editIssue(plan, workspace) {
|
|
|
280
613
|
if (clack.isCancel(choice)) return;
|
|
281
614
|
const issue = plan.issues.find((i) => i.order === choice);
|
|
282
615
|
if (!issue) return;
|
|
283
|
-
const tmpDir =
|
|
284
|
-
const tmpFile =
|
|
616
|
+
const tmpDir = mkdtempSync2(join3(tmpdir2(), "lisa-edit-"));
|
|
617
|
+
const tmpFile = join3(tmpDir, "issue.md");
|
|
285
618
|
writeFileSync2(tmpFile, issueToMarkdown(issue));
|
|
286
619
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
287
620
|
try {
|
|
@@ -348,6 +681,28 @@ async function reorderIssues(plan, _workspace) {
|
|
|
348
681
|
}
|
|
349
682
|
clack.log.success("Issues reordered.");
|
|
350
683
|
}
|
|
684
|
+
async function regeneratePlan(plan, opts) {
|
|
685
|
+
const feedback = await clack.text({
|
|
686
|
+
message: "What would you like to change?",
|
|
687
|
+
placeholder: 'e.g., "Group into max 3 issues" or "Add tests for each issue"',
|
|
688
|
+
validate: (v) => !v?.trim() ? "Please describe what to change" : void 0
|
|
689
|
+
});
|
|
690
|
+
if (clack.isCancel(feedback)) return null;
|
|
691
|
+
const spinner2 = clack.spinner();
|
|
692
|
+
spinner2.start("Regenerating plan...");
|
|
693
|
+
try {
|
|
694
|
+
const issues = await generatePlan(plan.goal, opts.config, {
|
|
695
|
+
feedback,
|
|
696
|
+
previousTitles: plan.issues.map((i) => i.title)
|
|
697
|
+
});
|
|
698
|
+
spinner2.stop("Plan regenerated.");
|
|
699
|
+
return issues;
|
|
700
|
+
} catch (err) {
|
|
701
|
+
spinner2.stop("Regeneration failed.");
|
|
702
|
+
error(`Failed to regenerate: ${err instanceof Error ? err.message : String(err)}`);
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
351
706
|
function issueToMarkdown(issue) {
|
|
352
707
|
let md = `# ${issue.title}
|
|
353
708
|
|
|
@@ -395,12 +750,15 @@ function markdownToIssue(content, original) {
|
|
|
395
750
|
}
|
|
396
751
|
|
|
397
752
|
export {
|
|
753
|
+
CliError,
|
|
754
|
+
detectLanguage,
|
|
755
|
+
languageName,
|
|
756
|
+
parseStructuredOutput,
|
|
398
757
|
createPlanIssues,
|
|
399
|
-
|
|
400
|
-
|
|
758
|
+
buildPlanningPrompt,
|
|
759
|
+
generatePlan,
|
|
401
760
|
savePlan,
|
|
402
761
|
loadLatestPlan,
|
|
403
|
-
buildPlanningPrompt,
|
|
404
762
|
runPlanWizard,
|
|
405
763
|
issueToMarkdown,
|
|
406
764
|
markdownToIssue
|