@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 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 it into atomic issues, created directly in your tracker. Press `r` to start processing. Lisa picks the highest-priority labeled issue, moves it to "In Progress", sends a structured prompt to the AI agent, and monitors execution. The agent works in an isolated git worktree, implements the change, runs tests, and commits. Lisa pushes, opens a PR, moves the ticket to "In Review", and picks up the next one.
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 (CLI mode)
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)
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  formatError,
4
4
  isGhCliAvailable
5
- } from "./chunk-2TW2MJXF.js";
5
+ } from "./chunk-NMQ6YMBH.js";
6
6
 
7
7
  // src/cli/detection.ts
8
8
  import { execSync } from "child_process";
@@ -1,67 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildContextMdBlock,
4
- readContext
5
- } from "./chunk-UXVSQQID.js";
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/plan/create.ts
13
- async function createPlanIssues(source, config, plan) {
14
- if (!source.createIssue) {
15
- throw new Error(`Source "${source.name}" does not support createIssue`);
16
- }
17
- const labels = normalizeLabels(config);
18
- const primaryLabel = labels[0] ?? "";
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
- return createdIds;
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/persistence.ts
120
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
121
- import { join } from "path";
122
- function plansDir(workspace) {
123
- return join(workspace, ".lisa", "plans");
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 savePlan(workspace, plan) {
126
- const dir = plansDir(workspace);
127
- mkdirSync(dir, { recursive: true });
128
- const filename = `${plan.createdAt.replace(/[:.]/g, "-")}.json`;
129
- const filePath = join(dir, filename);
130
- writeFileSync(filePath, JSON.stringify(plan, null, 2));
131
- return filePath;
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 loadPlan(filePath) {
134
- if (!existsSync(filePath)) return null;
136
+ function tryParseStructured(jsonStr) {
135
137
  try {
136
- return JSON.parse(readFileSync(filePath, "utf-8"));
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 loadLatestPlan(workspace) {
142
- const dir = plansDir(workspace);
143
- if (!existsSync(dir)) return null;
144
- const files = readdirSync(dir).filter((f) => f.endsWith(".json")).sort().reverse();
145
- for (const file of files) {
146
- const filePath = join(dir, file);
147
- const plan = loadPlan(filePath);
148
- if (plan && plan.status !== "created") return [plan, filePath];
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
- return null;
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 join2 } from "path";
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 = mkdtempSync(join2(tmpdir(), "lisa-edit-"));
284
- const tmpFile = join2(tmpDir, "issue.md");
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
- parsePlanResponse,
400
- PlanParseError,
758
+ buildPlanningPrompt,
759
+ generatePlan,
401
760
  savePlan,
402
761
  loadLatestPlan,
403
- buildPlanningPrompt,
404
762
  runPlanWizard,
405
763
  issueToMarkdown,
406
764
  markdownToIssue