@tarcisiopgs/lisa 1.8.2 → 1.9.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/dist/index.js CHANGED
@@ -6,13 +6,13 @@ import {
6
6
  setTitle,
7
7
  startSpinner,
8
8
  stopSpinner
9
- } from "./chunk-YZKNBQN6.js";
9
+ } from "./chunk-QQBOYEWI.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import { execSync as execSync9 } from "child_process";
13
- import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6 } from "fs";
13
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
14
14
  import { tmpdir as tmpdir9 } from "os";
15
- import { join as join13, resolve as resolvePath } from "path";
15
+ import { join as join14, resolve as resolvePath } from "path";
16
16
  import * as clack from "@clack/prompts";
17
17
  import { defineCommand, runMain } from "citty";
18
18
  import pc2 from "picocolors";
@@ -48,10 +48,6 @@ var DEFAULT_CONFIG = {
48
48
  cooldown: 0,
49
49
  max_sessions: 0
50
50
  },
51
- logs: {
52
- dir: "",
53
- format: ""
54
- },
55
51
  overseer: { ...DEFAULT_OVERSEER_CONFIG }
56
52
  };
57
53
  function getConfigPath(cwd = process.cwd()) {
@@ -77,13 +73,16 @@ function loadConfig(cwd = process.cwd()) {
77
73
  const raw = readFileSync(configPath, "utf-8");
78
74
  const parsed = parse(raw);
79
75
  const rawSource = parsed.source_config ?? {};
76
+ const rawLabel = rawSource.label;
77
+ const label = Array.isArray(rawLabel) ? rawLabel : typeof rawLabel === "string" ? rawLabel : "";
80
78
  const sourceConfig = {
81
- team: rawSource.team ?? rawSource.board ?? "",
82
- project: rawSource.project ?? rawSource.list ?? rawSource.pick_from ?? "",
83
- label: rawSource.label ?? "",
84
- pick_from: rawSource.pick_from ?? rawSource.initial_status ?? "",
85
- in_progress: rawSource.in_progress ?? rawSource.active_status ?? "",
86
- done: rawSource.done ?? rawSource.done_status ?? ""
79
+ team: (rawSource.team ?? rawSource.board) || "",
80
+ project: (rawSource.project ?? rawSource.list ?? rawSource.pick_from) || "",
81
+ label,
82
+ remove_label: rawSource.remove_label || void 0,
83
+ pick_from: (rawSource.pick_from ?? rawSource.initial_status) || "",
84
+ in_progress: (rawSource.in_progress ?? rawSource.active_status) || "",
85
+ done: (rawSource.done ?? rawSource.done_status) || ""
87
86
  };
88
87
  if (parsed.source === "trello" && !sourceConfig.pick_from) {
89
88
  sourceConfig.pick_from = sourceConfig.project;
@@ -100,12 +99,12 @@ function loadConfig(cwd = process.cwd()) {
100
99
  if (parsed.source === "jira" && !sourceConfig.team && rawSource.project) {
101
100
  sourceConfig.team = rawSource.project;
102
101
  }
102
+ const { logs: _ignoredLogs, ...parsedWithoutLogs } = parsed;
103
103
  const config2 = {
104
104
  ...DEFAULT_CONFIG,
105
- ...parsed,
105
+ ...parsedWithoutLogs,
106
106
  source_config: sourceConfig,
107
107
  loop: { ...DEFAULT_CONFIG.loop, ...parsed.loop ?? {} },
108
- logs: { ...DEFAULT_CONFIG.logs, ...parsed.logs ?? {} },
109
108
  overseer: {
110
109
  ...DEFAULT_OVERSEER_CONFIG,
111
110
  ...parsed.overseer ?? {}
@@ -127,25 +126,30 @@ function saveConfig(config2, cwd = process.cwd()) {
127
126
  mkdirSync(dir, { recursive: true });
128
127
  }
129
128
  const sc = config2.source_config;
129
+ const removeLabelEntry = sc.remove_label ? { remove_label: sc.remove_label } : {};
130
130
  const sourceYaml = config2.source === "trello" ? {
131
131
  board: sc.team,
132
132
  pick_from: sc.pick_from || sc.project,
133
133
  label: sc.label,
134
+ ...removeLabelEntry,
134
135
  in_progress: sc.in_progress,
135
136
  done: sc.done
136
137
  } : config2.source === "gitlab-issues" ? {
137
138
  team: sc.team,
138
139
  label: sc.label,
140
+ ...removeLabelEntry,
139
141
  in_progress: sc.in_progress,
140
142
  done: sc.done
141
143
  } : config2.source === "github-issues" ? {
142
144
  team: sc.team,
143
145
  label: sc.label,
146
+ ...removeLabelEntry,
144
147
  in_progress: sc.in_progress,
145
148
  done: sc.done
146
149
  } : config2.source === "jira" ? {
147
150
  team: sc.team,
148
151
  label: sc.label,
152
+ ...removeLabelEntry,
149
153
  pick_from: sc.pick_from,
150
154
  in_progress: sc.in_progress,
151
155
  done: sc.done
@@ -153,6 +157,7 @@ function saveConfig(config2, cwd = process.cwd()) {
153
157
  team: sc.team,
154
158
  project: sc.project,
155
159
  label: sc.label,
160
+ ...removeLabelEntry,
156
161
  pick_from: sc.pick_from,
157
162
  in_progress: sc.in_progress,
158
163
  done: sc.done
@@ -165,9 +170,26 @@ function mergeWithFlags(config2, flags) {
165
170
  if (flags.provider) merged.provider = flags.provider;
166
171
  if (flags.source) merged.source = flags.source;
167
172
  if (flags.github) merged.github = flags.github;
168
- if (flags.label) merged.source_config = { ...merged.source_config, label: flags.label };
173
+ if (flags.label) {
174
+ const parts = flags.label.split(",").map((s) => s.trim()).filter(Boolean);
175
+ const label = parts.length === 1 ? parts[0] : parts;
176
+ merged.source_config = { ...merged.source_config, label };
177
+ }
169
178
  return merged;
170
179
  }
180
+ function getRemoveLabel(sc) {
181
+ if (sc.remove_label) return sc.remove_label;
182
+ if (typeof sc.label === "string" && sc.label) return sc.label;
183
+ return void 0;
184
+ }
185
+ function getLabelsArray(sc) {
186
+ if (Array.isArray(sc.label)) return sc.label;
187
+ return sc.label ? [sc.label] : [];
188
+ }
189
+ function formatLabels(sc) {
190
+ const labels = getLabelsArray(sc);
191
+ return labels.length === 0 ? "(none)" : labels.join(", ");
192
+ }
171
193
 
172
194
  // src/git/github.ts
173
195
  import { execa } from "execa";
@@ -290,28 +312,6 @@ function ensureWorktreeGitignore(repoRoot) {
290
312
  `);
291
313
  }
292
314
  }
293
- var LOGS_GITIGNORE_ENTRY = ".lisa/logs/*";
294
- function ensureLogsGitignore(repoRoot) {
295
- if (!existsSync2(join(repoRoot, ".git"))) {
296
- return false;
297
- }
298
- const gitignorePath = join(repoRoot, ".gitignore");
299
- if (!existsSync2(gitignorePath)) {
300
- appendFileSync(gitignorePath, `# Lisa
301
- ${LOGS_GITIGNORE_ENTRY}
302
- `);
303
- return true;
304
- }
305
- const content = readFileSync2(gitignorePath, "utf-8");
306
- if (content.split("\n").some((line) => line.trim() === LOGS_GITIGNORE_ENTRY)) {
307
- return false;
308
- }
309
- const separator = content.endsWith("\n") ? "" : "\n";
310
- appendFileSync(gitignorePath, `${separator}# Lisa
311
- ${LOGS_GITIGNORE_ENTRY}
312
- `);
313
- return true;
314
- }
315
315
  function determineRepoPath(repos, issue2, workspace) {
316
316
  if (repos.length === 0) return void 0;
317
317
  if (issue2.repo) {
@@ -328,12 +328,293 @@ function determineRepoPath(repos, issue2, workspace) {
328
328
  }
329
329
 
330
330
  // src/loop.ts
331
- import { appendFileSync as appendFileSync11, existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync9 } from "fs";
332
- import { join as join12, resolve as resolve5 } from "path";
331
+ import { appendFileSync as appendFileSync11, existsSync as existsSync8, readFileSync as readFileSync6, unlinkSync as unlinkSync10 } from "fs";
332
+ import { resolve as resolve6 } from "path";
333
333
  import { execa as execa3 } from "execa";
334
334
 
335
+ // src/context.ts
336
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
337
+ import { join as join2, relative } from "path";
338
+ var QUALITY_SCRIPT_NAMES = /* @__PURE__ */ new Set([
339
+ "lint",
340
+ "typecheck",
341
+ "check",
342
+ "format",
343
+ "test",
344
+ "build",
345
+ "ci"
346
+ ]);
347
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
348
+ "node_modules",
349
+ "dist",
350
+ ".git",
351
+ ".worktrees",
352
+ "coverage",
353
+ ".next",
354
+ ".nuxt",
355
+ ".output",
356
+ "build",
357
+ ".cache",
358
+ ".turbo",
359
+ ".lisa"
360
+ ]);
361
+ function analyzeProject(cwd) {
362
+ return {
363
+ qualityScripts: detectQualityScripts(cwd),
364
+ testPattern: detectTestPattern(cwd),
365
+ codeTools: detectCodeTools(cwd),
366
+ projectTree: generateProjectTree(cwd)
367
+ };
368
+ }
369
+ function detectQualityScripts(cwd) {
370
+ const packageJsonPath = join2(cwd, "package.json");
371
+ if (!existsSync3(packageJsonPath)) return [];
372
+ try {
373
+ const content = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
374
+ if (!content.scripts) return [];
375
+ const scripts = [];
376
+ for (const [name, command] of Object.entries(content.scripts)) {
377
+ if (QUALITY_SCRIPT_NAMES.has(name)) {
378
+ scripts.push({ name, command });
379
+ }
380
+ }
381
+ return scripts;
382
+ } catch {
383
+ return [];
384
+ }
385
+ }
386
+ function detectTestPattern(cwd) {
387
+ const testFiles = findTestFiles(cwd, 3);
388
+ if (testFiles.length === 0) return null;
389
+ const location = inferTestLocation(cwd, testFiles);
390
+ let style = "unknown";
391
+ const mocking = /* @__PURE__ */ new Set();
392
+ let example;
393
+ for (const file of testFiles) {
394
+ try {
395
+ const content = readFileSync3(file, "utf-8");
396
+ const fileStyle = inferTestStyle(content);
397
+ if (style === "unknown") {
398
+ style = fileStyle;
399
+ } else if (style !== fileStyle && fileStyle !== "unknown") {
400
+ style = "mixed";
401
+ }
402
+ for (const mock of inferMocking(content)) {
403
+ mocking.add(mock);
404
+ }
405
+ if (!example) {
406
+ example = extractTestExample(content, file, cwd);
407
+ }
408
+ } catch {
409
+ }
410
+ }
411
+ return {
412
+ location,
413
+ style,
414
+ mocking: [...mocking],
415
+ example
416
+ };
417
+ }
418
+ function detectCodeTools(cwd) {
419
+ const tools = [];
420
+ const biomeConfig = ["biome.json", "biome.jsonc"].find((f) => existsSync3(join2(cwd, f)));
421
+ if (biomeConfig) {
422
+ tools.push({ name: "Biome", configFile: biomeConfig });
423
+ }
424
+ const eslintConfigs = [
425
+ ".eslintrc",
426
+ ".eslintrc.js",
427
+ ".eslintrc.cjs",
428
+ ".eslintrc.json",
429
+ ".eslintrc.yml",
430
+ ".eslintrc.yaml",
431
+ "eslint.config.js",
432
+ "eslint.config.mjs",
433
+ "eslint.config.cjs",
434
+ "eslint.config.ts"
435
+ ];
436
+ const eslintConfig = eslintConfigs.find((f) => existsSync3(join2(cwd, f)));
437
+ if (eslintConfig) {
438
+ tools.push({ name: "ESLint", configFile: eslintConfig });
439
+ }
440
+ const prettierConfigs = [
441
+ ".prettierrc",
442
+ ".prettierrc.json",
443
+ ".prettierrc.yml",
444
+ ".prettierrc.yaml",
445
+ ".prettierrc.js",
446
+ ".prettierrc.cjs",
447
+ ".prettierrc.mjs",
448
+ "prettier.config.js",
449
+ "prettier.config.cjs",
450
+ "prettier.config.mjs"
451
+ ];
452
+ const prettierConfig = prettierConfigs.find((f) => existsSync3(join2(cwd, f)));
453
+ if (prettierConfig) {
454
+ tools.push({ name: "Prettier", configFile: prettierConfig });
455
+ }
456
+ return tools;
457
+ }
458
+ function generateProjectTree(cwd) {
459
+ const lines = [];
460
+ try {
461
+ const entries = readdirSync(cwd);
462
+ const filtered = entries.filter((e) => !IGNORED_DIRS.has(e) && !e.startsWith(".")).sort((a, b) => {
463
+ const aIsDir = isDirectory(join2(cwd, a));
464
+ const bIsDir = isDirectory(join2(cwd, b));
465
+ if (aIsDir && !bIsDir) return -1;
466
+ if (!aIsDir && bIsDir) return 1;
467
+ return a.localeCompare(b);
468
+ });
469
+ for (const entry of filtered) {
470
+ const fullPath = join2(cwd, entry);
471
+ if (isDirectory(fullPath)) {
472
+ lines.push(`${entry}/`);
473
+ try {
474
+ const children = readdirSync(fullPath);
475
+ const filteredChildren = children.filter((c) => !IGNORED_DIRS.has(c) && !c.startsWith(".")).sort().slice(0, 15);
476
+ for (const child of filteredChildren) {
477
+ const childPath = join2(fullPath, child);
478
+ const suffix = isDirectory(childPath) ? "/" : "";
479
+ lines.push(` ${child}${suffix}`);
480
+ }
481
+ if (children.filter((c) => !IGNORED_DIRS.has(c) && !c.startsWith(".")).length > 15) {
482
+ lines.push(" ...");
483
+ }
484
+ } catch {
485
+ }
486
+ } else {
487
+ lines.push(entry);
488
+ }
489
+ }
490
+ } catch {
491
+ return "";
492
+ }
493
+ return lines.join("\n");
494
+ }
495
+ function formatProjectContext(ctx) {
496
+ const sections = [];
497
+ if (ctx.qualityScripts.length > 0) {
498
+ const scriptLines = ctx.qualityScripts.map((s) => `- \`${s.name}\`: \`${s.command}\``).join("\n");
499
+ sections.push(`### Quality Scripts
500
+
501
+ ${scriptLines}`);
502
+ }
503
+ if (ctx.testPattern) {
504
+ const tp = ctx.testPattern;
505
+ const details = [
506
+ `- Location: ${tp.location === "colocated" ? "tests are colocated next to source files" : tp.location === "separate" ? "tests are in a separate directory" : "unknown"}`,
507
+ `- Style: ${tp.style === "describe-it" ? "describe/it blocks" : tp.style === "test" ? "top-level test() calls" : tp.style === "mixed" ? "mixed (describe/it and test())" : "unknown"}`
508
+ ];
509
+ if (tp.mocking.length > 0) {
510
+ details.push(`- Mocking: ${tp.mocking.join(", ")}`);
511
+ }
512
+ let block = `### Test Patterns
513
+
514
+ ${details.join("\n")}`;
515
+ if (tp.example) {
516
+ block += `
517
+
518
+ **Reference test file:**
519
+ \`\`\`typescript
520
+ ${tp.example}
521
+ \`\`\``;
522
+ }
523
+ sections.push(block);
524
+ }
525
+ if (ctx.codeTools.length > 0) {
526
+ const toolLines = ctx.codeTools.map((t) => `- **${t.name}** (config: \`${t.configFile}\`)`).join("\n");
527
+ sections.push(`### Code Tools
528
+
529
+ ${toolLines}`);
530
+ }
531
+ if (ctx.projectTree) {
532
+ sections.push(`### Project Structure
533
+
534
+ \`\`\`
535
+ ${ctx.projectTree}
536
+ \`\`\``);
537
+ }
538
+ if (sections.length === 0) return "";
539
+ return `## Project Context
540
+
541
+ ${sections.join("\n\n")}`;
542
+ }
543
+ function findTestFiles(cwd, maxFiles) {
544
+ const results = [];
545
+ walkForTests(cwd, cwd, results, maxFiles, 0);
546
+ return results;
547
+ }
548
+ function walkForTests(root, dir, results, maxFiles, depth) {
549
+ if (results.length >= maxFiles || depth > 5) return;
550
+ try {
551
+ const entries = readdirSync(dir);
552
+ for (const entry of entries) {
553
+ if (results.length >= maxFiles) return;
554
+ if (IGNORED_DIRS.has(entry)) continue;
555
+ const fullPath = join2(dir, entry);
556
+ if (isDirectory(fullPath)) {
557
+ walkForTests(root, fullPath, results, maxFiles, depth + 1);
558
+ } else if (entry.endsWith(".test.ts") || entry.endsWith(".spec.ts") || entry.endsWith(".test.tsx") || entry.endsWith(".spec.tsx") || entry.endsWith(".test.js") || entry.endsWith(".spec.js")) {
559
+ results.push(fullPath);
560
+ }
561
+ }
562
+ } catch {
563
+ }
564
+ }
565
+ function inferTestLocation(cwd, testFiles) {
566
+ let colocated = 0;
567
+ let separate = 0;
568
+ for (const file of testFiles) {
569
+ const rel = relative(cwd, file);
570
+ const parts = rel.split("/");
571
+ if (parts.some(
572
+ (p) => p === "__tests__" || p === "tests" || p === "test" || p === "spec" || p === "__specs__"
573
+ )) {
574
+ separate++;
575
+ } else {
576
+ colocated++;
577
+ }
578
+ }
579
+ if (colocated > 0 && separate === 0) return "colocated";
580
+ if (separate > 0 && colocated === 0) return "separate";
581
+ if (colocated > 0 && separate > 0) return "colocated";
582
+ return "unknown";
583
+ }
584
+ function inferTestStyle(content) {
585
+ const hasDescribe = /\bdescribe\s*\(/.test(content);
586
+ const hasIt = /\bit\s*\(/.test(content);
587
+ const hasTopLevelTest = /\btest\s*\(/.test(content);
588
+ if (hasDescribe && hasIt) return "describe-it";
589
+ if (hasTopLevelTest && !hasDescribe) return "test";
590
+ if (hasDescribe || hasIt) return "describe-it";
591
+ return "unknown";
592
+ }
593
+ function inferMocking(content) {
594
+ const mocks = [];
595
+ if (/\bvi\.(mock|fn|spyOn)\b/.test(content)) mocks.push("vi.mock/vi.fn");
596
+ if (/\bjest\.(mock|fn|spyOn)\b/.test(content)) mocks.push("jest.mock/jest.fn");
597
+ if (/\bfixture/.test(content)) mocks.push("fixtures");
598
+ return mocks;
599
+ }
600
+ function extractTestExample(content, filePath, cwd) {
601
+ const lines = content.split("\n");
602
+ const snippet = lines.slice(0, 30).join("\n").trim();
603
+ if (snippet.length < 10) return void 0;
604
+ const relPath = relative(cwd, filePath);
605
+ return `// ${relPath}
606
+ ${snippet}`;
607
+ }
608
+ function isDirectory(path) {
609
+ try {
610
+ return statSync(path).isDirectory();
611
+ } catch {
612
+ return false;
613
+ }
614
+ }
615
+
335
616
  // src/output/logger.ts
336
- import { appendFileSync as appendFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
617
+ import { appendFileSync as appendFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
337
618
  import { dirname } from "path";
338
619
  import pc from "picocolors";
339
620
  var logFilePath = null;
@@ -350,7 +631,7 @@ function shouldPrintToConsole() {
350
631
  }
351
632
  function initLogFile(path) {
352
633
  const dir = dirname(path);
353
- if (!existsSync3(dir)) {
634
+ if (!existsSync4(dir)) {
354
635
  mkdirSync2(dir, { recursive: true });
355
636
  }
356
637
  writeFileSync2(path, `[${timestamp()}] Log started
@@ -425,20 +706,70 @@ function banner() {
425
706
  `));
426
707
  }
427
708
 
709
+ // src/paths.ts
710
+ import { createHash } from "crypto";
711
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync as statSync2, unlinkSync } from "fs";
712
+ import { homedir } from "os";
713
+ import { join as join3, resolve as resolve3 } from "path";
714
+ var MAX_LOG_FILES = 20;
715
+ function projectHash(cwd) {
716
+ const absolute = resolve3(cwd);
717
+ return createHash("sha256").update(absolute).digest("hex").slice(0, 12);
718
+ }
719
+ function getCacheDir(cwd) {
720
+ const base = process.env.XDG_CACHE_HOME || join3(homedir(), ".cache");
721
+ return join3(base, "lisa", projectHash(cwd));
722
+ }
723
+ function getLogsDir(cwd) {
724
+ return join3(getCacheDir(cwd), "logs");
725
+ }
726
+ function getGuardrailsPath(cwd) {
727
+ return join3(getCacheDir(cwd), "guardrails.md");
728
+ }
729
+ function getManifestPath(cwd) {
730
+ return join3(getCacheDir(cwd), "manifest.json");
731
+ }
732
+ function getPlanPath(cwd) {
733
+ return join3(getCacheDir(cwd), "plan.json");
734
+ }
735
+ function ensureCacheDir(cwd) {
736
+ const dir = getCacheDir(cwd);
737
+ if (!existsSync5(dir)) {
738
+ mkdirSync3(dir, { recursive: true });
739
+ }
740
+ }
741
+ function rotateLogFiles(cwd) {
742
+ const logsDir = getLogsDir(cwd);
743
+ if (!existsSync5(logsDir)) return;
744
+ const files = readdirSync2(logsDir).filter((f) => f.endsWith(".log")).map((f) => ({
745
+ name: f,
746
+ path: join3(logsDir, f),
747
+ mtime: statSync2(join3(logsDir, f)).mtimeMs
748
+ })).sort((a, b) => a.mtime - b.mtime);
749
+ const excess = files.length - MAX_LOG_FILES;
750
+ if (excess <= 0) return;
751
+ for (const file of files.slice(0, excess)) {
752
+ try {
753
+ unlinkSync(file.path);
754
+ } catch {
755
+ }
756
+ }
757
+ }
758
+
428
759
  // src/prompt.ts
429
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
430
- import { join as join2, resolve as resolve3 } from "path";
760
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
761
+ import { join as join4, resolve as resolve4 } from "path";
431
762
  function detectPackageManager(cwd) {
432
- if (existsSync4(join2(cwd, "bun.lockb")) || existsSync4(join2(cwd, "bun.lock"))) return "bun";
433
- if (existsSync4(join2(cwd, "pnpm-lock.yaml"))) return "pnpm";
434
- if (existsSync4(join2(cwd, "yarn.lock"))) return "yarn";
763
+ if (existsSync6(join4(cwd, "bun.lockb")) || existsSync6(join4(cwd, "bun.lock"))) return "bun";
764
+ if (existsSync6(join4(cwd, "pnpm-lock.yaml"))) return "pnpm";
765
+ if (existsSync6(join4(cwd, "yarn.lock"))) return "yarn";
435
766
  return "npm";
436
767
  }
437
768
  function detectTestRunner(cwd) {
438
- const packageJsonPath = join2(cwd, "package.json");
439
- if (!existsSync4(packageJsonPath)) return null;
769
+ const packageJsonPath = join4(cwd, "package.json");
770
+ if (!existsSync6(packageJsonPath)) return null;
440
771
  try {
441
- const content = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
772
+ const content = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
442
773
  const deps = { ...content.dependencies, ...content.devDependencies };
443
774
  if ("vitest" in deps) return "vitest";
444
775
  if ("jest" in deps) return "jest";
@@ -447,11 +778,20 @@ function detectTestRunner(cwd) {
447
778
  return null;
448
779
  }
449
780
  }
450
- function buildImplementPrompt(issue2, config2, testRunner, pm) {
781
+ function buildImplementPrompt(issue2, config2, testRunner, pm, projectContext) {
782
+ const workspace = resolve4(config2.workspace);
783
+ const manifestPath = getManifestPath(workspace);
451
784
  if (config2.workflow === "worktree") {
452
- return buildWorktreePrompt(issue2, testRunner, pm, config2.base_branch);
785
+ return buildWorktreePrompt(
786
+ issue2,
787
+ testRunner,
788
+ pm,
789
+ config2.base_branch,
790
+ projectContext,
791
+ manifestPath
792
+ );
453
793
  }
454
- return buildBranchPrompt(issue2, config2, testRunner, pm);
794
+ return buildBranchPrompt(issue2, config2, testRunner, pm, projectContext, manifestPath);
455
795
  }
456
796
  function buildTestInstructions(testRunner, pm = "npm") {
457
797
  if (!testRunner) return "";
@@ -498,10 +838,12 @@ Do NOT update README.md for:
498
838
  If an update is needed, keep the existing README style and structure. Include the README change in the same commit as the implementation.
499
839
  `;
500
840
  }
501
- function buildWorktreePrompt(issue2, testRunner, pm, baseBranch) {
841
+ function buildWorktreePrompt(issue2, testRunner, pm, baseBranch, projectContext, manifestPath) {
502
842
  const testBlock = buildTestInstructions(testRunner ?? null, pm);
503
843
  const readmeBlock = buildReadmeInstructions();
504
844
  const hookBlock = buildPreCommitHookInstructions();
845
+ const contextBlock = projectContext ? formatProjectContext(projectContext) : "";
846
+ const manifestLocation = manifestPath ? `\`${manifestPath}\`` : "`.lisa-manifest.json` in the **current directory**";
505
847
  return `You are an autonomous implementation agent. Your job is to implement an issue end-to-end: code, push, PR, and tracker update.
506
848
 
507
849
  You are already inside the correct repository worktree on the correct branch.
@@ -516,6 +858,9 @@ Do NOT create a new branch \u2014 just work on the current one.
516
858
  ### Description
517
859
 
518
860
  ${issue2.description}
861
+ ${contextBlock ? `
862
+ ${contextBlock}
863
+ ` : ""}
519
864
 
520
865
  ## Instructions
521
866
 
@@ -550,7 +895,7 @@ ${testBlock}${readmeBlock}${hookBlock}
550
895
  \`lisa issue done ${issue2.id} --pr-url <pr-url>\`
551
896
  Wait 1 second before calling this command.
552
897
 
553
- 7. **Write manifest**: Create \`.lisa-manifest.json\` in the **current directory** with JSON:
898
+ 7. **Write manifest**: Create ${manifestLocation} with JSON:
554
899
  \`\`\`json
555
900
  {"branch": "<final English branch name>", "prUrl": "<pull request URL>"}
556
901
  \`\`\`
@@ -565,17 +910,18 @@ ${testBlock}${readmeBlock}${hookBlock}
565
910
  - One issue only. Do not pick up additional issues.
566
911
  - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
567
912
  }
568
- function buildBranchPrompt(issue2, config2, testRunner, pm) {
569
- const workspace = resolve3(config2.workspace);
913
+ function buildBranchPrompt(issue2, config2, testRunner, pm, projectContext, manifestPath) {
914
+ const workspace = resolve4(config2.workspace);
570
915
  const repoEntries = config2.repos.map(
571
- (r) => ` - If it says "Repo: ${r.name}" or title starts with "${r.match}" \u2192 \`${resolve3(workspace, r.path)}\` (base branch: \`${r.base_branch}\`)`
916
+ (r) => ` - If it says "Repo: ${r.name}" or title starts with "${r.match}" \u2192 \`${resolve4(workspace, r.path)}\` (base branch: \`${r.base_branch}\`)`
572
917
  ).join("\n");
573
918
  const baseBranch = config2.base_branch;
574
919
  const baseBranchInstruction = config2.repos.length > 0 ? "From the repo's base branch (listed above)" : `From \`${baseBranch}\``;
575
920
  const testBlock = buildTestInstructions(testRunner ?? null, pm);
576
921
  const readmeBlock = buildReadmeInstructions();
577
922
  const hookBlock = buildPreCommitHookInstructions();
578
- const manifestPath = join2(workspace, ".lisa-manifest.json");
923
+ const contextBlock = projectContext ? formatProjectContext(projectContext) : "";
924
+ const resolvedManifestPath = manifestPath ?? getManifestPath(workspace);
579
925
  return `You are an autonomous implementation agent. Your job is to implement an issue end-to-end: code, push, PR, and tracker update.
580
926
 
581
927
  ## Issue
@@ -587,7 +933,9 @@ function buildBranchPrompt(issue2, config2, testRunner, pm) {
587
933
  ### Description
588
934
 
589
935
  ${issue2.description}
590
-
936
+ ${contextBlock ? `
937
+ ${contextBlock}
938
+ ` : ""}
591
939
  ## Instructions
592
940
 
593
941
  1. **Identify the repo**: Look at the issue description for relevant files or repo references.
@@ -626,7 +974,7 @@ ${testBlock}${readmeBlock}${hookBlock}
626
974
  \`lisa issue done ${issue2.id} --pr-url <pr-url>\`
627
975
  Wait 1 second before calling this command.
628
976
 
629
- 8. **Write manifest**: Before finishing, create \`${manifestPath}\` with JSON:
977
+ 8. **Write manifest**: Before finishing, create \`${resolvedManifestPath}\` with JSON:
630
978
  \`\`\`json
631
979
  {"repoPath": "<absolute path to this repo>", "branch": "<branch name>", "prUrl": "<pull request URL>"}
632
980
  \`\`\`
@@ -642,11 +990,12 @@ ${testBlock}${readmeBlock}${hookBlock}
642
990
  - One issue only. Do not pick up additional issues.
643
991
  - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
644
992
  }
645
- function buildNativeWorktreePrompt(issue2, repoPath, testRunner, pm, baseBranch) {
993
+ function buildNativeWorktreePrompt(issue2, _repoPath, testRunner, pm, baseBranch, projectContext, manifestPath) {
646
994
  const testBlock = buildTestInstructions(testRunner ?? null, pm);
647
995
  const readmeBlock = buildReadmeInstructions();
648
996
  const hookBlock = buildPreCommitHookInstructions();
649
- const manifestLocation = repoPath ? `\`${join2(repoPath, ".lisa-manifest.json")}\`` : "`.lisa-manifest.json` in the **current directory**";
997
+ const contextBlock = projectContext ? formatProjectContext(projectContext) : "";
998
+ const manifestLocation = manifestPath ? `\`${manifestPath}\`` : "`.lisa-manifest.json` in the **current directory**";
650
999
  return `You are an autonomous implementation agent. Your job is to implement an issue end-to-end: code, push, PR, and tracker update.
651
1000
 
652
1001
  You are working inside a git worktree that was automatically created for this task.
@@ -661,6 +1010,9 @@ Work on the current branch \u2014 it was created for you.
661
1010
  ### Description
662
1011
 
663
1012
  ${issue2.description}
1013
+ ${contextBlock ? `
1014
+ ${contextBlock}
1015
+ ` : ""}
664
1016
 
665
1017
  ## Instructions
666
1018
 
@@ -710,12 +1062,12 @@ ${testBlock}${readmeBlock}${hookBlock}
710
1062
  - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
711
1063
  }
712
1064
  function buildPlanningPrompt(issue2, config2) {
713
- const workspace = resolve3(config2.workspace);
1065
+ const workspace = resolve4(config2.workspace);
714
1066
  const repoBlock = config2.repos.map((r) => {
715
- const absPath = resolve3(workspace, r.path);
1067
+ const absPath = resolve4(workspace, r.path);
716
1068
  return `- **${r.name}**: \`${absPath}\` (base branch: \`${r.base_branch}\`)`;
717
1069
  }).join("\n");
718
- const planPath = join2(workspace, ".lisa-plan.json");
1070
+ const planPath = getPlanPath(workspace);
719
1071
  return `You are an issue analysis agent. Your job is to read the issue below, determine which repositories are affected, and produce an execution plan.
720
1072
 
721
1073
  **Do NOT implement anything.** Only analyze the issue and produce the plan file.
@@ -762,10 +1114,11 @@ ${repoBlock}
762
1114
  - Do NOT implement anything. Do NOT create branches, write code, or commit.
763
1115
  - If only one repo is affected, the plan should have a single step.`;
764
1116
  }
765
- function buildScopedImplementPrompt(issue2, step, previousResults, testRunner, pm, isLastStep = false, baseBranch) {
1117
+ function buildScopedImplementPrompt(issue2, step, previousResults, testRunner, pm, isLastStep = false, baseBranch, projectContext, manifestPath) {
766
1118
  const testBlock = buildTestInstructions(testRunner ?? null, pm);
767
1119
  const readmeBlock = buildReadmeInstructions();
768
1120
  const hookBlock = buildPreCommitHookInstructions();
1121
+ const contextBlock = projectContext ? formatProjectContext(projectContext) : "";
769
1122
  const previousBlock = previousResults.length > 0 ? `
770
1123
  ## Previous Steps
771
1124
 
@@ -794,7 +1147,9 @@ Work on the current branch \u2014 it was created for you.
794
1147
  ### Description
795
1148
 
796
1149
  ${issue2.description}
797
-
1150
+ ${contextBlock ? `
1151
+ ${contextBlock}
1152
+ ` : ""}
798
1153
  ## Your Scope
799
1154
 
800
1155
  You are responsible for **this specific part** of the issue:
@@ -830,7 +1185,7 @@ ${testBlock}${readmeBlock}${hookBlock}
830
1185
  \`gh pr create --title "<conventional-commit-title>" --body "<markdown-summary>"${baseBranch ? ` --base ${baseBranch}` : ""}\`
831
1186
  Capture the PR URL from the output.
832
1187
  ${trackerStep}
833
- 7. **Write manifest**: Create \`.lisa-manifest.json\` in the **current directory** with JSON:
1188
+ 7. **Write manifest**: Create ${manifestPath ? `\`${manifestPath}\`` : "`.lisa-manifest.json` in the **current directory**"} with JSON:
834
1189
  \`\`\`json
835
1190
  {"branch": "<final English branch name>", "prUrl": "<pull request URL>"}
836
1191
  \`\`\`
@@ -847,25 +1202,36 @@ ${trackerStep}
847
1202
  }
848
1203
 
849
1204
  // src/session/guardrails.ts
850
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
851
- import { dirname as dirname2, join as join3 } from "path";
852
- var GUARDRAILS_FILE = ".lisa/guardrails.md";
1205
+ import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1206
+ import { dirname as dirname2, join as join5 } from "path";
1207
+ var LEGACY_GUARDRAILS_FILE = ".lisa/guardrails.md";
853
1208
  var MAX_ENTRIES = 20;
854
1209
  var CONTEXT_LINES = 20;
855
- function guardrailsPath(dir) {
856
- return join3(dir, GUARDRAILS_FILE);
857
- }
858
- function readGuardrails(dir) {
859
- const path = guardrailsPath(dir);
860
- if (!existsSync5(path)) return "";
1210
+ function guardrailsPath(cwd) {
1211
+ return getGuardrailsPath(cwd);
1212
+ }
1213
+ function migrateGuardrails(cwd) {
1214
+ const legacyPath = join5(cwd, LEGACY_GUARDRAILS_FILE);
1215
+ if (!existsSync7(legacyPath)) return;
1216
+ const cachePath = getGuardrailsPath(cwd);
1217
+ if (existsSync7(cachePath)) return;
1218
+ const cacheDir = dirname2(cachePath);
1219
+ if (!existsSync7(cacheDir)) {
1220
+ mkdirSync4(cacheDir, { recursive: true });
1221
+ }
1222
+ copyFileSync(legacyPath, cachePath);
1223
+ }
1224
+ function readGuardrails(cwd) {
1225
+ const path = getGuardrailsPath(cwd);
1226
+ if (!existsSync7(path)) return "";
861
1227
  try {
862
- return readFileSync4(path, "utf-8");
1228
+ return readFileSync5(path, "utf-8");
863
1229
  } catch {
864
1230
  return "";
865
1231
  }
866
1232
  }
867
- function buildGuardrailsSection(dir) {
868
- const content = readGuardrails(dir);
1233
+ function buildGuardrailsSection(cwd) {
1234
+ const content = readGuardrails(cwd);
869
1235
  if (!content.trim()) return "";
870
1236
  return `
871
1237
  ## Guardrails \u2014 Avoid these known pitfalls
@@ -889,10 +1255,10 @@ function extractErrorType(output) {
889
1255
  function appendEntry(dir, entry) {
890
1256
  const path = guardrailsPath(dir);
891
1257
  const guardrailsDir = dirname2(path);
892
- if (!existsSync5(guardrailsDir)) {
893
- mkdirSync3(guardrailsDir, { recursive: true });
1258
+ if (!existsSync7(guardrailsDir)) {
1259
+ mkdirSync4(guardrailsDir, { recursive: true });
894
1260
  }
895
- const existing = existsSync5(path) ? readFileSync4(path, "utf-8") : "";
1261
+ const existing = existsSync7(path) ? readFileSync5(path, "utf-8") : "";
896
1262
  const newEntryText = formatEntry(entry);
897
1263
  let content;
898
1264
  if (!existing.trim()) {
@@ -939,10 +1305,10 @@ function splitEntries(content) {
939
1305
  }
940
1306
 
941
1307
  // src/providers/aider.ts
942
- import { execSync, spawn } from "child_process";
943
- import { appendFileSync as appendFileSync3, mkdtempSync, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
1308
+ import { execSync } from "child_process";
1309
+ import { appendFileSync as appendFileSync3, mkdtempSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
944
1310
  import { tmpdir } from "os";
945
- import { join as join4 } from "path";
1311
+ import { join as join6 } from "path";
946
1312
 
947
1313
  // src/session/overseer.ts
948
1314
  import { execFile } from "child_process";
@@ -971,11 +1337,21 @@ function startOverseer(proc, cwd, config2, getSnapshot = getGitSnapshot) {
971
1337
  };
972
1338
  }
973
1339
  let killed = false;
1340
+ let paused = false;
974
1341
  let lastSnapshot;
975
1342
  let lastChangeTime = Date.now();
976
1343
  let timer = null;
1344
+ const onPauseProvider = () => {
1345
+ paused = true;
1346
+ };
1347
+ const onResumeProvider = () => {
1348
+ paused = false;
1349
+ lastChangeTime = Date.now();
1350
+ };
1351
+ kanbanEmitter.on("loop:pause-provider", onPauseProvider);
1352
+ kanbanEmitter.on("loop:resume-provider", onResumeProvider);
977
1353
  const check = async () => {
978
- if (killed) return;
1354
+ if (killed || paused) return;
979
1355
  try {
980
1356
  const snapshot = await getSnapshot(cwd);
981
1357
  if (lastSnapshot === void 0) {
@@ -1007,6 +1383,8 @@ function startOverseer(proc, cwd, config2, getSnapshot = getGitSnapshot) {
1007
1383
  clearInterval(timer);
1008
1384
  timer = null;
1009
1385
  }
1386
+ kanbanEmitter.off("loop:pause-provider", onPauseProvider);
1387
+ kanbanEmitter.off("loop:resume-provider", onResumeProvider);
1010
1388
  },
1011
1389
  wasKilled() {
1012
1390
  return killed;
@@ -1014,6 +1392,39 @@ function startOverseer(proc, cwd, config2, getSnapshot = getGitSnapshot) {
1014
1392
  };
1015
1393
  }
1016
1394
 
1395
+ // src/providers/pty.ts
1396
+ import { spawn } from "child_process";
1397
+ import { platform } from "os";
1398
+ var ANSI_REGEX = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07]*\x07|\([A-Z0-9]|[A-Z])/g;
1399
+ function stripAnsi(text2) {
1400
+ return text2.replace(ANSI_REGEX, "").replace(/\r\n/g, "\n").replace(/\r/g, "");
1401
+ }
1402
+ function buildPtyArgs(command, os) {
1403
+ const currentOs = os ?? platform();
1404
+ if (currentOs === "darwin") {
1405
+ return { file: "script", args: ["-qF", "/dev/null", "sh", "-c", command] };
1406
+ }
1407
+ if (currentOs === "linux") {
1408
+ return { file: "script", args: ["-qef", "-c", command, "/dev/null"] };
1409
+ }
1410
+ return null;
1411
+ }
1412
+ function spawnWithPty(command, options = {}) {
1413
+ const ptyArgs = buildPtyArgs(command);
1414
+ if (ptyArgs) {
1415
+ const proc2 = spawn(ptyArgs.file, ptyArgs.args, {
1416
+ ...options,
1417
+ stdio: ["ignore", "pipe", "pipe"]
1418
+ });
1419
+ return { proc: proc2, isPty: true };
1420
+ }
1421
+ const proc = spawn("sh", ["-c", command], {
1422
+ ...options,
1423
+ stdio: ["ignore", "pipe", "pipe"]
1424
+ });
1425
+ return { proc, isPty: false };
1426
+ }
1427
+
1017
1428
  // src/providers/aider.ts
1018
1429
  var AiderProvider = class {
1019
1430
  name = "aider";
@@ -1027,27 +1438,22 @@ var AiderProvider = class {
1027
1438
  }
1028
1439
  async run(prompt, opts) {
1029
1440
  const start = Date.now();
1030
- const tmpDir = mkdtempSync(join4(tmpdir(), "lisa-"));
1031
- const promptFile = join4(tmpDir, "prompt.md");
1441
+ const tmpDir = mkdtempSync(join6(tmpdir(), "lisa-"));
1442
+ const promptFile = join6(tmpDir, "prompt.md");
1032
1443
  writeFileSync4(promptFile, prompt, "utf-8");
1033
1444
  try {
1034
1445
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1035
- const proc = spawn(
1036
- "sh",
1037
- ["-c", `aider --message "$(cat '${promptFile}')" --yes-always ${modelFlag}`],
1038
- {
1039
- cwd: opts.cwd,
1040
- stdio: ["ignore", "pipe", "pipe"]
1041
- }
1042
- );
1446
+ const command = `aider --message "$(cat '${promptFile}')" --yes-always ${modelFlag}`;
1447
+ const { proc, isPty } = spawnWithPty(command, { cwd: opts.cwd });
1043
1448
  if (proc.pid) opts.onProcess?.(proc.pid);
1044
1449
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1045
1450
  const chunks = [];
1046
- proc.stdout.on("data", (chunk) => {
1047
- const text2 = chunk.toString();
1048
- if (getOutputMode() !== "tui") process.stdout.write(text2);
1451
+ proc.stdout?.on("data", (chunk) => {
1452
+ const raw = chunk.toString();
1453
+ const text2 = isPty ? stripAnsi(raw) : raw;
1454
+ if (getOutputMode() !== "tui") process.stdout.write(raw);
1049
1455
  if (opts.issueId) {
1050
- kanbanEmitter.emit("issue:output", opts.issueId, text2);
1456
+ kanbanEmitter.emit("issue:output", opts.issueId, raw);
1051
1457
  }
1052
1458
  chunks.push(text2);
1053
1459
  try {
@@ -1055,18 +1461,19 @@ var AiderProvider = class {
1055
1461
  } catch {
1056
1462
  }
1057
1463
  });
1058
- proc.stderr.on("data", (chunk) => {
1059
- const text2 = chunk.toString();
1060
- if (getOutputMode() !== "tui") process.stderr.write(text2);
1464
+ proc.stderr?.on("data", (chunk) => {
1465
+ const raw = chunk.toString();
1466
+ const text2 = isPty ? stripAnsi(raw) : raw;
1467
+ if (getOutputMode() !== "tui") process.stderr.write(raw);
1061
1468
  try {
1062
1469
  appendFileSync3(opts.logFile, text2);
1063
1470
  } catch {
1064
1471
  }
1065
1472
  });
1066
- const exitCode = await new Promise((resolve6) => {
1473
+ const exitCode = await new Promise((resolve7) => {
1067
1474
  proc.on("close", (code) => {
1068
1475
  overseer?.stop();
1069
- resolve6(code ?? 1);
1476
+ resolve7(code ?? 1);
1070
1477
  });
1071
1478
  });
1072
1479
  if (overseer?.wasKilled()) {
@@ -1085,7 +1492,7 @@ var AiderProvider = class {
1085
1492
  };
1086
1493
  } finally {
1087
1494
  try {
1088
- unlinkSync(promptFile);
1495
+ unlinkSync2(promptFile);
1089
1496
  } catch {
1090
1497
  }
1091
1498
  }
@@ -1093,10 +1500,10 @@ var AiderProvider = class {
1093
1500
  };
1094
1501
 
1095
1502
  // src/providers/claude.ts
1096
- import { execSync as execSync2, spawn as spawn2 } from "child_process";
1097
- import { appendFileSync as appendFileSync4, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
1503
+ import { execSync as execSync2 } from "child_process";
1504
+ import { appendFileSync as appendFileSync4, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
1098
1505
  import { tmpdir as tmpdir2 } from "os";
1099
- import { join as join5 } from "path";
1506
+ import { join as join7 } from "path";
1100
1507
  var ClaudeProvider = class {
1101
1508
  name = "claude";
1102
1509
  supportsNativeWorktree = false;
@@ -1111,27 +1518,28 @@ var ClaudeProvider = class {
1111
1518
  }
1112
1519
  async run(prompt, opts) {
1113
1520
  const start = Date.now();
1114
- const tmpDir = mkdtempSync2(join5(tmpdir2(), "lisa-"));
1115
- const promptFile = join5(tmpDir, "prompt.md");
1521
+ const tmpDir = mkdtempSync2(join7(tmpdir2(), "lisa-"));
1522
+ const promptFile = join7(tmpDir, "prompt.md");
1116
1523
  writeFileSync5(promptFile, prompt, "utf-8");
1117
1524
  try {
1118
1525
  const flags = ["-p", "--dangerously-skip-permissions"];
1119
1526
  if (opts.model) {
1120
1527
  flags.push("--model", opts.model);
1121
1528
  }
1122
- const proc = spawn2("sh", ["-c", `claude ${flags.join(" ")} "$(cat '${promptFile}')"`], {
1529
+ const command = `claude ${flags.join(" ")} "$(cat '${promptFile}')"`;
1530
+ const { proc, isPty } = spawnWithPty(command, {
1123
1531
  cwd: opts.cwd,
1124
- stdio: ["ignore", "pipe", "pipe"],
1125
1532
  env: { ...process.env, CLAUDECODE: void 0 }
1126
1533
  });
1127
1534
  if (proc.pid) opts.onProcess?.(proc.pid);
1128
1535
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1129
1536
  const chunks = [];
1130
- proc.stdout.on("data", (chunk) => {
1131
- const text2 = chunk.toString();
1132
- if (getOutputMode() !== "tui") process.stdout.write(text2);
1537
+ proc.stdout?.on("data", (chunk) => {
1538
+ const raw = chunk.toString();
1539
+ const text2 = isPty ? stripAnsi(raw) : raw;
1540
+ if (getOutputMode() !== "tui") process.stdout.write(raw);
1133
1541
  if (opts.issueId) {
1134
- kanbanEmitter.emit("issue:output", opts.issueId, text2);
1542
+ kanbanEmitter.emit("issue:output", opts.issueId, raw);
1135
1543
  }
1136
1544
  chunks.push(text2);
1137
1545
  try {
@@ -1139,18 +1547,19 @@ var ClaudeProvider = class {
1139
1547
  } catch {
1140
1548
  }
1141
1549
  });
1142
- proc.stderr.on("data", (chunk) => {
1143
- const text2 = chunk.toString();
1144
- if (getOutputMode() !== "tui") process.stderr.write(text2);
1550
+ proc.stderr?.on("data", (chunk) => {
1551
+ const raw = chunk.toString();
1552
+ const text2 = isPty ? stripAnsi(raw) : raw;
1553
+ if (getOutputMode() !== "tui") process.stderr.write(raw);
1145
1554
  try {
1146
1555
  appendFileSync4(opts.logFile, text2);
1147
1556
  } catch {
1148
1557
  }
1149
1558
  });
1150
- const exitCode = await new Promise((resolve6) => {
1559
+ const exitCode = await new Promise((resolve7) => {
1151
1560
  proc.on("close", (code) => {
1152
1561
  overseer?.stop();
1153
- resolve6(code ?? 1);
1562
+ resolve7(code ?? 1);
1154
1563
  });
1155
1564
  });
1156
1565
  if (overseer?.wasKilled()) {
@@ -1169,7 +1578,7 @@ var ClaudeProvider = class {
1169
1578
  };
1170
1579
  } finally {
1171
1580
  try {
1172
- unlinkSync2(promptFile);
1581
+ unlinkSync3(promptFile);
1173
1582
  } catch {
1174
1583
  }
1175
1584
  }
@@ -1177,10 +1586,10 @@ var ClaudeProvider = class {
1177
1586
  };
1178
1587
 
1179
1588
  // src/providers/codex.ts
1180
- import { execSync as execSync3, spawn as spawn3 } from "child_process";
1181
- import { appendFileSync as appendFileSync5, mkdtempSync as mkdtempSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6 } from "fs";
1589
+ import { execSync as execSync3 } from "child_process";
1590
+ import { appendFileSync as appendFileSync5, mkdtempSync as mkdtempSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync6 } from "fs";
1182
1591
  import { tmpdir as tmpdir3 } from "os";
1183
- import { join as join6 } from "path";
1592
+ import { join as join8 } from "path";
1184
1593
  var CodexProvider = class {
1185
1594
  name = "codex";
1186
1595
  async isAvailable() {
@@ -1193,25 +1602,25 @@ var CodexProvider = class {
1193
1602
  }
1194
1603
  async run(prompt, opts) {
1195
1604
  const start = Date.now();
1196
- const tmpDir = mkdtempSync3(join6(tmpdir3(), "lisa-"));
1197
- const promptFile = join6(tmpDir, "prompt.md");
1605
+ const tmpDir = mkdtempSync3(join8(tmpdir3(), "lisa-"));
1606
+ const promptFile = join8(tmpDir, "prompt.md");
1198
1607
  writeFileSync6(promptFile, prompt, "utf-8");
1199
1608
  try {
1200
1609
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1201
- const cmd = `codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral ${modelFlag} "$(cat '${promptFile}')"`;
1202
- const proc = spawn3("sh", ["-c", cmd], {
1610
+ const command = `codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral ${modelFlag} "$(cat '${promptFile}')"`;
1611
+ const { proc, isPty } = spawnWithPty(command, {
1203
1612
  cwd: opts.cwd,
1204
- stdio: ["ignore", "pipe", "pipe"],
1205
1613
  env: { ...process.env, CODEX_QUIET_MODE: "1" }
1206
1614
  });
1207
1615
  if (proc.pid) opts.onProcess?.(proc.pid);
1208
1616
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1209
1617
  const chunks = [];
1210
- proc.stdout.on("data", (chunk) => {
1211
- const text2 = chunk.toString();
1212
- if (getOutputMode() !== "tui") process.stdout.write(text2);
1618
+ proc.stdout?.on("data", (chunk) => {
1619
+ const raw = chunk.toString();
1620
+ const text2 = isPty ? stripAnsi(raw) : raw;
1621
+ if (getOutputMode() !== "tui") process.stdout.write(raw);
1213
1622
  if (opts.issueId) {
1214
- kanbanEmitter.emit("issue:output", opts.issueId, text2);
1623
+ kanbanEmitter.emit("issue:output", opts.issueId, raw);
1215
1624
  }
1216
1625
  chunks.push(text2);
1217
1626
  try {
@@ -1219,18 +1628,19 @@ var CodexProvider = class {
1219
1628
  } catch {
1220
1629
  }
1221
1630
  });
1222
- proc.stderr.on("data", (chunk) => {
1223
- const text2 = chunk.toString();
1224
- if (getOutputMode() !== "tui") process.stderr.write(text2);
1631
+ proc.stderr?.on("data", (chunk) => {
1632
+ const raw = chunk.toString();
1633
+ const text2 = isPty ? stripAnsi(raw) : raw;
1634
+ if (getOutputMode() !== "tui") process.stderr.write(raw);
1225
1635
  try {
1226
1636
  appendFileSync5(opts.logFile, text2);
1227
1637
  } catch {
1228
1638
  }
1229
1639
  });
1230
- const exitCode = await new Promise((resolve6) => {
1640
+ const exitCode = await new Promise((resolve7) => {
1231
1641
  proc.on("close", (code) => {
1232
1642
  overseer?.stop();
1233
- resolve6(code ?? 1);
1643
+ resolve7(code ?? 1);
1234
1644
  });
1235
1645
  });
1236
1646
  if (overseer?.wasKilled()) {
@@ -1249,7 +1659,7 @@ var CodexProvider = class {
1249
1659
  };
1250
1660
  } finally {
1251
1661
  try {
1252
- unlinkSync3(promptFile);
1662
+ unlinkSync4(promptFile);
1253
1663
  } catch {
1254
1664
  }
1255
1665
  }
@@ -1257,10 +1667,10 @@ var CodexProvider = class {
1257
1667
  };
1258
1668
 
1259
1669
  // src/providers/copilot.ts
1260
- import { execSync as execSync4, spawn as spawn4 } from "child_process";
1261
- import { appendFileSync as appendFileSync6, mkdtempSync as mkdtempSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync7 } from "fs";
1670
+ import { execSync as execSync4 } from "child_process";
1671
+ import { appendFileSync as appendFileSync6, mkdtempSync as mkdtempSync4, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "fs";
1262
1672
  import { tmpdir as tmpdir4 } from "os";
1263
- import { join as join7 } from "path";
1673
+ import { join as join9 } from "path";
1264
1674
  var CopilotProvider = class {
1265
1675
  name = "copilot";
1266
1676
  async isAvailable() {
@@ -1273,22 +1683,21 @@ var CopilotProvider = class {
1273
1683
  }
1274
1684
  async run(prompt, opts) {
1275
1685
  const start = Date.now();
1276
- const tmpDir = mkdtempSync4(join7(tmpdir4(), "lisa-"));
1277
- const promptFile = join7(tmpDir, "prompt.md");
1686
+ const tmpDir = mkdtempSync4(join9(tmpdir4(), "lisa-"));
1687
+ const promptFile = join9(tmpDir, "prompt.md");
1278
1688
  writeFileSync7(promptFile, prompt, "utf-8");
1279
1689
  try {
1280
- const proc = spawn4("sh", ["-c", `copilot --allow-all -p "$(cat '${promptFile}')"`], {
1281
- cwd: opts.cwd,
1282
- stdio: ["ignore", "pipe", "pipe"]
1283
- });
1690
+ const command = `copilot --allow-all -p "$(cat '${promptFile}')"`;
1691
+ const { proc, isPty } = spawnWithPty(command, { cwd: opts.cwd });
1284
1692
  if (proc.pid) opts.onProcess?.(proc.pid);
1285
1693
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1286
1694
  const chunks = [];
1287
- proc.stdout.on("data", (chunk) => {
1288
- const text2 = chunk.toString();
1289
- if (getOutputMode() !== "tui") process.stdout.write(text2);
1695
+ proc.stdout?.on("data", (chunk) => {
1696
+ const raw = chunk.toString();
1697
+ const text2 = isPty ? stripAnsi(raw) : raw;
1698
+ if (getOutputMode() !== "tui") process.stdout.write(raw);
1290
1699
  if (opts.issueId) {
1291
- kanbanEmitter.emit("issue:output", opts.issueId, text2);
1700
+ kanbanEmitter.emit("issue:output", opts.issueId, raw);
1292
1701
  }
1293
1702
  chunks.push(text2);
1294
1703
  try {
@@ -1296,18 +1705,19 @@ var CopilotProvider = class {
1296
1705
  } catch {
1297
1706
  }
1298
1707
  });
1299
- proc.stderr.on("data", (chunk) => {
1300
- const text2 = chunk.toString();
1301
- if (getOutputMode() !== "tui") process.stderr.write(text2);
1708
+ proc.stderr?.on("data", (chunk) => {
1709
+ const raw = chunk.toString();
1710
+ const text2 = isPty ? stripAnsi(raw) : raw;
1711
+ if (getOutputMode() !== "tui") process.stderr.write(raw);
1302
1712
  try {
1303
1713
  appendFileSync6(opts.logFile, text2);
1304
1714
  } catch {
1305
1715
  }
1306
1716
  });
1307
- const exitCode = await new Promise((resolve6) => {
1717
+ const exitCode = await new Promise((resolve7) => {
1308
1718
  proc.on("close", (code) => {
1309
1719
  overseer?.stop();
1310
- resolve6(code ?? 1);
1720
+ resolve7(code ?? 1);
1311
1721
  });
1312
1722
  });
1313
1723
  if (overseer?.wasKilled()) {
@@ -1326,7 +1736,7 @@ var CopilotProvider = class {
1326
1736
  };
1327
1737
  } finally {
1328
1738
  try {
1329
- unlinkSync4(promptFile);
1739
+ unlinkSync5(promptFile);
1330
1740
  } catch {
1331
1741
  }
1332
1742
  }
@@ -1334,10 +1744,10 @@ var CopilotProvider = class {
1334
1744
  };
1335
1745
 
1336
1746
  // src/providers/cursor.ts
1337
- import { execSync as execSync5, spawn as spawn5 } from "child_process";
1338
- import { appendFileSync as appendFileSync7, mkdtempSync as mkdtempSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "fs";
1747
+ import { execSync as execSync5 } from "child_process";
1748
+ import { appendFileSync as appendFileSync7, mkdtempSync as mkdtempSync5, unlinkSync as unlinkSync6, writeFileSync as writeFileSync8 } from "fs";
1339
1749
  import { tmpdir as tmpdir5 } from "os";
1340
- import { join as join8 } from "path";
1750
+ import { join as join10 } from "path";
1341
1751
  function findCursorBinary() {
1342
1752
  for (const bin of ["agent", "cursor-agent"]) {
1343
1753
  try {
@@ -1363,27 +1773,22 @@ var CursorProvider = class {
1363
1773
  duration: Date.now() - start
1364
1774
  };
1365
1775
  }
1366
- const tmpDir = mkdtempSync5(join8(tmpdir5(), "lisa-"));
1367
- const promptFile = join8(tmpDir, "prompt.md");
1776
+ const tmpDir = mkdtempSync5(join10(tmpdir5(), "lisa-"));
1777
+ const promptFile = join10(tmpDir, "prompt.md");
1368
1778
  writeFileSync8(promptFile, prompt, "utf-8");
1369
1779
  try {
1370
1780
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1371
- const proc = spawn5(
1372
- "sh",
1373
- ["-c", `${bin} -p "$(cat '${promptFile}')" --output-format text --force ${modelFlag}`],
1374
- {
1375
- cwd: opts.cwd,
1376
- stdio: ["ignore", "pipe", "pipe"]
1377
- }
1378
- );
1781
+ const command = `${bin} -p "$(cat '${promptFile}')" --output-format text --force ${modelFlag}`;
1782
+ const { proc, isPty } = spawnWithPty(command, { cwd: opts.cwd });
1379
1783
  if (proc.pid) opts.onProcess?.(proc.pid);
1380
1784
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1381
1785
  const chunks = [];
1382
- proc.stdout.on("data", (chunk) => {
1383
- const text2 = chunk.toString();
1384
- if (getOutputMode() !== "tui") process.stdout.write(text2);
1786
+ proc.stdout?.on("data", (chunk) => {
1787
+ const raw = chunk.toString();
1788
+ const text2 = isPty ? stripAnsi(raw) : raw;
1789
+ if (getOutputMode() !== "tui") process.stdout.write(raw);
1385
1790
  if (opts.issueId) {
1386
- kanbanEmitter.emit("issue:output", opts.issueId, text2);
1791
+ kanbanEmitter.emit("issue:output", opts.issueId, raw);
1387
1792
  }
1388
1793
  chunks.push(text2);
1389
1794
  try {
@@ -1391,18 +1796,19 @@ var CursorProvider = class {
1391
1796
  } catch {
1392
1797
  }
1393
1798
  });
1394
- proc.stderr.on("data", (chunk) => {
1395
- const text2 = chunk.toString();
1396
- if (getOutputMode() !== "tui") process.stderr.write(text2);
1799
+ proc.stderr?.on("data", (chunk) => {
1800
+ const raw = chunk.toString();
1801
+ const text2 = isPty ? stripAnsi(raw) : raw;
1802
+ if (getOutputMode() !== "tui") process.stderr.write(raw);
1397
1803
  try {
1398
1804
  appendFileSync7(opts.logFile, text2);
1399
1805
  } catch {
1400
1806
  }
1401
1807
  });
1402
- const exitCode = await new Promise((resolve6) => {
1808
+ const exitCode = await new Promise((resolve7) => {
1403
1809
  proc.on("close", (code) => {
1404
1810
  overseer?.stop();
1405
- resolve6(code ?? 1);
1811
+ resolve7(code ?? 1);
1406
1812
  });
1407
1813
  });
1408
1814
  if (overseer?.wasKilled()) {
@@ -1421,7 +1827,7 @@ var CursorProvider = class {
1421
1827
  };
1422
1828
  } finally {
1423
1829
  try {
1424
- unlinkSync5(promptFile);
1830
+ unlinkSync6(promptFile);
1425
1831
  } catch {
1426
1832
  }
1427
1833
  }
@@ -1429,10 +1835,10 @@ var CursorProvider = class {
1429
1835
  };
1430
1836
 
1431
1837
  // src/providers/gemini.ts
1432
- import { execSync as execSync6, spawn as spawn6 } from "child_process";
1433
- import { appendFileSync as appendFileSync8, mkdtempSync as mkdtempSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "fs";
1838
+ import { execSync as execSync6 } from "child_process";
1839
+ import { appendFileSync as appendFileSync8, mkdtempSync as mkdtempSync6, unlinkSync as unlinkSync7, writeFileSync as writeFileSync9 } from "fs";
1434
1840
  import { tmpdir as tmpdir6 } from "os";
1435
- import { join as join9 } from "path";
1841
+ import { join as join11 } from "path";
1436
1842
  var GeminiProvider = class {
1437
1843
  name = "gemini";
1438
1844
  async isAvailable() {
@@ -1445,23 +1851,22 @@ var GeminiProvider = class {
1445
1851
  }
1446
1852
  async run(prompt, opts) {
1447
1853
  const start = Date.now();
1448
- const tmpDir = mkdtempSync6(join9(tmpdir6(), "lisa-"));
1449
- const promptFile = join9(tmpDir, "prompt.md");
1854
+ const tmpDir = mkdtempSync6(join11(tmpdir6(), "lisa-"));
1855
+ const promptFile = join11(tmpDir, "prompt.md");
1450
1856
  writeFileSync9(promptFile, prompt, "utf-8");
1451
1857
  try {
1452
1858
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1453
- const proc = spawn6("sh", ["-c", `gemini --yolo ${modelFlag} -p "$(cat '${promptFile}')"`], {
1454
- cwd: opts.cwd,
1455
- stdio: ["ignore", "pipe", "pipe"]
1456
- });
1859
+ const command = `gemini --yolo ${modelFlag} -p "$(cat '${promptFile}')"`;
1860
+ const { proc, isPty } = spawnWithPty(command, { cwd: opts.cwd });
1457
1861
  if (proc.pid) opts.onProcess?.(proc.pid);
1458
1862
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1459
1863
  const chunks = [];
1460
- proc.stdout.on("data", (chunk) => {
1461
- const text2 = chunk.toString();
1462
- if (getOutputMode() !== "tui") process.stdout.write(text2);
1864
+ proc.stdout?.on("data", (chunk) => {
1865
+ const raw = chunk.toString();
1866
+ const text2 = isPty ? stripAnsi(raw) : raw;
1867
+ if (getOutputMode() !== "tui") process.stdout.write(raw);
1463
1868
  if (opts.issueId) {
1464
- kanbanEmitter.emit("issue:output", opts.issueId, text2);
1869
+ kanbanEmitter.emit("issue:output", opts.issueId, raw);
1465
1870
  }
1466
1871
  chunks.push(text2);
1467
1872
  try {
@@ -1469,18 +1874,19 @@ var GeminiProvider = class {
1469
1874
  } catch {
1470
1875
  }
1471
1876
  });
1472
- proc.stderr.on("data", (chunk) => {
1473
- const text2 = chunk.toString();
1474
- if (getOutputMode() !== "tui") process.stderr.write(text2);
1877
+ proc.stderr?.on("data", (chunk) => {
1878
+ const raw = chunk.toString();
1879
+ const text2 = isPty ? stripAnsi(raw) : raw;
1880
+ if (getOutputMode() !== "tui") process.stderr.write(raw);
1475
1881
  try {
1476
1882
  appendFileSync8(opts.logFile, text2);
1477
1883
  } catch {
1478
1884
  }
1479
1885
  });
1480
- const exitCode = await new Promise((resolve6) => {
1886
+ const exitCode = await new Promise((resolve7) => {
1481
1887
  proc.on("close", (code) => {
1482
1888
  overseer?.stop();
1483
- resolve6(code ?? 1);
1889
+ resolve7(code ?? 1);
1484
1890
  });
1485
1891
  });
1486
1892
  if (overseer?.wasKilled()) {
@@ -1499,7 +1905,7 @@ var GeminiProvider = class {
1499
1905
  };
1500
1906
  } finally {
1501
1907
  try {
1502
- unlinkSync6(promptFile);
1908
+ unlinkSync7(promptFile);
1503
1909
  } catch {
1504
1910
  }
1505
1911
  }
@@ -1507,10 +1913,10 @@ var GeminiProvider = class {
1507
1913
  };
1508
1914
 
1509
1915
  // src/providers/goose.ts
1510
- import { execSync as execSync7, spawn as spawn7 } from "child_process";
1511
- import { appendFileSync as appendFileSync9, mkdtempSync as mkdtempSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync10 } from "fs";
1916
+ import { execSync as execSync7 } from "child_process";
1917
+ import { appendFileSync as appendFileSync9, mkdtempSync as mkdtempSync7, unlinkSync as unlinkSync8, writeFileSync as writeFileSync10 } from "fs";
1512
1918
  import { tmpdir as tmpdir7 } from "os";
1513
- import { join as join10 } from "path";
1919
+ import { join as join12 } from "path";
1514
1920
  var GooseProvider = class {
1515
1921
  name = "goose";
1516
1922
  async isAvailable() {
@@ -1523,23 +1929,22 @@ var GooseProvider = class {
1523
1929
  }
1524
1930
  async run(prompt, opts) {
1525
1931
  const start = Date.now();
1526
- const tmpDir = mkdtempSync7(join10(tmpdir7(), "lisa-"));
1527
- const promptFile = join10(tmpDir, "prompt.md");
1932
+ const tmpDir = mkdtempSync7(join12(tmpdir7(), "lisa-"));
1933
+ const promptFile = join12(tmpDir, "prompt.md");
1528
1934
  writeFileSync10(promptFile, prompt, "utf-8");
1529
1935
  try {
1530
1936
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1531
- const proc = spawn7("sh", ["-c", `goose run ${modelFlag} --text "$(cat '${promptFile}')"`], {
1532
- cwd: opts.cwd,
1533
- stdio: ["ignore", "pipe", "pipe"]
1534
- });
1937
+ const command = `goose run ${modelFlag} --text "$(cat '${promptFile}')"`;
1938
+ const { proc, isPty } = spawnWithPty(command, { cwd: opts.cwd });
1535
1939
  if (proc.pid) opts.onProcess?.(proc.pid);
1536
1940
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1537
1941
  const chunks = [];
1538
- proc.stdout.on("data", (chunk) => {
1539
- const text2 = chunk.toString();
1540
- if (getOutputMode() !== "tui") process.stdout.write(text2);
1942
+ proc.stdout?.on("data", (chunk) => {
1943
+ const raw = chunk.toString();
1944
+ const text2 = isPty ? stripAnsi(raw) : raw;
1945
+ if (getOutputMode() !== "tui") process.stdout.write(raw);
1541
1946
  if (opts.issueId) {
1542
- kanbanEmitter.emit("issue:output", opts.issueId, text2);
1947
+ kanbanEmitter.emit("issue:output", opts.issueId, raw);
1543
1948
  }
1544
1949
  chunks.push(text2);
1545
1950
  try {
@@ -1547,18 +1952,19 @@ var GooseProvider = class {
1547
1952
  } catch {
1548
1953
  }
1549
1954
  });
1550
- proc.stderr.on("data", (chunk) => {
1551
- const text2 = chunk.toString();
1552
- if (getOutputMode() !== "tui") process.stderr.write(text2);
1955
+ proc.stderr?.on("data", (chunk) => {
1956
+ const raw = chunk.toString();
1957
+ const text2 = isPty ? stripAnsi(raw) : raw;
1958
+ if (getOutputMode() !== "tui") process.stderr.write(raw);
1553
1959
  try {
1554
1960
  appendFileSync9(opts.logFile, text2);
1555
1961
  } catch {
1556
1962
  }
1557
1963
  });
1558
- const exitCode = await new Promise((resolve6) => {
1964
+ const exitCode = await new Promise((resolve7) => {
1559
1965
  proc.on("close", (code) => {
1560
1966
  overseer?.stop();
1561
- resolve6(code ?? 1);
1967
+ resolve7(code ?? 1);
1562
1968
  });
1563
1969
  });
1564
1970
  if (overseer?.wasKilled()) {
@@ -1577,7 +1983,7 @@ var GooseProvider = class {
1577
1983
  };
1578
1984
  } finally {
1579
1985
  try {
1580
- unlinkSync7(promptFile);
1986
+ unlinkSync8(promptFile);
1581
1987
  } catch {
1582
1988
  }
1583
1989
  }
@@ -1585,10 +1991,10 @@ var GooseProvider = class {
1585
1991
  };
1586
1992
 
1587
1993
  // src/providers/opencode.ts
1588
- import { execSync as execSync8, spawn as spawn8 } from "child_process";
1589
- import { appendFileSync as appendFileSync10, mkdtempSync as mkdtempSync8, unlinkSync as unlinkSync8, writeFileSync as writeFileSync11 } from "fs";
1994
+ import { execSync as execSync8 } from "child_process";
1995
+ import { appendFileSync as appendFileSync10, mkdtempSync as mkdtempSync8, unlinkSync as unlinkSync9, writeFileSync as writeFileSync11 } from "fs";
1590
1996
  import { tmpdir as tmpdir8 } from "os";
1591
- import { join as join11 } from "path";
1997
+ import { join as join13 } from "path";
1592
1998
  var OpenCodeProvider = class {
1593
1999
  name = "opencode";
1594
2000
  async isAvailable() {
@@ -1601,22 +2007,21 @@ var OpenCodeProvider = class {
1601
2007
  }
1602
2008
  async run(prompt, opts) {
1603
2009
  const start = Date.now();
1604
- const tmpDir = mkdtempSync8(join11(tmpdir8(), "lisa-"));
1605
- const promptFile = join11(tmpDir, "prompt.md");
2010
+ const tmpDir = mkdtempSync8(join13(tmpdir8(), "lisa-"));
2011
+ const promptFile = join13(tmpDir, "prompt.md");
1606
2012
  writeFileSync11(promptFile, prompt, "utf-8");
1607
2013
  try {
1608
- const proc = spawn8("sh", ["-c", `opencode run "$(cat '${promptFile}')"`], {
1609
- cwd: opts.cwd,
1610
- stdio: ["ignore", "pipe", "pipe"]
1611
- });
2014
+ const command = `opencode run "$(cat '${promptFile}')"`;
2015
+ const { proc, isPty } = spawnWithPty(command, { cwd: opts.cwd });
1612
2016
  if (proc.pid) opts.onProcess?.(proc.pid);
1613
2017
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1614
2018
  const chunks = [];
1615
- proc.stdout.on("data", (chunk) => {
1616
- const text2 = chunk.toString();
1617
- if (getOutputMode() !== "tui") process.stdout.write(text2);
2019
+ proc.stdout?.on("data", (chunk) => {
2020
+ const raw = chunk.toString();
2021
+ const text2 = isPty ? stripAnsi(raw) : raw;
2022
+ if (getOutputMode() !== "tui") process.stdout.write(raw);
1618
2023
  if (opts.issueId) {
1619
- kanbanEmitter.emit("issue:output", opts.issueId, text2);
2024
+ kanbanEmitter.emit("issue:output", opts.issueId, raw);
1620
2025
  }
1621
2026
  chunks.push(text2);
1622
2027
  try {
@@ -1624,18 +2029,19 @@ var OpenCodeProvider = class {
1624
2029
  } catch {
1625
2030
  }
1626
2031
  });
1627
- proc.stderr.on("data", (chunk) => {
1628
- const text2 = chunk.toString();
1629
- if (getOutputMode() !== "tui") process.stderr.write(text2);
2032
+ proc.stderr?.on("data", (chunk) => {
2033
+ const raw = chunk.toString();
2034
+ const text2 = isPty ? stripAnsi(raw) : raw;
2035
+ if (getOutputMode() !== "tui") process.stderr.write(raw);
1630
2036
  try {
1631
2037
  appendFileSync10(opts.logFile, text2);
1632
2038
  } catch {
1633
2039
  }
1634
2040
  });
1635
- const exitCode = await new Promise((resolve6) => {
2041
+ const exitCode = await new Promise((resolve7) => {
1636
2042
  proc.on("close", (code) => {
1637
2043
  overseer?.stop();
1638
- resolve6(code ?? 1);
2044
+ resolve7(code ?? 1);
1639
2045
  });
1640
2046
  });
1641
2047
  if (overseer?.wasKilled()) {
@@ -1654,7 +2060,7 @@ var OpenCodeProvider = class {
1654
2060
  };
1655
2061
  } finally {
1656
2062
  try {
1657
- unlinkSync8(promptFile);
2063
+ unlinkSync9(promptFile);
1658
2064
  } catch {
1659
2065
  }
1660
2066
  }
@@ -1718,6 +2124,9 @@ function isCompleteProviderExhaustion(attempts) {
1718
2124
  async function runWithFallback(models, prompt, opts) {
1719
2125
  const attempts = [];
1720
2126
  for (const spec of models) {
2127
+ if (opts.shouldAbort?.()) {
2128
+ break;
2129
+ }
1721
2130
  const provider = createProvider(spec.provider);
1722
2131
  const available = await provider.isAvailable();
1723
2132
  if (!available) {
@@ -1799,34 +2208,34 @@ function formatAttemptsReport(attempts) {
1799
2208
  }
1800
2209
 
1801
2210
  // src/session/lifecycle.ts
1802
- import { spawn as spawn9 } from "child_process";
2211
+ import { spawn as spawn2 } from "child_process";
1803
2212
  import { createConnection } from "net";
1804
- import { resolve as resolve4 } from "path";
2213
+ import { resolve as resolve5 } from "path";
1805
2214
  var managedResources = [];
1806
2215
  var cleanupRegistered = false;
1807
2216
  function isPortInUse(port) {
1808
- return new Promise((resolve6) => {
2217
+ return new Promise((resolve7) => {
1809
2218
  const socket = createConnection({ port }, () => {
1810
2219
  socket.destroy();
1811
- resolve6(true);
2220
+ resolve7(true);
1812
2221
  });
1813
2222
  socket.on("error", () => {
1814
2223
  socket.destroy();
1815
- resolve6(false);
2224
+ resolve7(false);
1816
2225
  });
1817
2226
  });
1818
2227
  }
1819
2228
  function waitForPort(port, timeoutMs) {
1820
- return new Promise((resolve6) => {
2229
+ return new Promise((resolve7) => {
1821
2230
  const deadline = Date.now() + timeoutMs;
1822
2231
  const check = () => {
1823
2232
  if (Date.now() > deadline) {
1824
- resolve6(false);
2233
+ resolve7(false);
1825
2234
  return;
1826
2235
  }
1827
2236
  isPortInUse(port).then((inUse) => {
1828
2237
  if (inUse) {
1829
- resolve6(true);
2238
+ resolve7(true);
1830
2239
  } else {
1831
2240
  setTimeout(check, 500);
1832
2241
  }
@@ -1836,8 +2245,8 @@ function waitForPort(port, timeoutMs) {
1836
2245
  });
1837
2246
  }
1838
2247
  function spawnResource(config2, baseCwd) {
1839
- const cwd = config2.cwd ? resolve4(baseCwd, config2.cwd) : baseCwd;
1840
- const child = spawn9("sh", ["-c", config2.up], {
2248
+ const cwd = config2.cwd ? resolve5(baseCwd, config2.cwd) : baseCwd;
2249
+ const child = spawn2("sh", ["-c", config2.up], {
1841
2250
  cwd,
1842
2251
  stdio: "ignore",
1843
2252
  detached: true
@@ -1846,14 +2255,14 @@ function spawnResource(config2, baseCwd) {
1846
2255
  return child;
1847
2256
  }
1848
2257
  function runSetupCommand(command, cwd) {
1849
- return new Promise((resolve6, reject) => {
1850
- const child = spawn9("sh", ["-c", command], {
2258
+ return new Promise((resolve7, reject) => {
2259
+ const child = spawn2("sh", ["-c", command], {
1851
2260
  cwd,
1852
2261
  stdio: "inherit"
1853
2262
  });
1854
2263
  child.on("close", (code) => {
1855
2264
  if (code === 0) {
1856
- resolve6();
2265
+ resolve7();
1857
2266
  } else {
1858
2267
  reject(new Error(`Setup command failed with exit code ${code}: ${command}`));
1859
2268
  }
@@ -1917,12 +2326,12 @@ async function stopResources() {
1917
2326
  }
1918
2327
  }
1919
2328
  } else {
1920
- await new Promise((resolve6) => {
1921
- const down = spawn9("sh", ["-c", config2.down], {
2329
+ await new Promise((resolve7) => {
2330
+ const down = spawn2("sh", ["-c", config2.down], {
1922
2331
  stdio: "ignore"
1923
2332
  });
1924
- down.on("close", () => resolve6());
1925
- down.on("error", () => resolve6());
2333
+ down.on("close", () => resolve7());
2334
+ down.on("error", () => resolve7());
1926
2335
  });
1927
2336
  }
1928
2337
  ok(`Resource "${name}" stopped`);
@@ -1965,6 +2374,7 @@ function registerCleanup() {
1965
2374
  var API_URL = "https://api.github.com";
1966
2375
  var REQUEST_TIMEOUT_MS = 3e4;
1967
2376
  var PRIORITY_LABELS = ["p1", "p2", "p3"];
2377
+ var DEPENDENCY_PATTERN = /(?:depends\s+on|blocked\s+by)\s+#(\d+)/gi;
1968
2378
  function getAuthHeaders() {
1969
2379
  const token = process.env.GITHUB_TOKEN;
1970
2380
  if (!token) throw new Error("GITHUB_TOKEN must be set");
@@ -2034,6 +2444,16 @@ function parseGitHubIssueNumber(id) {
2034
2444
  }
2035
2445
  return { owner: "", repo: "", number: id };
2036
2446
  }
2447
+ function parseDependencies(body) {
2448
+ if (!body) return [];
2449
+ const deps = [];
2450
+ const matches = body.matchAll(DEPENDENCY_PATTERN);
2451
+ for (const match of matches) {
2452
+ const num = Number.parseInt(match[1] ?? "", 10);
2453
+ if (!Number.isNaN(num)) deps.push(num);
2454
+ }
2455
+ return [...new Set(deps)];
2456
+ }
2037
2457
  function makeIssueId(owner, repo, number) {
2038
2458
  return `${owner}/${repo}#${number}`;
2039
2459
  }
@@ -2041,11 +2461,48 @@ var GitHubIssuesSource = class {
2041
2461
  name = "github-issues";
2042
2462
  async fetchNextIssue(config2) {
2043
2463
  const { owner, repo } = parseOwnerRepo(config2.team);
2044
- const label = encodeURIComponent(config2.label);
2464
+ const labels = Array.isArray(config2.label) ? config2.label : [config2.label];
2465
+ const label = labels.map((l) => encodeURIComponent(l)).join(",");
2045
2466
  const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
2046
2467
  const issues = await githubGet(path);
2047
2468
  if (issues.length === 0) return null;
2048
- const sorted = [...issues].sort((a, b) => {
2469
+ const unblocked = [];
2470
+ const blocked = [];
2471
+ for (const issue3 of issues) {
2472
+ const depNumbers = parseDependencies(issue3.body);
2473
+ if (depNumbers.length === 0) {
2474
+ unblocked.push(issue3);
2475
+ continue;
2476
+ }
2477
+ const activeBlockers = [];
2478
+ for (const depNum of depNumbers) {
2479
+ try {
2480
+ const dep = await githubGet(`/repos/${owner}/${repo}/issues/${depNum}`);
2481
+ if (!dep.state || dep.state === "open") {
2482
+ activeBlockers.push(depNum);
2483
+ }
2484
+ } catch {
2485
+ activeBlockers.push(depNum);
2486
+ }
2487
+ }
2488
+ if (activeBlockers.length === 0) {
2489
+ unblocked.push(issue3);
2490
+ } else {
2491
+ blocked.push({ number: issue3.number, blockers: activeBlockers });
2492
+ }
2493
+ }
2494
+ if (unblocked.length === 0) {
2495
+ if (blocked.length > 0) {
2496
+ warn("No unblocked issues found. Blocked issues:");
2497
+ for (const entry of blocked) {
2498
+ warn(
2499
+ ` #${entry.number} \u2014 blocked by: ${entry.blockers.map((b) => `#${b}`).join(", ")}`
2500
+ );
2501
+ }
2502
+ }
2503
+ return null;
2504
+ }
2505
+ const sorted = [...unblocked].sort((a, b) => {
2049
2506
  const pa = priorityRank(a.labels);
2050
2507
  const pb = priorityRank(b.labels);
2051
2508
  if (pa !== pb) return pa - pb;
@@ -2099,7 +2556,8 @@ var GitHubIssuesSource = class {
2099
2556
  }
2100
2557
  async listIssues(config2) {
2101
2558
  const { owner, repo } = parseOwnerRepo(config2.team);
2102
- const label = encodeURIComponent(config2.label);
2559
+ const labels = Array.isArray(config2.label) ? config2.label : [config2.label];
2560
+ const label = labels.map((l) => encodeURIComponent(l)).join(",");
2103
2561
  const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
2104
2562
  const issues = await githubGet(path);
2105
2563
  return issues.map((issue2) => ({
@@ -2181,11 +2639,41 @@ var GitLabIssuesSource = class {
2181
2639
  name = "gitlab-issues";
2182
2640
  async fetchNextIssue(config2) {
2183
2641
  const project = parseGitLabProject(config2.team);
2184
- const label = encodeURIComponent(config2.label);
2642
+ const labelsArr = Array.isArray(config2.label) ? config2.label : [config2.label];
2643
+ const label = labelsArr.map((l) => encodeURIComponent(l)).join(",");
2185
2644
  const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
2186
2645
  const issues = await gitlabGet(path);
2187
2646
  if (issues.length === 0) return null;
2188
- const sorted = [...issues].sort((a, b) => {
2647
+ const unblocked = [];
2648
+ const blocked = [];
2649
+ for (const issue3 of issues) {
2650
+ const links = await gitlabGet(
2651
+ `/projects/${project}/issues/${issue3.iid}/links`
2652
+ );
2653
+ const activeBlockers = links.filter((link) => {
2654
+ if (link.link_type === "is_blocked_by") {
2655
+ return link.source.state !== "closed";
2656
+ }
2657
+ return false;
2658
+ }).map((link) => link.source.iid);
2659
+ if (activeBlockers.length === 0) {
2660
+ unblocked.push(issue3);
2661
+ } else {
2662
+ blocked.push({ iid: issue3.iid, blockers: activeBlockers });
2663
+ }
2664
+ }
2665
+ if (unblocked.length === 0) {
2666
+ if (blocked.length > 0) {
2667
+ warn("No unblocked issues found. Blocked issues:");
2668
+ for (const entry of blocked) {
2669
+ warn(
2670
+ ` #${entry.iid} \u2014 blocked by: ${entry.blockers.map((b) => `#${b}`).join(", ")}`
2671
+ );
2672
+ }
2673
+ }
2674
+ return null;
2675
+ }
2676
+ const sorted = [...unblocked].sort((a, b) => {
2189
2677
  const pa = priorityRank2(a.labels);
2190
2678
  const pb = priorityRank2(b.labels);
2191
2679
  if (pa !== pb) return pa - pb;
@@ -2241,7 +2729,8 @@ var GitLabIssuesSource = class {
2241
2729
  }
2242
2730
  async listIssues(config2) {
2243
2731
  const project = parseGitLabProject(config2.team);
2244
- const label = encodeURIComponent(config2.label);
2732
+ const labelsArr = Array.isArray(config2.label) ? config2.label : [config2.label];
2733
+ const label = labelsArr.map((l) => encodeURIComponent(l)).join(",");
2245
2734
  const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
2246
2735
  const issues = await gitlabGet(path);
2247
2736
  return issues.map((issue2) => ({
@@ -2356,16 +2845,42 @@ function issueUrl(baseUrl, key) {
2356
2845
  var JiraSource = class {
2357
2846
  name = "jira";
2358
2847
  async fetchNextIssue(config2) {
2848
+ const labels = Array.isArray(config2.label) ? config2.label : [config2.label];
2849
+ const labelClause = labels.map((l) => `labels = "${l}"`).join(" AND ");
2359
2850
  const jql = encodeURIComponent(
2360
- `project = "${config2.team}" AND labels = "${config2.label}" AND status = "${config2.pick_from}" ORDER BY priority ASC, created ASC`
2851
+ `project = "${config2.team}" AND ${labelClause} AND status = "${config2.pick_from}" ORDER BY priority ASC, created ASC`
2361
2852
  );
2362
- const fields = "summary,description,priority,status,labels";
2853
+ const fields = "summary,description,priority,status,labels,issuelinks";
2363
2854
  const data = await jiraGet(
2364
2855
  `/search?jql=${jql}&fields=${fields}&maxResults=50`
2365
2856
  );
2366
2857
  const issues = data.issues ?? [];
2367
2858
  if (issues.length === 0) return null;
2368
- const sorted = [...issues].sort((a, b) => priorityRank3(a) - priorityRank3(b));
2859
+ const unblocked = [];
2860
+ const blocked = [];
2861
+ for (const issue3 of issues) {
2862
+ const activeBlockers = (issue3.fields.issuelinks ?? []).filter((link) => {
2863
+ if (link.inwardIssue && link.type.inward.toLowerCase().includes("is blocked by")) {
2864
+ return link.inwardIssue.fields.status.statusCategory.key !== "done";
2865
+ }
2866
+ return false;
2867
+ }).map((link) => link.inwardIssue?.key ?? "");
2868
+ if (activeBlockers.length === 0) {
2869
+ unblocked.push(issue3);
2870
+ } else {
2871
+ blocked.push({ key: issue3.key, blockers: activeBlockers });
2872
+ }
2873
+ }
2874
+ if (unblocked.length === 0) {
2875
+ if (blocked.length > 0) {
2876
+ warn("No unblocked issues found. Blocked issues:");
2877
+ for (const entry of blocked) {
2878
+ warn(` ${entry.key} \u2014 blocked by: ${entry.blockers.join(", ")}`);
2879
+ }
2880
+ }
2881
+ return null;
2882
+ }
2883
+ const sorted = [...unblocked].sort((a, b) => priorityRank3(a) - priorityRank3(b));
2369
2884
  const issue2 = sorted[0];
2370
2885
  if (!issue2) return null;
2371
2886
  const baseUrl = getBaseUrl2();
@@ -2425,8 +2940,10 @@ var JiraSource = class {
2425
2940
  }
2426
2941
  }
2427
2942
  async listIssues(config2) {
2943
+ const labels = Array.isArray(config2.label) ? config2.label : [config2.label];
2944
+ const labelClause = labels.map((l) => `labels = "${l}"`).join(" AND ");
2428
2945
  const jql = encodeURIComponent(
2429
- `project = "${config2.team}" AND labels = "${config2.label}" AND status = "${config2.pick_from}" ORDER BY priority ASC, created ASC`
2946
+ `project = "${config2.team}" AND ${labelClause} AND status = "${config2.pick_from}" ORDER BY priority ASC, created ASC`
2430
2947
  );
2431
2948
  const fields = "summary,description,priority,status,labels";
2432
2949
  const data = await jiraGet(
@@ -2486,6 +3003,8 @@ async function gql(query, variables) {
2486
3003
  var LinearSource = class {
2487
3004
  name = "linear";
2488
3005
  async fetchNextIssue(config2) {
3006
+ const labels = Array.isArray(config2.label) ? config2.label : [config2.label];
3007
+ const primaryLabel = labels[0] ?? "";
2489
3008
  const data = await gql(
2490
3009
  `query($teamName: String!, $projectName: String!, $labelName: String!, $statusName: String!) {
2491
3010
  issues(
@@ -2504,6 +3023,7 @@ var LinearSource = class {
2504
3023
  description
2505
3024
  url
2506
3025
  priority
3026
+ labels { nodes { name } }
2507
3027
  inverseRelations(first: 50) {
2508
3028
  nodes {
2509
3029
  type
@@ -2519,11 +3039,14 @@ var LinearSource = class {
2519
3039
  {
2520
3040
  teamName: config2.team,
2521
3041
  projectName: config2.project,
2522
- labelName: config2.label,
3042
+ labelName: primaryLabel,
2523
3043
  statusName: config2.pick_from
2524
3044
  }
2525
3045
  );
2526
- const issues = data.issues.nodes;
3046
+ const issues = labels.length > 1 ? data.issues.nodes.filter((issue3) => {
3047
+ const issueLabels = new Set(issue3.labels.nodes.map((l) => l.name.toLowerCase()));
3048
+ return labels.every((l) => issueLabels.has(l.toLowerCase()));
3049
+ }) : data.issues.nodes;
2527
3050
  if (issues.length === 0) return null;
2528
3051
  const unblocked = [];
2529
3052
  const blocked = [];
@@ -2672,6 +3195,8 @@ var LinearSource = class {
2672
3195
  }
2673
3196
  }
2674
3197
  async listIssues(config2) {
3198
+ const labels = Array.isArray(config2.label) ? config2.label : [config2.label];
3199
+ const primaryLabel = labels[0] ?? "";
2675
3200
  const data = await gql(
2676
3201
  `query($teamName: String!, $projectName: String!, $labelName: String!, $statusName: String!) {
2677
3202
  issues(
@@ -2688,17 +3213,22 @@ var LinearSource = class {
2688
3213
  title
2689
3214
  description
2690
3215
  url
3216
+ labels { nodes { name } }
2691
3217
  }
2692
3218
  }
2693
3219
  }`,
2694
3220
  {
2695
3221
  teamName: config2.team,
2696
3222
  projectName: config2.project,
2697
- labelName: config2.label,
3223
+ labelName: primaryLabel,
2698
3224
  statusName: config2.pick_from
2699
3225
  }
2700
3226
  );
2701
- return data.issues.nodes.map((issue2) => ({
3227
+ const filtered = labels.length > 1 ? data.issues.nodes.filter((issue2) => {
3228
+ const issueLabels = new Set(issue2.labels.nodes.map((l) => l.name.toLowerCase()));
3229
+ return labels.every((l) => issueLabels.has(l.toLowerCase()));
3230
+ }) : data.issues.nodes;
3231
+ return filtered.map((issue2) => ({
2702
3232
  id: issue2.identifier,
2703
3233
  title: issue2.title,
2704
3234
  description: issue2.description || "",
@@ -2861,13 +3391,56 @@ var PlaneSource = class {
2861
3391
  const workspaceSlug = config2.team;
2862
3392
  const projectId = await resolveProjectId(workspaceSlug, config2.project);
2863
3393
  const stateId = await resolveStateId(workspaceSlug, projectId, config2.pick_from);
2864
- const labelId = await resolveLabelId(workspaceSlug, projectId, config2.label);
3394
+ const labelNames = Array.isArray(config2.label) ? config2.label : [config2.label];
3395
+ const labelIds = await Promise.all(
3396
+ labelNames.map((name) => resolveLabelId(workspaceSlug, projectId, name))
3397
+ );
2865
3398
  const data = await planeGet(
2866
3399
  `/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${stateId}&per_page=100`
2867
3400
  );
2868
- const matching = data.results.filter((i) => i.label_ids.includes(labelId));
3401
+ const matching = data.results.filter((i) => labelIds.every((lid) => i.label_ids.includes(lid)));
2869
3402
  if (matching.length === 0) return null;
2870
- const sorted = [...matching].sort(
3403
+ const allStates = await fetchAll(
3404
+ `/workspaces/${workspaceSlug}/projects/${projectId}/states/`
3405
+ );
3406
+ const doneGroups = /* @__PURE__ */ new Set(["completed", "cancelled"]);
3407
+ const doneStateIds = new Set(allStates.filter((s) => doneGroups.has(s.group)).map((s) => s.id));
3408
+ const unblocked = [];
3409
+ const blocked = [];
3410
+ for (const issue3 of matching) {
3411
+ const relations = await fetchAll(
3412
+ `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issue3.id}/relations/`
3413
+ );
3414
+ const blockerIds = relations.filter((r) => r.relation_type === "blocked_by").map((r) => r.related_issue);
3415
+ const activeBlockers = [];
3416
+ for (const blockerId of blockerIds) {
3417
+ try {
3418
+ const blocker = await planeGet(
3419
+ `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${blockerId}/`
3420
+ );
3421
+ if (!doneStateIds.has(blocker.state)) {
3422
+ activeBlockers.push(blockerId);
3423
+ }
3424
+ } catch {
3425
+ activeBlockers.push(blockerId);
3426
+ }
3427
+ }
3428
+ if (activeBlockers.length === 0) {
3429
+ unblocked.push(issue3);
3430
+ } else {
3431
+ blocked.push({ id: issue3.id, name: issue3.name, blockers: activeBlockers });
3432
+ }
3433
+ }
3434
+ if (unblocked.length === 0) {
3435
+ if (blocked.length > 0) {
3436
+ warn("No unblocked issues found. Blocked issues:");
3437
+ for (const entry of blocked) {
3438
+ warn(` ${entry.name} \u2014 blocked by: ${entry.blockers.join(", ")}`);
3439
+ }
3440
+ }
3441
+ return null;
3442
+ }
3443
+ const sorted = [...unblocked].sort(
2871
3444
  (a, b) => priorityRank4(a.priority) - priorityRank4(b.priority)
2872
3445
  );
2873
3446
  const issue2 = sorted[0];
@@ -2922,11 +3495,14 @@ var PlaneSource = class {
2922
3495
  const workspaceSlug = config2.team;
2923
3496
  const projectId = await resolveProjectId(workspaceSlug, config2.project);
2924
3497
  const stateId = await resolveStateId(workspaceSlug, projectId, config2.pick_from);
2925
- const labelId = await resolveLabelId(workspaceSlug, projectId, config2.label);
3498
+ const labelNames = Array.isArray(config2.label) ? config2.label : [config2.label];
3499
+ const labelIds = await Promise.all(
3500
+ labelNames.map((name) => resolveLabelId(workspaceSlug, projectId, name))
3501
+ );
2926
3502
  const data = await planeGet(
2927
3503
  `/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${stateId}&per_page=100`
2928
3504
  );
2929
- return data.results.filter((i) => i.label_ids.includes(labelId)).map((i) => {
3505
+ return data.results.filter((i) => labelIds.every((lid) => i.label_ids.includes(lid))).map((i) => {
2930
3506
  const webUrl = `${getAppUrl()}/${workspaceSlug}/projects/${projectId}/issues/${i.id}`;
2931
3507
  return {
2932
3508
  id: makeIssueId3(workspaceSlug, projectId, i.id),
@@ -3030,15 +3606,58 @@ var ShortcutSource = class {
3030
3606
  name = "shortcut";
3031
3607
  async fetchNextIssue(config2) {
3032
3608
  const stateIds = await resolveAllWorkflowStateIds(config2.pick_from);
3033
- const labelId = await resolveLabelId2(config2.label);
3609
+ const labelNames = Array.isArray(config2.label) ? config2.label : [config2.label];
3610
+ const labelIds = await Promise.all(labelNames.map((name) => resolveLabelId2(name)));
3611
+ const workflows = await shortcutGet("/api/v3/workflows");
3612
+ const doneStateIds = /* @__PURE__ */ new Set();
3613
+ for (const workflow of workflows) {
3614
+ for (const state of workflow.states) {
3615
+ if (state.type === "done") {
3616
+ doneStateIds.add(state.id);
3617
+ }
3618
+ }
3619
+ }
3034
3620
  const searchResult = await shortcutPost("/api/v3/stories/search", {
3035
3621
  workflow_state_ids: stateIds,
3036
- label_ids: [labelId],
3622
+ label_ids: labelIds,
3037
3623
  archived: false
3038
3624
  });
3039
3625
  const stories = searchResult.data ?? [];
3040
3626
  if (stories.length === 0) return null;
3041
- const sorted = [...stories].sort((a, b) => {
3627
+ const unblocked = [];
3628
+ const blocked = [];
3629
+ for (const story2 of stories) {
3630
+ const storyLinks = story2.story_links ?? [];
3631
+ const blockerIds = storyLinks.filter((link) => link.verb === "blocks" && link.object_id === story2.id).map((link) => link.subject_id);
3632
+ const activeBlockers = [];
3633
+ for (const blockerId of blockerIds) {
3634
+ try {
3635
+ const blocker = await shortcutGet(`/api/v3/stories/${blockerId}`);
3636
+ if (!doneStateIds.has(blocker.workflow_state_id)) {
3637
+ activeBlockers.push(blockerId);
3638
+ }
3639
+ } catch {
3640
+ activeBlockers.push(blockerId);
3641
+ }
3642
+ }
3643
+ if (activeBlockers.length === 0) {
3644
+ unblocked.push(story2);
3645
+ } else {
3646
+ blocked.push({ id: story2.id, name: story2.name, blockers: activeBlockers });
3647
+ }
3648
+ }
3649
+ if (unblocked.length === 0) {
3650
+ if (blocked.length > 0) {
3651
+ warn("No unblocked issues found. Blocked issues:");
3652
+ for (const entry of blocked) {
3653
+ warn(
3654
+ ` #${entry.id} (${entry.name}) \u2014 blocked by: ${entry.blockers.map((b) => `#${b}`).join(", ")}`
3655
+ );
3656
+ }
3657
+ }
3658
+ return null;
3659
+ }
3660
+ const sorted = [...unblocked].sort((a, b) => {
3042
3661
  const pa = priorityRank5(a.priority);
3043
3662
  const pb = priorityRank5(b.priority);
3044
3663
  if (pa !== pb) return pa - pb;
@@ -3086,10 +3705,11 @@ var ShortcutSource = class {
3086
3705
  }
3087
3706
  async listIssues(config2) {
3088
3707
  const stateIds = await resolveAllWorkflowStateIds(config2.pick_from);
3089
- const labelId = await resolveLabelId2(config2.label);
3708
+ const labelNames = Array.isArray(config2.label) ? config2.label : [config2.label];
3709
+ const labelIds = await Promise.all(labelNames.map((name) => resolveLabelId2(name)));
3090
3710
  const searchResult = await shortcutPost("/api/v3/stories/search", {
3091
3711
  workflow_state_ids: stateIds,
3092
- label_ids: [labelId],
3712
+ label_ids: labelIds,
3093
3713
  archived: false
3094
3714
  });
3095
3715
  return (searchResult.data ?? []).map((story) => ({
@@ -3182,12 +3802,15 @@ var TrelloSource = class {
3182
3802
  async fetchNextIssue(config2) {
3183
3803
  const board = await findBoardByName(config2.team);
3184
3804
  const list = await findListByName(board.id, config2.pick_from);
3185
- const label = await findLabelByName(board.id, config2.label);
3805
+ const labelNames = Array.isArray(config2.label) ? config2.label : [config2.label];
3806
+ const labelIds = await Promise.all(
3807
+ labelNames.map((name) => findLabelByName(board.id, name).then((l) => l.id))
3808
+ );
3186
3809
  const cards = await trelloGet(
3187
3810
  `/lists/${list.id}/cards`,
3188
3811
  "fields=name,desc,url,idLabels,idList"
3189
3812
  );
3190
- const matching = cards.filter((c) => c.idLabels.includes(label.id));
3813
+ const matching = cards.filter((c) => labelIds.every((lid) => c.idLabels.includes(lid)));
3191
3814
  if (matching.length === 0) return null;
3192
3815
  const card = matching[0];
3193
3816
  if (!card) return null;
@@ -3232,12 +3855,15 @@ var TrelloSource = class {
3232
3855
  async listIssues(config2) {
3233
3856
  const board = await findBoardByName(config2.team);
3234
3857
  const list = await findListByName(board.id, config2.pick_from);
3235
- const label = await findLabelByName(board.id, config2.label);
3858
+ const labelNames = Array.isArray(config2.label) ? config2.label : [config2.label];
3859
+ const labelIds = await Promise.all(
3860
+ labelNames.map((name) => findLabelByName(board.id, name).then((l) => l.id))
3861
+ );
3236
3862
  const cards = await trelloGet(
3237
3863
  `/lists/${list.id}/cards`,
3238
3864
  "fields=name,desc,url,idLabels,idList"
3239
3865
  );
3240
- return cards.filter((c) => c.idLabels.includes(label.id)).map((c) => ({
3866
+ return cards.filter((c) => labelIds.every((lid) => c.idLabels.includes(lid))).map((c) => ({
3241
3867
  id: c.id,
3242
3868
  title: c.name,
3243
3869
  description: c.desc || "",
@@ -3283,12 +3909,64 @@ var activeCleanup = null;
3283
3909
  var activeProviderPid = null;
3284
3910
  var shuttingDown = false;
3285
3911
  var loopPaused = false;
3912
+ var providerPaused = false;
3913
+ var userKilled = false;
3914
+ var userSkipped = false;
3286
3915
  kanbanEmitter.on("loop:pause", () => {
3287
3916
  loopPaused = true;
3288
3917
  });
3289
3918
  kanbanEmitter.on("loop:resume", () => {
3290
3919
  loopPaused = false;
3291
3920
  });
3921
+ kanbanEmitter.on("loop:pause-provider", () => {
3922
+ if (activeProviderPid) {
3923
+ try {
3924
+ process.kill(activeProviderPid, "SIGSTOP");
3925
+ } catch {
3926
+ }
3927
+ providerPaused = true;
3928
+ kanbanEmitter.emit("provider:paused");
3929
+ }
3930
+ });
3931
+ kanbanEmitter.on("loop:resume-provider", () => {
3932
+ if (activeProviderPid && providerPaused) {
3933
+ try {
3934
+ process.kill(activeProviderPid, "SIGCONT");
3935
+ } catch {
3936
+ }
3937
+ providerPaused = false;
3938
+ kanbanEmitter.emit("provider:resumed");
3939
+ }
3940
+ });
3941
+ function killActiveProvider() {
3942
+ if (!activeProviderPid) return;
3943
+ if (providerPaused) {
3944
+ try {
3945
+ process.kill(activeProviderPid, "SIGCONT");
3946
+ } catch {
3947
+ }
3948
+ providerPaused = false;
3949
+ }
3950
+ try {
3951
+ process.kill(activeProviderPid, "SIGTERM");
3952
+ } catch {
3953
+ }
3954
+ const pid = activeProviderPid;
3955
+ setTimeout(() => {
3956
+ try {
3957
+ process.kill(pid, "SIGKILL");
3958
+ } catch {
3959
+ }
3960
+ }, 5e3);
3961
+ }
3962
+ kanbanEmitter.on("loop:kill", () => {
3963
+ userKilled = true;
3964
+ killActiveProvider();
3965
+ });
3966
+ kanbanEmitter.on("loop:skip", () => {
3967
+ userSkipped = true;
3968
+ killActiveProvider();
3969
+ });
3292
3970
  function resolveModels(config2) {
3293
3971
  if (!config2.models || config2.models.length === 0) {
3294
3972
  return [{ provider: config2.provider }];
@@ -3324,35 +4002,33 @@ function resolveModels(config2) {
3324
4002
  model: m === config2.provider ? void 0 : m
3325
4003
  }));
3326
4004
  }
3327
- var PLAN_FILE = ".lisa-plan.json";
3328
- function readLisaPlan(dir) {
3329
- const planPath = join12(dir, PLAN_FILE);
3330
- if (!existsSync6(planPath)) return null;
4005
+ function readLisaPlan(cwd) {
4006
+ const planPath = getPlanPath(cwd);
4007
+ if (!existsSync8(planPath)) return null;
3331
4008
  try {
3332
- return JSON.parse(readFileSync5(planPath, "utf-8").trim());
4009
+ return JSON.parse(readFileSync6(planPath, "utf-8").trim());
3333
4010
  } catch {
3334
4011
  return null;
3335
4012
  }
3336
4013
  }
3337
- function cleanupPlan(dir) {
4014
+ function cleanupPlan(cwd) {
3338
4015
  try {
3339
- unlinkSync9(join12(dir, PLAN_FILE));
4016
+ unlinkSync10(getPlanPath(cwd));
3340
4017
  } catch {
3341
4018
  }
3342
4019
  }
3343
- var MANIFEST_FILE = ".lisa-manifest.json";
3344
- function readLisaManifest(dir) {
3345
- const manifestPath = join12(dir, MANIFEST_FILE);
3346
- if (!existsSync6(manifestPath)) return null;
4020
+ function readLisaManifest(cwd) {
4021
+ const manifestPath = getManifestPath(cwd);
4022
+ if (!existsSync8(manifestPath)) return null;
3347
4023
  try {
3348
- return JSON.parse(readFileSync5(manifestPath, "utf-8").trim());
4024
+ return JSON.parse(readFileSync6(manifestPath, "utf-8").trim());
3349
4025
  } catch {
3350
4026
  return null;
3351
4027
  }
3352
4028
  }
3353
- function cleanupManifest(dir) {
4029
+ function cleanupManifest(cwd) {
3354
4030
  try {
3355
- unlinkSync9(join12(dir, MANIFEST_FILE));
4031
+ unlinkSync10(getManifestPath(cwd));
3356
4032
  } catch {
3357
4033
  }
3358
4034
  }
@@ -3392,7 +4068,7 @@ function installSignalHandlers() {
3392
4068
  const hasTUI = kanbanEmitter.listenerCount("tui:exit") > 0;
3393
4069
  kanbanEmitter.emit("tui:exit");
3394
4070
  if (hasTUI) {
3395
- await new Promise((resolve6) => setTimeout(resolve6, 250));
4071
+ await new Promise((resolve7) => setTimeout(resolve7, 250));
3396
4072
  }
3397
4073
  process.exit(0);
3398
4074
  };
@@ -3436,9 +4112,13 @@ async function recoverOrphanIssues(source, config2) {
3436
4112
  async function runLoop(config2, opts) {
3437
4113
  const source = createSource(config2.source);
3438
4114
  const models = resolveModels(config2);
4115
+ const workspace = resolve6(config2.workspace);
3439
4116
  installSignalHandlers();
4117
+ ensureCacheDir(workspace);
4118
+ migrateGuardrails(workspace);
4119
+ rotateLogFiles(workspace);
3440
4120
  log(
3441
- `Starting loop (models: ${models.map((m) => m.model ? `${m.provider}/${m.model}` : m.provider).join(" \u2192 ")}, source: ${config2.source}, label: ${config2.source_config.label}, workflow: ${config2.workflow})`
4121
+ `Starting loop (models: ${models.map((m) => m.model ? `${m.provider}/${m.model}` : m.provider).join(" \u2192 ")}, source: ${config2.source}, label: ${formatLabels(config2.source_config)}, workflow: ${config2.workflow})`
3442
4122
  );
3443
4123
  if (!opts.dryRun) {
3444
4124
  await recoverOrphanIssues(source, config2);
@@ -3462,14 +4142,16 @@ async function runLoop(config2, opts) {
3462
4142
  break;
3463
4143
  }
3464
4144
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
3465
- const logFile = resolve5(config2.logs.dir, `session_${session}_${timestamp2}.log`);
4145
+ const logFile = resolve6(getLogsDir(workspace), `session_${session}_${timestamp2}.log`);
3466
4146
  divider(session);
3467
4147
  await waitIfPaused();
3468
4148
  startSpinner("fetching issue...");
3469
4149
  if (opts.issueId) {
3470
4150
  log(`Fetching issue '${opts.issueId}' from ${config2.source}...`);
3471
4151
  } else {
3472
- log(`Fetching next '${config2.source_config.label}' issue from ${config2.source}...`);
4152
+ log(
4153
+ `Fetching next '${formatLabels(config2.source_config)}' issue from ${config2.source}...`
4154
+ );
3473
4155
  }
3474
4156
  if (opts.dryRun) {
3475
4157
  stopSpinner();
@@ -3503,7 +4185,7 @@ async function runLoop(config2, opts) {
3503
4185
  if (opts.issueId) {
3504
4186
  error(`Issue '${opts.issueId}' not found.`);
3505
4187
  } else {
3506
- ok(`No more issues with label '${config2.source_config.label}'. Done.`);
4188
+ ok(`No more issues with label '${formatLabels(config2.source_config)}'. Done.`);
3507
4189
  if (session === 1) {
3508
4190
  kanbanEmitter.emit("work:empty");
3509
4191
  }
@@ -3512,6 +4194,7 @@ async function runLoop(config2, opts) {
3512
4194
  }
3513
4195
  ok(`Picked up: ${issue2.id} \u2014 ${issue2.title}`);
3514
4196
  setTitle(`Lisa \u2014 ${issue2.id}`);
4197
+ kanbanEmitter.emit("issue:queued", issue2);
3515
4198
  const previousStatus = config2.source_config.pick_from;
3516
4199
  try {
3517
4200
  const inProgress = config2.source_config.in_progress;
@@ -3547,6 +4230,42 @@ async function runLoop(config2, opts) {
3547
4230
  continue;
3548
4231
  }
3549
4232
  if (!sessionResult.success) {
4233
+ if (userKilled) {
4234
+ userKilled = false;
4235
+ providerPaused = false;
4236
+ warn(`Issue ${issue2.id} killed by user.`);
4237
+ try {
4238
+ await source.updateStatus(issue2.id, previousStatus);
4239
+ ok(`Reverted ${issue2.id} to "${previousStatus}"`);
4240
+ } catch (err) {
4241
+ error(
4242
+ `Failed to revert status: ${err instanceof Error ? err.message : String(err)}`
4243
+ );
4244
+ }
4245
+ kanbanEmitter.emit("issue:killed", issue2.id);
4246
+ activeCleanup = null;
4247
+ notify();
4248
+ if (opts.once) break;
4249
+ continue;
4250
+ }
4251
+ if (userSkipped) {
4252
+ userSkipped = false;
4253
+ providerPaused = false;
4254
+ warn(`Issue ${issue2.id} skipped by user.`);
4255
+ try {
4256
+ await source.updateStatus(issue2.id, previousStatus);
4257
+ ok(`Reverted ${issue2.id} to "${previousStatus}"`);
4258
+ } catch (err) {
4259
+ error(
4260
+ `Failed to revert status: ${err instanceof Error ? err.message : String(err)}`
4261
+ );
4262
+ }
4263
+ kanbanEmitter.emit("issue:skipped", issue2.id);
4264
+ activeCleanup = null;
4265
+ notify();
4266
+ if (opts.once) break;
4267
+ continue;
4268
+ }
3550
4269
  error(`All models failed for ${issue2.id}. Reverting to "${previousStatus}".`);
3551
4270
  logAttemptHistory(sessionResult);
3552
4271
  try {
@@ -3610,7 +4329,7 @@ async function runLoop(config2, opts) {
3610
4329
  }
3611
4330
  try {
3612
4331
  const doneStatus = config2.source_config.done;
3613
- const labelToRemove = opts.issueId ? void 0 : config2.source_config.label;
4332
+ const labelToRemove = opts.issueId ? void 0 : getRemoveLabel(config2.source_config);
3614
4333
  await source.completeIssue(issue2.id, doneStatus, labelToRemove);
3615
4334
  ok(`Updated ${issue2.id} status to "${doneStatus}"`);
3616
4335
  for (const prUrl of sessionResult.prUrls) {
@@ -3652,8 +4371,8 @@ function logAttemptHistory(result) {
3652
4371
  }
3653
4372
  }
3654
4373
  function resolveBaseBranch(config2, repoPath) {
3655
- const workspace = resolve5(config2.workspace);
3656
- const repo = config2.repos.find((r) => resolve5(workspace, r.path) === repoPath);
4374
+ const workspace = resolve6(config2.workspace);
4375
+ const repo = config2.repos.find((r) => resolve6(workspace, r.path) === repoPath);
3657
4376
  return repo?.base_branch ?? config2.base_branch;
3658
4377
  }
3659
4378
  function findRepoConfig(config2, issue2) {
@@ -3689,7 +4408,7 @@ async function runWorktreeSession(config2, issue2, logFile, session, models) {
3689
4408
  if (config2.repos.length > 1) {
3690
4409
  return runWorktreeMultiRepoSession(config2, issue2, logFile, session, models);
3691
4410
  }
3692
- const workspace = resolve5(config2.workspace);
4411
+ const workspace = resolve6(config2.workspace);
3693
4412
  const repoPath = determineRepoPath(config2.repos, issue2, workspace) ?? workspace;
3694
4413
  const defaultBranch = resolveBaseBranch(config2, repoPath);
3695
4414
  const primaryProvider = createProvider(models[0]?.provider ?? "claude");
@@ -3727,21 +4446,32 @@ async function runNativeWorktreeSession(config2, issue2, logFile, session, model
3727
4446
  const testRunner = detectTestRunner(repoPath);
3728
4447
  if (testRunner) log(`Detected test runner: ${testRunner}`);
3729
4448
  const pm = detectPackageManager(repoPath);
3730
- cleanupManifest(repoPath);
3731
- const prompt = buildNativeWorktreePrompt(issue2, repoPath, testRunner, pm, _defaultBranch);
4449
+ const projectContext = analyzeProject(repoPath);
4450
+ const workspace = resolve6(config2.workspace);
4451
+ cleanupManifest(workspace);
4452
+ const prompt = buildNativeWorktreePrompt(
4453
+ issue2,
4454
+ repoPath,
4455
+ testRunner,
4456
+ pm,
4457
+ _defaultBranch,
4458
+ projectContext,
4459
+ getManifestPath(workspace)
4460
+ );
3732
4461
  initLogFile(logFile);
3733
4462
  startSpinner(`${issue2.id} \u2014 implementing (native worktree)...`);
3734
4463
  log(`Implementing with native worktree... (log: ${logFile})`);
3735
4464
  const result = await runWithFallback(models, prompt, {
3736
4465
  logFile,
3737
4466
  cwd: repoPath,
3738
- guardrailsDir: repoPath,
4467
+ guardrailsDir: workspace,
3739
4468
  issueId: issue2.id,
3740
4469
  overseer: config2.overseer,
3741
4470
  useNativeWorktree: true,
3742
4471
  onProcess: (pid) => {
3743
4472
  activeProviderPid = pid;
3744
- }
4473
+ },
4474
+ shouldAbort: () => userKilled || userSkipped
3745
4475
  });
3746
4476
  stopSpinner();
3747
4477
  try {
@@ -3759,15 +4489,13 @@ ${result.output}
3759
4489
  if (repo?.lifecycle) await stopResources();
3760
4490
  if (!result.success) {
3761
4491
  error(`Session ${session} failed for ${issue2.id}. Check ${logFile}`);
3762
- cleanupManifest(repoPath);
4492
+ cleanupManifest(workspace);
3763
4493
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
3764
4494
  }
3765
- const manifest = readLisaManifest(repoPath);
3766
- cleanupManifest(repoPath);
4495
+ const manifest = readLisaManifest(workspace);
4496
+ cleanupManifest(workspace);
3767
4497
  if (!manifest?.prUrl) {
3768
- error(
3769
- `Agent did not produce a .lisa-manifest.json with prUrl for ${issue2.id}. Aborting.`
3770
- );
4498
+ error(`Agent did not produce a manifest with prUrl for ${issue2.id}. Aborting.`);
3771
4499
  const worktreePath2 = manifest?.branch ? await findWorktreeForBranch(repoPath, manifest.branch) : null;
3772
4500
  if (worktreePath2) await cleanupWorktree(repoPath, worktreePath2);
3773
4501
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
@@ -3836,19 +4564,22 @@ async function runManualWorktreeSession(config2, issue2, logFile, session, model
3836
4564
  log(`Detected test runner: ${testRunner}`);
3837
4565
  }
3838
4566
  const pm = detectPackageManager(worktreePath);
3839
- const prompt = buildImplementPrompt(issue2, config2, testRunner, pm);
4567
+ const projectContext = analyzeProject(worktreePath);
4568
+ const workspace = resolve6(config2.workspace);
4569
+ const prompt = buildImplementPrompt(issue2, config2, testRunner, pm, projectContext);
3840
4570
  initLogFile(logFile);
3841
4571
  startSpinner(`${issue2.id} \u2014 implementing...`);
3842
4572
  log(`Implementing in worktree... (log: ${logFile})`);
3843
4573
  const result = await runWithFallback(models, prompt, {
3844
4574
  logFile,
3845
4575
  cwd: worktreePath,
3846
- guardrailsDir: repoPath,
4576
+ guardrailsDir: workspace,
3847
4577
  issueId: issue2.id,
3848
4578
  overseer: config2.overseer,
3849
4579
  onProcess: (pid) => {
3850
4580
  activeProviderPid = pid;
3851
- }
4581
+ },
4582
+ shouldAbort: () => userKilled || userSkipped
3852
4583
  });
3853
4584
  stopSpinner();
3854
4585
  try {
@@ -3871,12 +4602,10 @@ ${result.output}
3871
4602
  await cleanupWorktree(repoPath, worktreePath);
3872
4603
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
3873
4604
  }
3874
- const manifest = readLisaManifest(worktreePath);
3875
- cleanupManifest(worktreePath);
4605
+ const manifest = readLisaManifest(workspace);
4606
+ cleanupManifest(workspace);
3876
4607
  if (!manifest?.prUrl) {
3877
- error(
3878
- `Agent did not produce a .lisa-manifest.json with prUrl for ${issue2.id}. Aborting.`
3879
- );
4608
+ error(`Agent did not produce a manifest with prUrl for ${issue2.id}. Aborting.`);
3880
4609
  await cleanupWorktree(repoPath, worktreePath);
3881
4610
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
3882
4611
  }
@@ -3892,7 +4621,7 @@ ${result.output}
3892
4621
  };
3893
4622
  }
3894
4623
  async function runWorktreeMultiRepoSession(config2, issue2, logFile, session, models) {
3895
- const workspace = resolve5(config2.workspace);
4624
+ const workspace = resolve6(config2.workspace);
3896
4625
  cleanupManifest(workspace);
3897
4626
  cleanupPlan(workspace);
3898
4627
  initLogFile(logFile);
@@ -3907,7 +4636,8 @@ async function runWorktreeMultiRepoSession(config2, issue2, logFile, session, mo
3907
4636
  overseer: config2.overseer,
3908
4637
  onProcess: (pid) => {
3909
4638
  activeProviderPid = pid;
3910
- }
4639
+ },
4640
+ shouldAbort: () => userKilled || userSkipped
3911
4641
  });
3912
4642
  stopSpinner();
3913
4643
  try {
@@ -3933,7 +4663,7 @@ ${planResult.output}
3933
4663
  }
3934
4664
  const plan = readLisaPlan(workspace);
3935
4665
  if (!plan?.steps || plan.steps.length === 0) {
3936
- error(`Agent did not produce a valid .lisa-plan.json for ${issue2.id}. Aborting.`);
4666
+ error(`Agent did not produce a valid execution plan for ${issue2.id}. Aborting.`);
3937
4667
  cleanupPlan(workspace);
3938
4668
  return {
3939
4669
  success: false,
@@ -4013,7 +4743,8 @@ async function runMultiRepoStep(config2, issue2, step, previousResults, logFile,
4013
4743
  const testRunner = detectTestRunner(worktreePath);
4014
4744
  if (testRunner) log(`Detected test runner: ${testRunner}`);
4015
4745
  const pm = detectPackageManager(worktreePath);
4016
- const repoConfig = config2.repos.find((r) => resolve5(config2.workspace, r.path) === step.repoPath);
4746
+ const projectContext = analyzeProject(worktreePath);
4747
+ const repoConfig = config2.repos.find((r) => resolve6(config2.workspace, r.path) === step.repoPath);
4017
4748
  if (repoConfig?.lifecycle) {
4018
4749
  startSpinner(`${issue2.id} step ${stepNum} \u2014 starting resources...`);
4019
4750
  const started = await startResources(repoConfig, worktreePath);
@@ -4024,6 +4755,7 @@ async function runMultiRepoStep(config2, issue2, step, previousResults, logFile,
4024
4755
  return failResult(models[0]?.provider ?? "claude");
4025
4756
  }
4026
4757
  }
4758
+ const workspace = resolve6(config2.workspace);
4027
4759
  const prompt = buildScopedImplementPrompt(
4028
4760
  issue2,
4029
4761
  step,
@@ -4031,18 +4763,21 @@ async function runMultiRepoStep(config2, issue2, step, previousResults, logFile,
4031
4763
  testRunner,
4032
4764
  pm,
4033
4765
  isLastStep,
4034
- defaultBranch
4766
+ defaultBranch,
4767
+ projectContext,
4768
+ getManifestPath(workspace)
4035
4769
  );
4036
4770
  startSpinner(`${issue2.id} step ${stepNum} \u2014 implementing...`);
4037
4771
  const result = await runWithFallback(models, prompt, {
4038
4772
  logFile,
4039
4773
  cwd: worktreePath,
4040
- guardrailsDir: repoPath,
4774
+ guardrailsDir: workspace,
4041
4775
  issueId: issue2.id,
4042
4776
  overseer: config2.overseer,
4043
4777
  onProcess: (pid) => {
4044
4778
  activeProviderPid = pid;
4045
- }
4779
+ },
4780
+ shouldAbort: () => userKilled || userSkipped
4046
4781
  });
4047
4782
  stopSpinner();
4048
4783
  if (repoConfig?.lifecycle) await stopResources();
@@ -4062,10 +4797,10 @@ ${result.output}
4062
4797
  await cleanupWorktree(repoPath, worktreePath);
4063
4798
  return { ...failResult(result.providerUsed, result), branch: branchName };
4064
4799
  }
4065
- const manifest = readLisaManifest(worktreePath);
4066
- cleanupManifest(worktreePath);
4800
+ const manifest = readLisaManifest(workspace);
4801
+ cleanupManifest(workspace);
4067
4802
  if (!manifest?.prUrl) {
4068
- error(`Agent did not produce a .lisa-manifest.json with prUrl for step ${stepNum}.`);
4803
+ error(`Agent did not produce a manifest with prUrl for step ${stepNum}.`);
4069
4804
  await cleanupWorktree(repoPath, worktreePath);
4070
4805
  return { ...failResult(result.providerUsed, result), branch: branchName };
4071
4806
  }
@@ -4081,18 +4816,19 @@ ${result.output}
4081
4816
  };
4082
4817
  }
4083
4818
  async function runBranchSession(config2, issue2, logFile, session, models) {
4084
- const workspace = resolve5(config2.workspace);
4819
+ const workspace = resolve6(config2.workspace);
4085
4820
  cleanupManifest(workspace);
4086
4821
  const testRunner = detectTestRunner(workspace);
4087
4822
  if (testRunner) {
4088
4823
  log(`Detected test runner: ${testRunner}`);
4089
4824
  }
4090
4825
  const pm = detectPackageManager(workspace);
4091
- const prompt = buildImplementPrompt(issue2, config2, testRunner, pm);
4826
+ const projectContext = analyzeProject(workspace);
4827
+ const prompt = buildImplementPrompt(issue2, config2, testRunner, pm, projectContext);
4092
4828
  const repo = findRepoConfig(config2, issue2);
4093
4829
  if (repo?.lifecycle) {
4094
4830
  startSpinner(`${issue2.id} \u2014 starting resources...`);
4095
- const cwd = resolve5(workspace, repo.path);
4831
+ const cwd = resolve6(workspace, repo.path);
4096
4832
  const started = await startResources(repo, cwd);
4097
4833
  stopSpinner();
4098
4834
  if (!started) {
@@ -4122,7 +4858,8 @@ async function runBranchSession(config2, issue2, logFile, session, models) {
4122
4858
  overseer: config2.overseer,
4123
4859
  onProcess: (pid) => {
4124
4860
  activeProviderPid = pid;
4125
- }
4861
+ },
4862
+ shouldAbort: () => userKilled || userSkipped
4126
4863
  });
4127
4864
  stopSpinner();
4128
4865
  try {
@@ -4147,7 +4884,7 @@ ${result.output}
4147
4884
  const manifest = readLisaManifest(workspace);
4148
4885
  cleanupManifest(workspace);
4149
4886
  if (!manifest?.prUrl) {
4150
- error(`Agent did not produce a .lisa-manifest.json with prUrl for ${issue2.id}.`);
4887
+ error(`Agent did not produce a manifest with prUrl for ${issue2.id}.`);
4151
4888
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
4152
4889
  }
4153
4890
  ok(`PR created by provider: ${manifest.prUrl}`);
@@ -4169,7 +4906,7 @@ async function cleanupWorktree(repoRoot, worktreePath) {
4169
4906
  }
4170
4907
  }
4171
4908
  function sleep(ms) {
4172
- return new Promise((resolve6) => setTimeout(resolve6, ms));
4909
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
4173
4910
  }
4174
4911
  async function waitIfPaused() {
4175
4912
  while (loopPaused) {
@@ -4179,7 +4916,7 @@ async function waitIfPaused() {
4179
4916
 
4180
4917
  // src/cli.ts
4181
4918
  function sleep2(ms) {
4182
- return new Promise((resolve6) => setTimeout(resolve6, ms));
4919
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
4183
4920
  }
4184
4921
  var run = defineCommand({
4185
4922
  meta: { name: "run", description: "Run the agent loop" },
@@ -4232,7 +4969,7 @@ Add them to your ${shell} and run: source ${shell}`));
4232
4969
  if (isTUI) {
4233
4970
  const { render } = await import("ink");
4234
4971
  const { createElement } = await import("react");
4235
- const { KanbanApp } = await import("./kanban-PD2F4KWT.js");
4972
+ const { KanbanApp } = await import("./kanban-OGYPCVF4.js");
4236
4973
  render(createElement(KanbanApp, { config: merged }), { exitOnCtrlC: false });
4237
4974
  }
4238
4975
  await runLoop(merged, {
@@ -4301,7 +5038,7 @@ var status = defineCommand({
4301
5038
  console.log(` Provider: ${pc2.bold(config2.provider)}`);
4302
5039
  console.log(` Source: ${pc2.bold(config2.source)}`);
4303
5040
  console.log(` Workflow: ${pc2.bold(config2.workflow)}`);
4304
- console.log(` Label: ${pc2.bold(config2.source_config.label)}`);
5041
+ console.log(` Label: ${pc2.bold(formatLabels(config2.source_config))}`);
4305
5042
  console.log(` ${isLinear ? "Team" : "Board"}: ${pc2.bold(config2.source_config.team)}`);
4306
5043
  if (isLinear) {
4307
5044
  console.log(` Project: ${pc2.bold(config2.source_config.project)}`);
@@ -4309,10 +5046,10 @@ var status = defineCommand({
4309
5046
  console.log(` Pick from: ${pc2.bold(config2.source_config.pick_from)}`);
4310
5047
  console.log(` In progress: ${pc2.bold(config2.source_config.in_progress)}`);
4311
5048
  console.log(` Done: ${pc2.bold(config2.source_config.done)}`);
4312
- console.log(` Logs: ${pc2.dim(config2.logs.dir)}`);
4313
- const { readdirSync: readdirSync2, existsSync: existsSync8 } = await import("fs");
4314
- if (existsSync8(config2.logs.dir)) {
4315
- const logs = readdirSync2(config2.logs.dir).filter((f) => f.endsWith(".log"));
5049
+ const logsDir = getLogsDir(process.cwd());
5050
+ console.log(` Logs: ${pc2.dim(logsDir)}`);
5051
+ if (existsSync9(logsDir)) {
5052
+ const logs = readdirSync3(logsDir).filter((f) => f.endsWith(".log"));
4316
5053
  console.log(`
4317
5054
  ${pc2.cyan("Sessions:")} ${logs.length} log file(s) found`);
4318
5055
  } else {
@@ -4324,7 +5061,7 @@ ${pc2.dim("No sessions yet.")}`);
4324
5061
  function getVersion() {
4325
5062
  try {
4326
5063
  const pkgPath = resolvePath(new URL(".", import.meta.url).pathname, "../package.json");
4327
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
5064
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
4328
5065
  return pkg.version;
4329
5066
  } catch {
4330
5067
  return "0.0.0";
@@ -4332,9 +5069,9 @@ function getVersion() {
4332
5069
  }
4333
5070
  var CURSOR_FREE_PLAN_ERROR = "Free plans can only use Auto";
4334
5071
  async function isCursorFreePlan() {
4335
- const { mkdtempSync: mkdtempSync9, unlinkSync: unlinkSync10, writeFileSync: writeFileSync12 } = await import("fs");
4336
- const tmpDir = mkdtempSync9(join13(tmpdir9(), "lisa-cursor-check-"));
4337
- const promptFile = join13(tmpDir, "prompt.txt");
5072
+ const { mkdtempSync: mkdtempSync9, unlinkSync: unlinkSync11, writeFileSync: writeFileSync12 } = await import("fs");
5073
+ const tmpDir = mkdtempSync9(join14(tmpdir9(), "lisa-cursor-check-"));
5074
+ const promptFile = join14(tmpDir, "prompt.txt");
4338
5075
  writeFileSync12(promptFile, "test", "utf-8");
4339
5076
  try {
4340
5077
  const bin = ["agent", "cursor-agent"].find((b) => {
@@ -4357,7 +5094,7 @@ async function isCursorFreePlan() {
4357
5094
  return errorOutput.includes(CURSOR_FREE_PLAN_ERROR);
4358
5095
  } finally {
4359
5096
  try {
4360
- unlinkSync10(promptFile);
5097
+ unlinkSync11(promptFile);
4361
5098
  } catch {
4362
5099
  }
4363
5100
  try {
@@ -4411,7 +5148,11 @@ var issueDone = defineCommand({
4411
5148
  const source = createSource(config2.source);
4412
5149
  try {
4413
5150
  await source.attachPullRequest(args.id, args["pr-url"]);
4414
- await source.completeIssue(args.id, config2.source_config.done, config2.source_config.label);
5151
+ await source.completeIssue(
5152
+ args.id,
5153
+ config2.source_config.done,
5154
+ getRemoveLabel(config2.source_config)
5155
+ );
4415
5156
  console.log(JSON.stringify({ success: true, issueId: args.id, prUrl: args["pr-url"] }));
4416
5157
  } catch (err) {
4417
5158
  console.error(
@@ -4660,13 +5401,25 @@ Then reload: ${pc2.cyan(`source ${shell}`)}`
4660
5401
  });
4661
5402
  if (clack.isCancel(teamAnswer)) return process.exit(0);
4662
5403
  const team = teamAnswer;
5404
+ const existingLabelStr = existing ? Array.isArray(existing.source_config.label) ? existing.source_config.label.join(", ") : existing.source_config.label : "";
4663
5405
  const labelAnswer = await clack.text({
4664
- message: "Which label marks issues as ready for the agent to pick up?",
4665
- initialValue: existing?.source_config.label ?? "ready",
4666
- placeholder: "e.g. ready, ai, lisa"
5406
+ message: "Which label(s) mark issues as ready? (comma-separated for multiple, e.g. ready,api)",
5407
+ initialValue: existingLabelStr || "ready",
5408
+ placeholder: "e.g. ready or ready, api"
4667
5409
  });
4668
5410
  if (clack.isCancel(labelAnswer)) return process.exit(0);
4669
- const label = labelAnswer;
5411
+ const labelParts = labelAnswer.split(",").map((s) => s.trim()).filter(Boolean);
5412
+ const label = labelParts.length === 1 ? labelParts[0] : labelParts;
5413
+ let removeLabel;
5414
+ if (Array.isArray(label) && label.length > 1) {
5415
+ const removeLabelAnswer = await clack.text({
5416
+ message: "Which label should be removed when an issue is completed? (required for multi-label)",
5417
+ initialValue: existing?.source_config.remove_label ?? label[0] ?? "",
5418
+ placeholder: `e.g. ${label[0]}`
5419
+ });
5420
+ if (clack.isCancel(removeLabelAnswer)) return process.exit(0);
5421
+ removeLabel = removeLabelAnswer || void 0;
5422
+ }
4670
5423
  let project;
4671
5424
  let pickFrom;
4672
5425
  let inProgress;
@@ -4778,6 +5531,7 @@ Then reload: ${pc2.cyan(`source ${shell}`)}`
4778
5531
  team,
4779
5532
  project,
4780
5533
  label,
5534
+ ...removeLabel ? { remove_label: removeLabel } : {},
4781
5535
  pick_from: pickFrom,
4782
5536
  in_progress: inProgress,
4783
5537
  done
@@ -4787,13 +5541,9 @@ Then reload: ${pc2.cyan(`source ${shell}`)}`
4787
5541
  workspace: ".",
4788
5542
  base_branch: baseBranch,
4789
5543
  repos,
4790
- loop: { cooldown: 10, max_sessions: 0 },
4791
- logs: { dir: ".lisa/logs", format: "text" }
5544
+ loop: { cooldown: 10, max_sessions: 0 }
4792
5545
  };
4793
5546
  saveConfig(cfg);
4794
- if (ensureLogsGitignore(process.cwd())) {
4795
- clack.log.info("Added .lisa/logs/* to .gitignore");
4796
- }
4797
5547
  clack.outro(
4798
5548
  `${pc2.green("All set!")} Config saved to ${pc2.cyan(".lisa/config.yaml")}
4799
5549
  Run ${pc2.bold(pc2.cyan("lisa run"))} to start resolving issues.`
@@ -4825,12 +5575,12 @@ async function detectGitHubMethod() {
4825
5575
  }
4826
5576
  async function detectGitRepos() {
4827
5577
  const cwd = process.cwd();
4828
- if (existsSync7(join13(cwd, ".git"))) {
5578
+ if (existsSync9(join14(cwd, ".git"))) {
4829
5579
  clack.log.info("Found a git repository in the current directory.");
4830
5580
  return [];
4831
5581
  }
4832
- const entries = readdirSync(cwd, { withFileTypes: true });
4833
- const gitDirs = entries.filter((e) => e.isDirectory() && existsSync7(join13(cwd, e.name, ".git"))).map((e) => e.name);
5582
+ const entries = readdirSync3(cwd, { withFileTypes: true });
5583
+ const gitDirs = entries.filter((e) => e.isDirectory() && existsSync9(join14(cwd, e.name, ".git"))).map((e) => e.name);
4834
5584
  if (gitDirs.length === 0) {
4835
5585
  return [];
4836
5586
  }
@@ -4840,7 +5590,7 @@ async function detectGitRepos() {
4840
5590
  });
4841
5591
  if (clack.isCancel(selected)) return process.exit(0);
4842
5592
  return selected.map((dir) => ({
4843
- name: getGitRepoName(join13(cwd, dir)) ?? dir,
5593
+ name: getGitRepoName(join14(cwd, dir)) ?? dir,
4844
5594
  path: `./${dir}`,
4845
5595
  match: "",
4846
5596
  base_branch: ""