@tarcisiopgs/lisa 1.35.0 → 1.36.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.
@@ -65,7 +65,9 @@ function parsePlanResponse(raw) {
65
65
  relevantFiles: Array.isArray(issue.relevantFiles) ? issue.relevantFiles.filter((f) => typeof f === "string") : [],
66
66
  order: typeof issue.order === "number" ? issue.order : idx + 1,
67
67
  dependsOn: Array.isArray(issue.dependsOn) ? issue.dependsOn.filter((d) => typeof d === "number") : [],
68
- repo: typeof issue.repo === "string" ? issue.repo : void 0
68
+ repo: typeof issue.repo === "string" ? issue.repo : void 0,
69
+ verifyCommand: typeof issue.verifyCommand === "string" ? issue.verifyCommand : void 0,
70
+ doneCriteria: typeof issue.doneCriteria === "string" ? issue.doneCriteria : void 0
69
71
  };
70
72
  });
71
73
  return issues;
@@ -175,6 +177,20 @@ async function createPlanIssues(source, config, plan, workspace) {
175
177
  const orderToId = /* @__PURE__ */ new Map();
176
178
  for (const issue of sorted) {
177
179
  let description = ensureAcceptanceCriteria(issue.description, issue.acceptanceCriteria);
180
+ if (issue.verifyCommand) {
181
+ description += `
182
+
183
+ ## Verify Command
184
+
185
+ \`\`\`bash
186
+ ${issue.verifyCommand}
187
+ \`\`\`
188
+ `;
189
+ if (issue.doneCriteria) {
190
+ description += `Expected: ${issue.doneCriteria}
191
+ `;
192
+ }
193
+ }
178
194
  if (issue.dependsOn.length > 0 && !source.linkDependency) {
179
195
  const depRefs = issue.dependsOn.map((depOrder) => {
180
196
  const depId = orderToId.get(depOrder);
@@ -275,6 +291,8 @@ For each issue, provide:
275
291
  - **relevantFiles**: Array of file paths in the codebase that will be modified or created
276
292
  - **order**: Integer (1-based) \u2014 execution order based on dependencies
277
293
  - **dependsOn**: Array of order numbers this issue depends on (empty if independent)
294
+ - **verifyCommand**: A shell command that validates the issue is complete (e.g., \`npm test\`, \`npx tsc --noEmit\`, \`curl localhost:3000/health\`)
295
+ - **doneCriteria**: What success looks like when the verify command runs (e.g., "All tests pass", "Returns 200 OK")
278
296
  ${config.repos.length > 1 ? "- **repo**: Name of the target repository from the list above (required for multi-repo)\n" : ""}
279
297
  ## Rules
280
298
 
@@ -284,13 +302,14 @@ ${config.repos.length > 1 ? "- **repo**: Name of the target repository from the
284
302
  4. Issues MUST include test expectations in their acceptance criteria
285
303
  5. Order issues so dependencies come first (lower order = executes first)
286
304
  6. Use clear, specific titles \u2014 not vague ("Improve X" is bad, "Add rate limit middleware to /api/users" is good)
287
- 7. Output ONLY valid JSON \u2014 no markdown code fences, no explanation text
305
+ 7. Each issue SHOULD include a verifyCommand that can programmatically validate completion
306
+ 8. Output ONLY valid JSON \u2014 no markdown code fences, no explanation text
288
307
 
289
308
  ## Output Format
290
309
 
291
310
  Respond with ONLY this JSON structure (no wrapping, no markdown):
292
311
 
293
- {"issues":[{"title":"...","description":"...","acceptanceCriteria":["..."],"relevantFiles":["..."],"order":1,"dependsOn":[]${config.repos.length > 1 ? ',"repo":"..."' : ""}}]}`;
312
+ {"issues":[{"title":"...","description":"...","acceptanceCriteria":["..."],"relevantFiles":["..."],"order":1,"dependsOn":[],"verifyCommand":"...","doneCriteria":"..."${config.repos.length > 1 ? ',"repo":"..."' : ""}}]}`;
294
313
  }
295
314
 
296
315
  // src/plan/persistence.ts
@@ -404,6 +423,40 @@ Please output ONLY valid JSON with the exact structure specified above.`;
404
423
  );
405
424
  }
406
425
 
426
+ // src/plan/waves.ts
427
+ function buildExecutionWaves(issues) {
428
+ if (issues.length === 0) return [];
429
+ const remaining = /* @__PURE__ */ new Map();
430
+ for (const issue of issues) {
431
+ remaining.set(issue.order, new Set(issue.dependsOn));
432
+ }
433
+ const waves = [];
434
+ while (remaining.size > 0) {
435
+ const wave = [];
436
+ for (const [order, deps] of remaining) {
437
+ if (deps.size === 0) {
438
+ wave.push(order);
439
+ }
440
+ }
441
+ if (wave.length === 0) {
442
+ waves.push([...remaining.keys()].sort((a, b) => a - b));
443
+ break;
444
+ }
445
+ wave.sort((a, b) => a - b);
446
+ waves.push(wave);
447
+ const assigned = new Set(wave);
448
+ for (const order of wave) {
449
+ remaining.delete(order);
450
+ }
451
+ for (const deps of remaining.values()) {
452
+ for (const a of assigned) {
453
+ deps.delete(a);
454
+ }
455
+ }
456
+ }
457
+ return waves;
458
+ }
459
+
407
460
  // src/plan/wizard.ts
408
461
  async function runPlanWizard(plan, planPath, opts) {
409
462
  const workspace = opts.config.workspace;
@@ -457,17 +510,29 @@ async function runPlanWizard(plan, planPath, opts) {
457
510
  function displayPlan(plan) {
458
511
  clack.log.info(`${pc.bold("Goal:")} ${plan.goal}`);
459
512
  clack.log.info("");
513
+ const waves = buildExecutionWaves(plan.issues);
460
514
  const sorted = [...plan.issues].sort((a, b) => a.order - b.order);
461
- for (const issue of sorted) {
462
- const deps = issue.dependsOn.length > 0 ? pc.gray(` \u2192 depends on: ${issue.dependsOn.map((d) => `#${d}`).join(", ")}`) : "";
463
- const repo = issue.repo ? pc.cyan(` [${issue.repo}]`) : "";
464
- const files = issue.relevantFiles.length > 0 ? pc.gray(
465
- `
515
+ for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) {
516
+ const wave = waves[waveIdx];
517
+ const label = wave.length > 1 ? "parallel" : "sequential";
518
+ clack.log.info(pc.dim(` Wave ${waveIdx + 1} (${label}):`));
519
+ for (const orderNum of wave) {
520
+ const issue = sorted.find((i) => i.order === orderNum);
521
+ if (!issue) continue;
522
+ const deps = issue.dependsOn.length > 0 ? pc.gray(` \u2192 depends on: ${issue.dependsOn.map((d) => `#${d}`).join(", ")}`) : "";
523
+ const repo = issue.repo ? pc.cyan(` [${issue.repo}]`) : "";
524
+ const files = issue.relevantFiles.length > 0 ? pc.gray(
525
+ `
466
526
  Files: ${issue.relevantFiles.slice(0, 3).join(", ")}${issue.relevantFiles.length > 3 ? ` +${issue.relevantFiles.length - 3}` : ""}`
467
- ) : "";
468
- clack.log.info(
469
- ` ${pc.yellow(String(issue.order))}. ${pc.bold(issue.title)}${repo}${deps}${files}`
470
- );
527
+ ) : "";
528
+ const verify = issue.verifyCommand ? pc.gray(`
529
+ Verify: ${issue.verifyCommand}`) : "";
530
+ const criteria = issue.acceptanceCriteria.length > 0 ? pc.gray(`
531
+ Criteria: ${issue.acceptanceCriteria.length} item(s)`) : "";
532
+ clack.log.info(
533
+ ` ${pc.yellow(String(issue.order))}. ${pc.bold(issue.title)}${repo}${deps}${files}${criteria}${verify}`
534
+ );
535
+ }
471
536
  }
472
537
  clack.log.info("");
473
538
  }
@@ -542,15 +607,13 @@ async function reorderIssues(plan, _workspace) {
542
607
  });
543
608
  if (clack.isCancel(answer)) return;
544
609
  const newOrder = answer.split(",").map((n) => Number(n.trim()));
545
- const oldOrderMap = /* @__PURE__ */ new Map();
546
- for (const issue of plan.issues) {
547
- oldOrderMap.set(issue.order, issue);
548
- }
610
+ const oldToNew = /* @__PURE__ */ new Map();
549
611
  for (let i = 0; i < newOrder.length; i++) {
550
- const issue = oldOrderMap.get(newOrder[i]);
551
- if (issue) {
552
- issue.order = i + 1;
553
- }
612
+ oldToNew.set(newOrder[i], i + 1);
613
+ }
614
+ for (const issue of plan.issues) {
615
+ issue.order = oldToNew.get(issue.order) ?? issue.order;
616
+ issue.dependsOn = issue.dependsOn.map((d) => oldToNew.get(d) ?? d).filter((d) => d !== issue.order);
554
617
  }
555
618
  clack.log.success("Issues reordered.");
556
619
  }
@@ -582,6 +645,14 @@ function issueToMarkdown(issue) {
582
645
  `;
583
646
  md += `${issue.description}
584
647
  `;
648
+ if (issue.dependsOn.length > 0) {
649
+ md += `
650
+ ## Depends On
651
+
652
+ `;
653
+ md += `${issue.dependsOn.join(", ")}
654
+ `;
655
+ }
585
656
  if (issue.relevantFiles.length > 0) {
586
657
  md += `
587
658
  ## Relevant Files
@@ -589,6 +660,21 @@ function issueToMarkdown(issue) {
589
660
  `;
590
661
  for (const f of issue.relevantFiles) {
591
662
  md += `- ${f}
663
+ `;
664
+ }
665
+ }
666
+ if (issue.verifyCommand) {
667
+ md += `
668
+ ## Verify
669
+
670
+ `;
671
+ md += `\`\`\`bash
672
+ ${issue.verifyCommand}
673
+ \`\`\`
674
+ `;
675
+ if (issue.doneCriteria) {
676
+ md += `
677
+ Expected: ${issue.doneCriteria}
592
678
  `;
593
679
  }
594
680
  }
@@ -599,10 +685,24 @@ function markdownToIssue(content, original) {
599
685
  const titleLine = lines.find((l) => l.startsWith("# "));
600
686
  const title = titleLine ? titleLine.replace(/^#\s+/, "").trim() : original.title;
601
687
  const titleIdx = lines.indexOf(titleLine ?? "");
688
+ const depsIdx = lines.findIndex((l) => l.startsWith("## Depends On"));
602
689
  const filesIdx = lines.findIndex((l) => l.startsWith("## Relevant Files"));
603
- const descLines = filesIdx > 0 ? lines.slice(titleIdx + 1, filesIdx) : lines.slice(titleIdx + 1);
690
+ const verifyIdx = lines.findIndex((l) => l.startsWith("## Verify"));
691
+ const sectionBoundaries = [depsIdx, filesIdx, verifyIdx].filter((i) => i > 0);
692
+ const descEnd = sectionBoundaries.length > 0 ? Math.min(...sectionBoundaries) : -1;
693
+ const descLines = descEnd > 0 ? lines.slice(titleIdx + 1, descEnd) : lines.slice(titleIdx + 1);
604
694
  const description = descLines.join("\n").trim();
605
695
  const acceptanceCriteria = descLines.filter((l) => l.trim().startsWith("- [ ]") || l.trim().startsWith("- [x]")).map((l) => l.trim().replace(/^- \[[ x]\]\s*/, ""));
696
+ let dependsOn;
697
+ if (depsIdx > 0) {
698
+ const nextSection = [filesIdx, verifyIdx].filter((i) => i > depsIdx);
699
+ const depsEnd = nextSection.length > 0 ? Math.min(...nextSection) : lines.length;
700
+ const depsContent = lines.slice(depsIdx + 1, depsEnd).join(" ").trim();
701
+ if (depsContent) {
702
+ const parsed = depsContent.split(/[,\s]+/).map((s) => Number.parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
703
+ if (parsed.length > 0) dependsOn = parsed;
704
+ }
705
+ }
606
706
  const relevantFiles = [];
607
707
  if (filesIdx > 0) {
608
708
  for (let i = filesIdx + 1; i < lines.length; i++) {
@@ -614,11 +714,28 @@ function markdownToIssue(content, original) {
614
714
  }
615
715
  }
616
716
  }
717
+ let verifyCommand;
718
+ let doneCriteria;
719
+ if (verifyIdx > 0) {
720
+ const verifyLines = lines.slice(verifyIdx + 1);
721
+ const codeStart = verifyLines.findIndex((l) => l.trim().startsWith("```"));
722
+ const codeEnd = verifyLines.findIndex((l, i) => i > codeStart && l.trim().startsWith("```"));
723
+ if (codeStart >= 0 && codeEnd > codeStart) {
724
+ verifyCommand = verifyLines.slice(codeStart + 1, codeEnd).join("\n").trim();
725
+ }
726
+ const expectedLine = verifyLines.find((l) => l.trim().startsWith("Expected:"));
727
+ if (expectedLine) {
728
+ doneCriteria = expectedLine.trim().replace(/^Expected:\s*/, "");
729
+ }
730
+ }
617
731
  return {
618
732
  title,
619
733
  description: description || original.description,
620
734
  acceptanceCriteria: acceptanceCriteria.length > 0 ? acceptanceCriteria : original.acceptanceCriteria,
621
- relevantFiles: relevantFiles.length > 0 ? relevantFiles : original.relevantFiles
735
+ relevantFiles: relevantFiles.length > 0 ? relevantFiles : original.relevantFiles,
736
+ ...dependsOn ? { dependsOn } : {},
737
+ ...verifyCommand ? { verifyCommand } : {},
738
+ ...doneCriteria ? { doneCriteria } : {}
622
739
  };
623
740
  }
624
741
 
@@ -630,6 +747,7 @@ export {
630
747
  generatePlan,
631
748
  savePlan,
632
749
  loadLatestPlan,
750
+ buildExecutionWaves,
633
751
  runPlanWizard,
634
752
  issueToMarkdown,
635
753
  markdownToIssue
@@ -255,6 +255,7 @@ function loadConfig(cwd = process.cwd()) {
255
255
  const rawReviewMonitor = parsed.review_monitor;
256
256
  const rawReactions = parsed.reactions;
257
257
  const rawSpecCompliance = parsed.spec_compliance;
258
+ const rawPlanValidation = parsed.plan_validation;
258
259
  const rawProgress = parsed.progress_comments;
259
260
  const rawPr = parsed.pr;
260
261
  const config = {
@@ -309,6 +310,10 @@ function loadConfig(cwd = process.cwd()) {
309
310
  max_retries: rawSpecCompliance.max_retries,
310
311
  block_on_failure: rawSpecCompliance.block_on_failure
311
312
  } : void 0,
313
+ plan_validation: rawPlanValidation ? {
314
+ enabled: rawPlanValidation.enabled ?? false,
315
+ max_iterations: rawPlanValidation.max_iterations
316
+ } : void 0,
312
317
  progress_comments: rawProgress ? { enabled: rawProgress.enabled ?? false } : void 0,
313
318
  pr: parsePrConfig(rawPr),
314
319
  provider_options: {
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CliError,
4
+ buildExecutionWaves,
4
5
  createPlanIssues,
5
6
  generatePlan,
6
7
  loadLatestPlan,
7
8
  parseStructuredOutput,
8
9
  runPlanWizard,
9
10
  savePlan
10
- } from "./chunk-6SNT7TND.js";
11
+ } from "./chunk-AFKXWCAM.js";
11
12
  import {
12
13
  detectDefaultBranch,
13
14
  detectGitRepos,
@@ -37,7 +38,7 @@ import {
37
38
  runLoop,
38
39
  saveConfig,
39
40
  validateConfig
40
- } from "./chunk-BWND35E5.js";
41
+ } from "./chunk-PGIXWLQT.js";
41
42
  import {
42
43
  buildContextMdBlock,
43
44
  createProvider,
@@ -58,7 +59,8 @@ import {
58
59
  ok,
59
60
  setLogLevel,
60
61
  setOutputMode,
61
- updateNotice
62
+ updateNotice,
63
+ warn
62
64
  } from "./chunk-V44FTYWZ.js";
63
65
  import "./chunk-3EOEDL3T.js";
64
66
  import {
@@ -955,6 +957,14 @@ function runAdvancedChecks(config2) {
955
957
  category: "advanced"
956
958
  });
957
959
  }
960
+ if (config2.plan_validation?.enabled) {
961
+ const maxIter = config2.plan_validation.max_iterations ?? 2;
962
+ results.push({
963
+ passed: true,
964
+ label: `Plan validation is enabled (max ${maxIter} iteration${maxIter !== 1 ? "s" : ""})`,
965
+ category: "advanced"
966
+ });
967
+ }
958
968
  if (config2.hooks) {
959
969
  for (const [name, cmd] of Object.entries(config2.hooks)) {
960
970
  if (name === "timeout" || !cmd || typeof cmd !== "string") continue;
@@ -1293,7 +1303,7 @@ var init = defineCommand5({
1293
1303
  // src/cli/commands/issue.ts
1294
1304
  import { defineCommand as defineCommand6 } from "citty";
1295
1305
  function sleep(ms) {
1296
- return new Promise((resolve6) => setTimeout(resolve6, ms));
1306
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
1297
1307
  }
1298
1308
  var issueGet = defineCommand6({
1299
1309
  meta: { name: "get", description: "Fetch full issue details as JSON" },
@@ -1370,7 +1380,7 @@ var issue = defineCommand6({
1370
1380
  import { defineCommand as defineCommand7 } from "citty";
1371
1381
 
1372
1382
  // src/plan/index.ts
1373
- import { resolve as resolve4 } from "path";
1383
+ import { resolve as resolve5 } from "path";
1374
1384
  import * as clack4 from "@clack/prompts";
1375
1385
 
1376
1386
  // src/plan/brainstorm.ts
@@ -1455,10 +1465,230 @@ ALWAYS respond with a single JSON object (no markdown fences, no extra text):
1455
1465
  Output ONLY the JSON object.`;
1456
1466
  }
1457
1467
 
1468
+ // src/plan/plan-checker.ts
1469
+ import { mkdtempSync as mkdtempSync2 } from "fs";
1470
+ import { tmpdir as tmpdir2 } from "os";
1471
+ import { join as join3, resolve as resolve4 } from "path";
1472
+ function buildPlanValidationPrompt(goal, issues, contextMd) {
1473
+ const contextBlock = buildContextMdBlock(contextMd);
1474
+ const issuesBlock = issues.map((issue2) => {
1475
+ const deps = issue2.dependsOn.length > 0 ? ` (depends on: ${issue2.dependsOn.join(", ")})` : "";
1476
+ const verify = issue2.verifyCommand ? `
1477
+ Verify: ${issue2.verifyCommand}` : "";
1478
+ const done = issue2.doneCriteria ? `
1479
+ Done: ${issue2.doneCriteria}` : "";
1480
+ const files = issue2.relevantFiles.length > 0 ? `
1481
+ Files: ${issue2.relevantFiles.join(", ")}` : "";
1482
+ const criteria = issue2.acceptanceCriteria.length > 0 ? `
1483
+ Criteria: ${issue2.acceptanceCriteria.join("; ")}` : "";
1484
+ return `${issue2.order}. ${issue2.title}${deps}${files}${criteria}${verify}${done}`;
1485
+ }).join("\n\n");
1486
+ return `You are a plan quality validator. Your ONLY task is to evaluate whether an implementation plan is well-structured and complete. Do NOT modify any files or run any commands.
1487
+
1488
+ Always respond in the same language the user wrote their goal in.
1489
+
1490
+ ## Goal
1491
+
1492
+ ${goal}
1493
+ ${contextBlock}
1494
+ ## Plan to Validate
1495
+
1496
+ ${issuesBlock}
1497
+
1498
+ ## Evaluation Dimensions
1499
+
1500
+ Evaluate the plan across these 6 dimensions:
1501
+
1502
+ 1. **Requirement Coverage**: Does the plan fully address the stated goal? Are there aspects of the goal that no issue covers?
1503
+ 2. **Task Atomicity**: Is each issue small enough to complete in a single AI coding session (under 1 hour)? Are any issues too broad or too granular?
1504
+ 3. **Dependency Correctness**: Are dependencies properly ordered? Are there missing dependencies where one issue clearly requires another to be completed first?
1505
+ 4. **File Scope**: Is the file scope per task reasonable? Do multiple issues modify the same files (merge conflict risk)?
1506
+ 5. **Verification**: Does each issue have testable acceptance criteria or a verify command? Can completion be objectively determined?
1507
+ 6. **Gap Detection**: Are there missing implementation steps? Would executing all issues actually achieve the goal?
1508
+
1509
+ ## Response Format
1510
+
1511
+ Respond with ONLY a valid JSON object \u2014 no markdown fences, no explanation, no other text:
1512
+
1513
+ {
1514
+ "passed": true,
1515
+ "findings": [
1516
+ { "dimension": "requirement_coverage", "severity": "low", "description": "Minor: no logging added", "suggestion": "Consider adding a logging issue" }
1517
+ ],
1518
+ "refinedPlan": null
1519
+ }
1520
+
1521
+ When "passed" is false, include a "refinedPlan" with the corrected issues:
1522
+
1523
+ {
1524
+ "passed": false,
1525
+ "findings": [
1526
+ { "dimension": "gap_detection", "severity": "high", "description": "Missing database migration step", "suggestion": "Add an issue for the migration", "issueOrder": 2 }
1527
+ ],
1528
+ "refinedPlan": {
1529
+ "issues": [
1530
+ { "title": "...", "description": "...", "acceptanceCriteria": ["..."], "relevantFiles": ["..."], "order": 1, "dependsOn": [], "verifyCommand": "...", "doneCriteria": "..." }
1531
+ ]
1532
+ }
1533
+ }
1534
+
1535
+ IMPORTANT:
1536
+ - Set "passed" to false ONLY for high-severity findings that would cause implementation failure.
1537
+ - Medium and low findings are informational \u2014 the plan can still pass.
1538
+ - Do NOT create, edit, or modify any files.
1539
+ - Do NOT run any shell commands.
1540
+ - ONLY output the JSON object above.`;
1541
+ }
1542
+ function parsePlanValidationResponse(output) {
1543
+ const jsonPatterns = [
1544
+ /\{[\s\S]*"findings"[\s\S]*\}/,
1545
+ /```(?:json)?\s*(\{[\s\S]*"findings"[\s\S]*\})\s*```/
1546
+ ];
1547
+ for (const pattern of jsonPatterns) {
1548
+ const match = pattern.exec(output);
1549
+ if (match) {
1550
+ const jsonStr = match[1] ?? match[0];
1551
+ try {
1552
+ const parsed = JSON.parse(jsonStr);
1553
+ if (Array.isArray(parsed.findings)) {
1554
+ const hasHighSeverity = parsed.findings.some((f) => f.severity === "high");
1555
+ return {
1556
+ passed: hasHighSeverity ? false : parsed.passed !== false,
1557
+ findings: parsed.findings,
1558
+ refinedIssues: parseRefinedIssues(parsed)
1559
+ };
1560
+ }
1561
+ } catch {
1562
+ }
1563
+ }
1564
+ }
1565
+ return null;
1566
+ }
1567
+ function parseRefinedIssues(parsed) {
1568
+ const refined = parsed.refinedPlan;
1569
+ if (!refined?.issues || !Array.isArray(refined.issues)) return void 0;
1570
+ return refined.issues.filter(
1571
+ (i) => typeof i === "object" && i !== null && typeof i.title === "string"
1572
+ ).map((issue2, idx) => ({
1573
+ title: String(issue2.title),
1574
+ description: typeof issue2.description === "string" ? issue2.description : "",
1575
+ acceptanceCriteria: Array.isArray(issue2.acceptanceCriteria) ? issue2.acceptanceCriteria.filter((c) => typeof c === "string") : [],
1576
+ relevantFiles: Array.isArray(issue2.relevantFiles) ? issue2.relevantFiles.filter((f) => typeof f === "string") : [],
1577
+ order: typeof issue2.order === "number" ? issue2.order : idx + 1,
1578
+ dependsOn: Array.isArray(issue2.dependsOn) ? issue2.dependsOn.filter((d) => typeof d === "number") : [],
1579
+ repo: typeof issue2.repo === "string" ? issue2.repo : void 0,
1580
+ verifyCommand: typeof issue2.verifyCommand === "string" ? issue2.verifyCommand : void 0,
1581
+ doneCriteria: typeof issue2.doneCriteria === "string" ? issue2.doneCriteria : void 0
1582
+ }));
1583
+ }
1584
+ async function validateAndRefinePlan(goal, issues, config2) {
1585
+ const maxIterations = config2.plan_validation?.max_iterations ?? 2;
1586
+ const workspace = resolve4(config2.workspace);
1587
+ const contextMd = readContext(workspace);
1588
+ const models = resolveModels(config2);
1589
+ const logDir = mkdtempSync2(join3(tmpdir2(), "lisa-plan-check-"));
1590
+ const logFile = join3(logDir, "plan-check.log");
1591
+ let currentIssues = issues;
1592
+ const allFindings = [];
1593
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
1594
+ const prompt = buildPlanValidationPrompt(goal, currentIssues, contextMd);
1595
+ const result = await runWithFallback(models, prompt, {
1596
+ logFile,
1597
+ cwd: workspace,
1598
+ sessionTimeout: 120
1599
+ });
1600
+ if (!result.success) {
1601
+ warn("Plan validation failed \u2014 skipping quality gate.");
1602
+ break;
1603
+ }
1604
+ const validation = parsePlanValidationResponse(result.output);
1605
+ if (!validation) {
1606
+ warn("Could not parse validation response \u2014 skipping quality gate.");
1607
+ break;
1608
+ }
1609
+ allFindings.push(...validation.findings);
1610
+ if (validation.passed) {
1611
+ ok(
1612
+ `Plan validated (iteration ${iteration + 1}): ${validation.findings.length} finding(s).`
1613
+ );
1614
+ break;
1615
+ }
1616
+ if (validation.refinedIssues && validation.refinedIssues.length > 0) {
1617
+ log(
1618
+ `Plan refined (iteration ${iteration + 1}/${maxIterations}): ${validation.findings.filter((f) => f.severity === "high").length} high-severity finding(s).`
1619
+ );
1620
+ currentIssues = validation.refinedIssues;
1621
+ } else {
1622
+ warn("Validation failed but no refined plan provided \u2014 using current plan.");
1623
+ break;
1624
+ }
1625
+ }
1626
+ return { issues: currentIssues, findings: allFindings };
1627
+ }
1628
+
1629
+ // src/plan/validate.ts
1630
+ function detectDependencyCycles(issues) {
1631
+ const issueByOrder = /* @__PURE__ */ new Map();
1632
+ for (const issue2 of issues) {
1633
+ issueByOrder.set(issue2.order, issue2);
1634
+ }
1635
+ const adjacency = /* @__PURE__ */ new Map();
1636
+ const inDegree = /* @__PURE__ */ new Map();
1637
+ for (const issue2 of issues) {
1638
+ adjacency.set(issue2.order, []);
1639
+ inDegree.set(issue2.order, 0);
1640
+ }
1641
+ for (const issue2 of issues) {
1642
+ for (const dep of issue2.dependsOn) {
1643
+ if (!adjacency.has(dep)) continue;
1644
+ adjacency.get(dep).push(issue2.order);
1645
+ inDegree.set(issue2.order, (inDegree.get(issue2.order) ?? 0) + 1);
1646
+ }
1647
+ }
1648
+ const queue = [];
1649
+ for (const [order, degree] of inDegree) {
1650
+ if (degree === 0) queue.push(order);
1651
+ }
1652
+ const sorted = [];
1653
+ while (queue.length > 0) {
1654
+ const node = queue.shift();
1655
+ sorted.push(node);
1656
+ for (const neighbor of adjacency.get(node) ?? []) {
1657
+ const newDegree = (inDegree.get(neighbor) ?? 1) - 1;
1658
+ inDegree.set(neighbor, newDegree);
1659
+ if (newDegree === 0) queue.push(neighbor);
1660
+ }
1661
+ }
1662
+ if (sorted.length === issues.length) return null;
1663
+ const sortedSet = new Set(sorted);
1664
+ const cycleNodes = issues.filter((i) => !sortedSet.has(i.order)).map((i) => `#${i.order} "${i.title}"`);
1665
+ return [`Circular dependency involving: ${cycleNodes.join(" -> ")}`];
1666
+ }
1667
+ function detectFileOverlaps(issues) {
1668
+ const fileMap = /* @__PURE__ */ new Map();
1669
+ for (const issue2 of issues) {
1670
+ for (const file of issue2.relevantFiles) {
1671
+ const existing = fileMap.get(file);
1672
+ if (existing) {
1673
+ existing.push(issue2.order);
1674
+ } else {
1675
+ fileMap.set(file, [issue2.order]);
1676
+ }
1677
+ }
1678
+ }
1679
+ const overlaps = [];
1680
+ for (const [file, issueOrders] of fileMap) {
1681
+ if (issueOrders.length >= 2) {
1682
+ overlaps.push({ file, issues: issueOrders });
1683
+ }
1684
+ }
1685
+ return overlaps;
1686
+ }
1687
+
1458
1688
  // src/plan/index.ts
1459
1689
  async function runPlan(opts) {
1460
1690
  const { config: config2 } = opts;
1461
- const workspace = resolve4(config2.workspace);
1691
+ const workspace = resolve5(config2.workspace);
1462
1692
  if (opts.continueLatest) {
1463
1693
  const latest = loadLatestPlan(workspace);
1464
1694
  if (!latest)
@@ -1498,11 +1728,41 @@ async function runPlan(opts) {
1498
1728
  }
1499
1729
  }
1500
1730
  }
1501
- const issues = await generatePlan(refinedGoal, config2, { parentDescription });
1731
+ let validatedIssues = await generatePlan(refinedGoal, config2, { parentDescription });
1732
+ const cycles = detectDependencyCycles(validatedIssues);
1733
+ if (cycles) {
1734
+ warn(`Dependency cycles detected: ${cycles.join(", ")}`);
1735
+ validatedIssues = await generatePlan(refinedGoal, config2, {
1736
+ parentDescription,
1737
+ feedback: `Fix dependency cycles: ${cycles.join(", ")}. Ensure no circular dependencies.`
1738
+ });
1739
+ }
1740
+ const overlaps = detectFileOverlaps(validatedIssues);
1741
+ if (overlaps.length > 0) {
1742
+ for (const o of overlaps) {
1743
+ clack4.log.warning(
1744
+ `File ${o.file} touched by issues ${o.issues.join(", ")} \u2014 merge conflict risk`
1745
+ );
1746
+ }
1747
+ }
1748
+ if (config2.plan_validation?.enabled && !opts.jsonOutput) {
1749
+ log("Validating plan quality...");
1750
+ const { issues: validated, findings } = await validateAndRefinePlan(
1751
+ refinedGoal,
1752
+ validatedIssues,
1753
+ config2
1754
+ );
1755
+ validatedIssues = validated;
1756
+ for (const f of findings) {
1757
+ if (f.severity === "high") {
1758
+ clack4.log.warning(`[${f.dimension}] ${f.description}`);
1759
+ }
1760
+ }
1761
+ }
1502
1762
  const plan2 = {
1503
1763
  goal: refinedGoal,
1504
1764
  sourceIssueId: opts.issueId,
1505
- issues,
1765
+ issues: validatedIssues,
1506
1766
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1507
1767
  status: "draft",
1508
1768
  brainstormHistory
@@ -1533,7 +1793,7 @@ async function reviewAndCreate(plan2, planPath, opts) {
1533
1793
  });
1534
1794
  if (clack4.isCancel(confirm3) || !confirm3) {
1535
1795
  plan2.status = "draft";
1536
- savePlan(resolve4(config2.workspace), plan2);
1796
+ savePlan(resolve5(config2.workspace), plan2);
1537
1797
  log("Plan saved. Resume with: lisa plan --continue");
1538
1798
  return;
1539
1799
  }
@@ -1542,7 +1802,7 @@ async function reviewAndCreate(plan2, planPath, opts) {
1542
1802
  const createdIds = await createPlanIssues(source, config2.source_config, plan2, config2.workspace);
1543
1803
  plan2.status = "created";
1544
1804
  plan2.createdIssueIds = createdIds;
1545
- savePlan(resolve4(config2.workspace), plan2);
1805
+ savePlan(resolve5(config2.workspace), plan2);
1546
1806
  ok(`${createdIds.length} issue${createdIds.length !== 1 ? "s" : ""} created.`);
1547
1807
  for (let i = 0; i < createdIds.length; i++) {
1548
1808
  log(` ${plan2.issues[i].order}. ${createdIds[i]}: ${plan2.issues[i].title}`);
@@ -1555,13 +1815,15 @@ async function reviewAndCreate(plan2, planPath, opts) {
1555
1815
  log("Run `lisa run` when ready.");
1556
1816
  return;
1557
1817
  }
1558
- const { runLoop: runLoop2 } = await import("./loop-N5D27JQX.js");
1818
+ const { runLoop: runLoop2 } = await import("./loop-732CLNLZ.js");
1819
+ const waves = buildExecutionWaves(plan2.issues);
1820
+ const maxWaveSize = Math.max(...waves.map((w) => w.length));
1559
1821
  await runLoop2(config2, {
1560
1822
  once: false,
1561
1823
  watch: false,
1562
1824
  limit: createdIds.length,
1563
1825
  dryRun: false,
1564
- concurrency: 1
1826
+ concurrency: maxWaveSize > 1 ? maxWaveSize : 1
1565
1827
  });
1566
1828
  }
1567
1829
 
@@ -1628,7 +1890,7 @@ var plan = defineCommand7({
1628
1890
  });
1629
1891
 
1630
1892
  // src/cli/commands/run.ts
1631
- import { resolve as resolve5 } from "path";
1893
+ import { resolve as resolve6 } from "path";
1632
1894
  import { defineCommand as defineCommand8 } from "citty";
1633
1895
  import pc6 from "picocolors";
1634
1896
 
@@ -2018,12 +2280,12 @@ Add them to your ${shell} and run: source ${shell}`));
2018
2280
  let onBeforeExit;
2019
2281
  let persistedCards;
2020
2282
  if (isTTY) {
2021
- const workspace = resolve5(merged.workspace);
2283
+ const workspace = resolve6(merged.workspace);
2022
2284
  const persistence = createKanbanPersistence(workspace);
2023
2285
  const initialCards = persistence.load();
2024
2286
  persistedCards = initialCards;
2025
2287
  persistence.start();
2026
- const { registerPlanBridge } = await import("./tui-bridge-LQDYRWHY.js");
2288
+ const { registerPlanBridge } = await import("./tui-bridge-TSGCSJV4.js");
2027
2289
  const cleanupPlan = registerPlanBridge(merged);
2028
2290
  onBeforeExit = () => {
2029
2291
  persistence.stop();
@@ -3,7 +3,7 @@ import {
3
3
  checkoutBaseBranches,
4
4
  runDemoLoop,
5
5
  runLoop
6
- } from "./chunk-BWND35E5.js";
6
+ } from "./chunk-PGIXWLQT.js";
7
7
  import {
8
8
  WATCH_POLL_INTERVAL_MS
9
9
  } from "./chunk-6VIN5PMW.js";
@@ -6,7 +6,7 @@ import {
6
6
  markdownToIssue,
7
7
  parseStructuredOutput,
8
8
  savePlan
9
- } from "./chunk-6SNT7TND.js";
9
+ } from "./chunk-AFKXWCAM.js";
10
10
  import {
11
11
  createSource,
12
12
  resolveModels,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "description": "Autonomous issue resolver",
5
5
  "keywords": [
6
6
  "loop",