@tarcisiopgs/lisa 1.26.2 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -5
- package/dist/chunk-2TW2MJXF.js +116 -0
- package/dist/{chunk-CDI22S63.js → chunk-3EOEDL3T.js} +2 -2
- package/dist/{chunk-AGYOJQBR.js → chunk-42NKASE3.js} +8 -106
- package/dist/{chunk-235DMOXJ.js → chunk-5N4BWHIT.js} +186 -116
- package/dist/chunk-FCEUJ7VK.js +407 -0
- package/dist/chunk-UXVSQQID.js +3924 -0
- package/dist/chunk-W73XGHD4.js +3059 -0
- package/dist/{detection-OWEDBW7B.js → detection-JT7HSKSX.js} +2 -1
- package/dist/{guardrails-6IG2C4KJ.js → guardrails-IX3VVUO5.js} +1 -1
- package/dist/index.js +1474 -7340
- package/dist/{kanban-PSOTFEQI.js → kanban-2WMA5VPQ.js} +551 -116
- package/dist/loop-KNJIRK7T.js +21 -0
- package/dist/tui-bridge-DCC4JAPM.js +164 -0
- package/package.json +5 -4
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildContextMdBlock,
|
|
4
|
+
readContext
|
|
5
|
+
} from "./chunk-UXVSQQID.js";
|
|
6
|
+
import {
|
|
7
|
+
normalizeLabels,
|
|
8
|
+
ok,
|
|
9
|
+
warn
|
|
10
|
+
} from "./chunk-5N4BWHIT.js";
|
|
11
|
+
|
|
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
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return createdIds;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/plan/parser.ts
|
|
67
|
+
function parsePlanResponse(raw) {
|
|
68
|
+
let cleaned = raw.trim();
|
|
69
|
+
const fenceMatch = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
70
|
+
if (fenceMatch) {
|
|
71
|
+
cleaned = fenceMatch[1].trim();
|
|
72
|
+
}
|
|
73
|
+
const jsonStart = cleaned.indexOf("{");
|
|
74
|
+
const jsonEnd = cleaned.lastIndexOf("}");
|
|
75
|
+
if (jsonStart === -1 || jsonEnd === -1) {
|
|
76
|
+
throw new PlanParseError("No JSON object found in AI response");
|
|
77
|
+
}
|
|
78
|
+
cleaned = cleaned.slice(jsonStart, jsonEnd + 1);
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(cleaned);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
throw new PlanParseError(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(parsed.issues) || parsed.issues.length === 0) {
|
|
86
|
+
throw new PlanParseError("Response must contain a non-empty 'issues' array");
|
|
87
|
+
}
|
|
88
|
+
if (parsed.issues.length > 12) {
|
|
89
|
+
throw new PlanParseError(
|
|
90
|
+
`Too many issues (${parsed.issues.length}). Decompose into 2-8 atomic issues.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const issues = parsed.issues.map((issue, idx) => {
|
|
94
|
+
if (!issue.title || typeof issue.title !== "string") {
|
|
95
|
+
throw new PlanParseError(`Issue ${idx + 1}: missing or invalid 'title'`);
|
|
96
|
+
}
|
|
97
|
+
if (!issue.description || typeof issue.description !== "string") {
|
|
98
|
+
throw new PlanParseError(`Issue ${idx + 1}: missing or invalid 'description'`);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
title: issue.title,
|
|
102
|
+
description: issue.description,
|
|
103
|
+
acceptanceCriteria: Array.isArray(issue.acceptanceCriteria) ? issue.acceptanceCriteria.filter((c) => typeof c === "string") : [],
|
|
104
|
+
relevantFiles: Array.isArray(issue.relevantFiles) ? issue.relevantFiles.filter((f) => typeof f === "string") : [],
|
|
105
|
+
order: typeof issue.order === "number" ? issue.order : idx + 1,
|
|
106
|
+
dependsOn: Array.isArray(issue.dependsOn) ? issue.dependsOn.filter((d) => typeof d === "number") : [],
|
|
107
|
+
repo: typeof issue.repo === "string" ? issue.repo : void 0
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
return issues;
|
|
111
|
+
}
|
|
112
|
+
var PlanParseError = class extends Error {
|
|
113
|
+
constructor(message) {
|
|
114
|
+
super(message);
|
|
115
|
+
this.name = "PlanParseError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
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");
|
|
124
|
+
}
|
|
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;
|
|
132
|
+
}
|
|
133
|
+
function loadPlan(filePath) {
|
|
134
|
+
if (!existsSync(filePath)) return null;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
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];
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/plan/prompt.ts
|
|
154
|
+
import { resolve } from "path";
|
|
155
|
+
function buildPlanningPrompt(goal, config, parentIssueDescription) {
|
|
156
|
+
const workspace = resolve(config.workspace);
|
|
157
|
+
const contextMd = readContext(workspace);
|
|
158
|
+
const contextBlock = buildContextMdBlock(contextMd);
|
|
159
|
+
const repoBlock = config.repos.length > 0 ? config.repos.map(
|
|
160
|
+
(r) => `- **${r.name}**: \`${resolve(workspace, r.path)}\` (base: \`${r.base_branch}\`)`
|
|
161
|
+
).join("\n") : `- **${workspace}** (single-repo, base: \`${config.base_branch}\`)`;
|
|
162
|
+
const parentBlock = parentIssueDescription ? `
|
|
163
|
+
## Parent Issue Description
|
|
164
|
+
|
|
165
|
+
${parentIssueDescription}
|
|
166
|
+
` : "";
|
|
167
|
+
return `You are a project planning agent. Your job is to decompose a high-level goal into atomic, implementable issues.
|
|
168
|
+
|
|
169
|
+
## Goal
|
|
170
|
+
|
|
171
|
+
${goal}
|
|
172
|
+
${parentBlock}
|
|
173
|
+
## Available Repositories
|
|
174
|
+
|
|
175
|
+
${repoBlock}
|
|
176
|
+
${contextBlock}
|
|
177
|
+
## Instructions
|
|
178
|
+
|
|
179
|
+
Analyze the goal and the codebase context above. Decompose the goal into **2-8 atomic issues** that can each be completed in a single AI coding session (under 1 hour of agent work).
|
|
180
|
+
|
|
181
|
+
For each issue, provide:
|
|
182
|
+
- **title**: Short, descriptive title (imperative: "Add X", "Fix Y", "Create Z")
|
|
183
|
+
- **description**: Full markdown description with context, approach, and acceptance criteria as a \`- [ ]\` checklist
|
|
184
|
+
- **acceptanceCriteria**: Array of the checklist items as plain strings
|
|
185
|
+
- **relevantFiles**: Array of file paths in the codebase that will be modified or created
|
|
186
|
+
- **order**: Integer (1-based) \u2014 execution order based on dependencies
|
|
187
|
+
- **dependsOn**: Array of order numbers this issue depends on (empty if independent)
|
|
188
|
+
${config.repos.length > 1 ? "- **repo**: Name of the target repository from the list above (required for multi-repo)\n" : ""}
|
|
189
|
+
## Rules
|
|
190
|
+
|
|
191
|
+
1. Each issue MUST be self-contained and completable in a single session
|
|
192
|
+
2. Each issue MUST have at least 2 acceptance criteria
|
|
193
|
+
3. Each issue MUST reference specific file paths (existing or to be created)
|
|
194
|
+
4. Issues MUST include test expectations in their acceptance criteria
|
|
195
|
+
5. Order issues so dependencies come first (lower order = executes first)
|
|
196
|
+
6. Use clear, specific titles \u2014 not vague ("Improve X" is bad, "Add rate limit middleware to /api/users" is good)
|
|
197
|
+
7. Output ONLY valid JSON \u2014 no markdown code fences, no explanation text
|
|
198
|
+
|
|
199
|
+
## Output Format
|
|
200
|
+
|
|
201
|
+
Respond with ONLY this JSON structure (no wrapping, no markdown):
|
|
202
|
+
|
|
203
|
+
{"issues":[{"title":"...","description":"...","acceptanceCriteria":["..."],"relevantFiles":["..."],"order":1,"dependsOn":[]${config.repos.length > 1 ? ',"repo":"..."' : ""}}]}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/plan/wizard.ts
|
|
207
|
+
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";
|
|
211
|
+
import * as clack from "@clack/prompts";
|
|
212
|
+
import pc from "picocolors";
|
|
213
|
+
async function runPlanWizard(plan, planPath, opts) {
|
|
214
|
+
const workspace = opts.config.workspace;
|
|
215
|
+
while (true) {
|
|
216
|
+
displayPlan(plan);
|
|
217
|
+
const action = await clack.select({
|
|
218
|
+
message: "What would you like to do?",
|
|
219
|
+
options: [
|
|
220
|
+
{ value: "approve", label: `${pc.green("Approve all")} \u2014 create issues in source` },
|
|
221
|
+
{ value: "edit", label: `${pc.yellow("Edit")} \u2014 edit an issue in $EDITOR` },
|
|
222
|
+
{ value: "delete", label: `${pc.red("Delete")} \u2014 remove an issue` },
|
|
223
|
+
{ value: "reorder", label: `${pc.cyan("Reorder")} \u2014 change execution order` },
|
|
224
|
+
{ value: "cancel", label: `${pc.gray("Cancel")} \u2014 save and exit` }
|
|
225
|
+
]
|
|
226
|
+
});
|
|
227
|
+
if (clack.isCancel(action) || action === "cancel") {
|
|
228
|
+
plan.status = "draft";
|
|
229
|
+
savePlan(workspace, plan);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (action === "approve") {
|
|
233
|
+
plan.status = "approved";
|
|
234
|
+
savePlan(workspace, plan);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (action === "edit") {
|
|
238
|
+
await editIssue(plan, workspace);
|
|
239
|
+
savePlan(workspace, plan);
|
|
240
|
+
}
|
|
241
|
+
if (action === "delete") {
|
|
242
|
+
await deleteIssue(plan, workspace);
|
|
243
|
+
savePlan(workspace, plan);
|
|
244
|
+
}
|
|
245
|
+
if (action === "reorder") {
|
|
246
|
+
await reorderIssues(plan, workspace);
|
|
247
|
+
savePlan(workspace, plan);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function displayPlan(plan) {
|
|
252
|
+
clack.log.info(`${pc.bold("Goal:")} ${plan.goal}`);
|
|
253
|
+
clack.log.info("");
|
|
254
|
+
const sorted = [...plan.issues].sort((a, b) => a.order - b.order);
|
|
255
|
+
for (const issue of sorted) {
|
|
256
|
+
const deps = issue.dependsOn.length > 0 ? pc.gray(` \u2192 depends on: ${issue.dependsOn.map((d) => `#${d}`).join(", ")}`) : "";
|
|
257
|
+
const repo = issue.repo ? pc.cyan(` [${issue.repo}]`) : "";
|
|
258
|
+
const files = issue.relevantFiles.length > 0 ? pc.gray(
|
|
259
|
+
`
|
|
260
|
+
Files: ${issue.relevantFiles.slice(0, 3).join(", ")}${issue.relevantFiles.length > 3 ? ` +${issue.relevantFiles.length - 3}` : ""}`
|
|
261
|
+
) : "";
|
|
262
|
+
clack.log.info(
|
|
263
|
+
` ${pc.yellow(String(issue.order))}. ${pc.bold(issue.title)}${repo}${deps}${files}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
clack.log.info("");
|
|
267
|
+
}
|
|
268
|
+
async function editIssue(plan, workspace) {
|
|
269
|
+
if (plan.issues.length === 0) {
|
|
270
|
+
clack.log.warning("No issues to edit.");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const choice = await clack.select({
|
|
274
|
+
message: "Which issue to edit?",
|
|
275
|
+
options: plan.issues.map((issue2) => ({
|
|
276
|
+
value: issue2.order,
|
|
277
|
+
label: `${issue2.order}. ${issue2.title}`
|
|
278
|
+
}))
|
|
279
|
+
});
|
|
280
|
+
if (clack.isCancel(choice)) return;
|
|
281
|
+
const issue = plan.issues.find((i) => i.order === choice);
|
|
282
|
+
if (!issue) return;
|
|
283
|
+
const tmpDir = mkdtempSync(join2(tmpdir(), "lisa-edit-"));
|
|
284
|
+
const tmpFile = join2(tmpDir, "issue.md");
|
|
285
|
+
writeFileSync2(tmpFile, issueToMarkdown(issue));
|
|
286
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
287
|
+
try {
|
|
288
|
+
execSync(`${editor} "${tmpFile}"`, { stdio: "inherit" });
|
|
289
|
+
} catch {
|
|
290
|
+
clack.log.error("Editor failed. Issue unchanged.");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const content = readFileSync2(tmpFile, "utf-8");
|
|
294
|
+
const updated = markdownToIssue(content, issue);
|
|
295
|
+
Object.assign(issue, updated);
|
|
296
|
+
clack.log.success(`Updated: ${issue.title}`);
|
|
297
|
+
}
|
|
298
|
+
async function deleteIssue(plan, _workspace) {
|
|
299
|
+
if (plan.issues.length === 0) {
|
|
300
|
+
clack.log.warning("No issues to delete.");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const choice = await clack.select({
|
|
304
|
+
message: "Which issue to delete?",
|
|
305
|
+
options: plan.issues.map((issue) => ({
|
|
306
|
+
value: issue.order,
|
|
307
|
+
label: `${issue.order}. ${issue.title}`
|
|
308
|
+
}))
|
|
309
|
+
});
|
|
310
|
+
if (clack.isCancel(choice)) return;
|
|
311
|
+
const idx = plan.issues.findIndex((i) => i.order === choice);
|
|
312
|
+
if (idx === -1) return;
|
|
313
|
+
const removed = plan.issues.splice(idx, 1)[0];
|
|
314
|
+
for (const issue of plan.issues) {
|
|
315
|
+
issue.dependsOn = issue.dependsOn.filter((d) => d !== removed.order);
|
|
316
|
+
}
|
|
317
|
+
clack.log.success(`Deleted: ${removed.title}`);
|
|
318
|
+
}
|
|
319
|
+
async function reorderIssues(plan, _workspace) {
|
|
320
|
+
if (plan.issues.length <= 1) {
|
|
321
|
+
clack.log.warning("Need at least 2 issues to reorder.");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
clack.log.info("Enter new order (comma-separated issue numbers):");
|
|
325
|
+
clack.log.info(
|
|
326
|
+
`Current: ${plan.issues.sort((a, b) => a.order - b.order).map((i) => i.order).join(", ")}`
|
|
327
|
+
);
|
|
328
|
+
const answer = await clack.text({
|
|
329
|
+
message: "New order (e.g., 3,1,2,4):",
|
|
330
|
+
validate: (input = "") => {
|
|
331
|
+
const nums = input.split(",").map((n) => Number(n.trim()));
|
|
332
|
+
if (nums.some(Number.isNaN)) return "Enter comma-separated numbers";
|
|
333
|
+
if (nums.length !== plan.issues.length) return `Expected ${plan.issues.length} numbers`;
|
|
334
|
+
return void 0;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
if (clack.isCancel(answer)) return;
|
|
338
|
+
const newOrder = answer.split(",").map((n) => Number(n.trim()));
|
|
339
|
+
const oldOrderMap = /* @__PURE__ */ new Map();
|
|
340
|
+
for (const issue of plan.issues) {
|
|
341
|
+
oldOrderMap.set(issue.order, issue);
|
|
342
|
+
}
|
|
343
|
+
for (let i = 0; i < newOrder.length; i++) {
|
|
344
|
+
const issue = oldOrderMap.get(newOrder[i]);
|
|
345
|
+
if (issue) {
|
|
346
|
+
issue.order = i + 1;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
clack.log.success("Issues reordered.");
|
|
350
|
+
}
|
|
351
|
+
function issueToMarkdown(issue) {
|
|
352
|
+
let md = `# ${issue.title}
|
|
353
|
+
|
|
354
|
+
`;
|
|
355
|
+
md += `${issue.description}
|
|
356
|
+
`;
|
|
357
|
+
if (issue.relevantFiles.length > 0) {
|
|
358
|
+
md += `
|
|
359
|
+
## Relevant Files
|
|
360
|
+
|
|
361
|
+
`;
|
|
362
|
+
for (const f of issue.relevantFiles) {
|
|
363
|
+
md += `- ${f}
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return md;
|
|
368
|
+
}
|
|
369
|
+
function markdownToIssue(content, original) {
|
|
370
|
+
const lines = content.split("\n");
|
|
371
|
+
const titleLine = lines.find((l) => l.startsWith("# "));
|
|
372
|
+
const title = titleLine ? titleLine.replace(/^#\s+/, "").trim() : original.title;
|
|
373
|
+
const titleIdx = lines.indexOf(titleLine ?? "");
|
|
374
|
+
const filesIdx = lines.findIndex((l) => l.startsWith("## Relevant Files"));
|
|
375
|
+
const descLines = filesIdx > 0 ? lines.slice(titleIdx + 1, filesIdx) : lines.slice(titleIdx + 1);
|
|
376
|
+
const description = descLines.join("\n").trim();
|
|
377
|
+
const acceptanceCriteria = descLines.filter((l) => l.trim().startsWith("- [ ]") || l.trim().startsWith("- [x]")).map((l) => l.trim().replace(/^- \[[ x]\]\s*/, ""));
|
|
378
|
+
const relevantFiles = [];
|
|
379
|
+
if (filesIdx > 0) {
|
|
380
|
+
for (let i = filesIdx + 1; i < lines.length; i++) {
|
|
381
|
+
const line = lines[i].trim();
|
|
382
|
+
if (line.startsWith("- ")) {
|
|
383
|
+
relevantFiles.push(line.replace(/^- /, ""));
|
|
384
|
+
} else if (line.startsWith("#")) {
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
title,
|
|
391
|
+
description: description || original.description,
|
|
392
|
+
acceptanceCriteria: acceptanceCriteria.length > 0 ? acceptanceCriteria : original.acceptanceCriteria,
|
|
393
|
+
relevantFiles: relevantFiles.length > 0 ? relevantFiles : original.relevantFiles
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export {
|
|
398
|
+
createPlanIssues,
|
|
399
|
+
parsePlanResponse,
|
|
400
|
+
PlanParseError,
|
|
401
|
+
savePlan,
|
|
402
|
+
loadLatestPlan,
|
|
403
|
+
buildPlanningPrompt,
|
|
404
|
+
runPlanWizard,
|
|
405
|
+
issueToMarkdown,
|
|
406
|
+
markdownToIssue
|
|
407
|
+
};
|