@tarcisiopgs/lisa 1.4.0 → 1.6.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
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ kanbanEmitter
4
+ } from "./chunk-KAME5MG7.js";
2
5
 
3
6
  // src/cli.ts
4
- import { execSync as execSync6 } from "child_process";
7
+ import { execSync as execSync8 } from "child_process";
5
8
  import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6 } from "fs";
6
- import { join as join10, resolve as resolvePath } from "path";
9
+ import { tmpdir as tmpdir8 } from "os";
10
+ import { join as join12, resolve as resolvePath } from "path";
7
11
  import * as clack from "@clack/prompts";
8
12
  import { defineCommand, runMain } from "citty";
9
13
  import pc2 from "picocolors";
@@ -51,6 +55,15 @@ function getConfigPath(cwd = process.cwd()) {
51
55
  function configExists(cwd = process.cwd()) {
52
56
  return existsSync(getConfigPath(cwd));
53
57
  }
58
+ function findConfigDir(startDir = process.cwd()) {
59
+ let dir = startDir;
60
+ while (true) {
61
+ if (existsSync(getConfigPath(dir))) return dir;
62
+ const parent = resolve(dir, "..");
63
+ if (parent === dir) return null;
64
+ dir = parent;
65
+ }
66
+ }
54
67
  function loadConfig(cwd = process.cwd()) {
55
68
  const configPath = getConfigPath(cwd);
56
69
  if (!existsSync(configPath)) {
@@ -70,6 +83,18 @@ function loadConfig(cwd = process.cwd()) {
70
83
  if (parsed.source === "trello" && !sourceConfig.pick_from) {
71
84
  sourceConfig.pick_from = sourceConfig.project;
72
85
  }
86
+ if (parsed.source === "plane" && !sourceConfig.team && process.env.PLANE_WORKSPACE) {
87
+ sourceConfig.team = process.env.PLANE_WORKSPACE;
88
+ }
89
+ if (parsed.source === "gitlab-issues" && !sourceConfig.team && rawSource.project) {
90
+ sourceConfig.team = rawSource.project;
91
+ }
92
+ if (parsed.source === "github-issues" && !sourceConfig.team && rawSource.project) {
93
+ sourceConfig.team = rawSource.project;
94
+ }
95
+ if (parsed.source === "jira" && !sourceConfig.team && rawSource.project) {
96
+ sourceConfig.team = rawSource.project;
97
+ }
73
98
  const config2 = {
74
99
  ...DEFAULT_CONFIG,
75
100
  ...parsed,
@@ -103,6 +128,22 @@ function saveConfig(config2, cwd = process.cwd()) {
103
128
  label: sc.label,
104
129
  in_progress: sc.in_progress,
105
130
  done: sc.done
131
+ } : config2.source === "gitlab-issues" ? {
132
+ team: sc.team,
133
+ label: sc.label,
134
+ in_progress: sc.in_progress,
135
+ done: sc.done
136
+ } : config2.source === "github-issues" ? {
137
+ team: sc.team,
138
+ label: sc.label,
139
+ in_progress: sc.in_progress,
140
+ done: sc.done
141
+ } : config2.source === "jira" ? {
142
+ team: sc.team,
143
+ label: sc.label,
144
+ pick_from: sc.pick_from,
145
+ in_progress: sc.in_progress,
146
+ done: sc.done
106
147
  } : {
107
148
  team: sc.team,
108
149
  project: sc.project,
@@ -123,10 +164,8 @@ function mergeWithFlags(config2, flags) {
123
164
  return merged;
124
165
  }
125
166
 
126
- // src/github.ts
167
+ // src/git/github.ts
127
168
  import { execa } from "execa";
128
- var API_URL = "https://api.github.com";
129
- var REQUEST_TIMEOUT_MS = 3e4;
130
169
  async function isGhCliAvailable() {
131
170
  try {
132
171
  await execa("gh", ["auth", "status"]);
@@ -135,91 +174,87 @@ async function isGhCliAvailable() {
135
174
  return false;
136
175
  }
137
176
  }
138
- function getToken() {
139
- const token = process.env.GITHUB_TOKEN;
140
- if (!token) throw new Error("GITHUB_TOKEN is not set");
141
- return token;
177
+
178
+ // src/git/worktree.ts
179
+ import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
180
+ import { join, resolve as resolve2 } from "path";
181
+ import { execa as execa2 } from "execa";
182
+ var WORKTREES_DIR = ".worktrees";
183
+ function generateBranchName(issueId, title) {
184
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").substring(0, 40).replace(/^-|-$/g, "");
185
+ return `feat/${issueId.toLowerCase()}-${slug}`;
142
186
  }
143
- async function createPullRequest(opts, method = "cli") {
144
- if (method === "cli" && await isGhCliAvailable()) {
145
- return createPullRequestWithGhCli(opts);
187
+ async function cleanupOrphanedWorktree(repoRoot, branchName) {
188
+ const { stdout: branchList } = await execa2("git", ["branch", "--list", branchName], {
189
+ cwd: repoRoot,
190
+ reject: false
191
+ });
192
+ if (!branchList.trim()) {
193
+ return false;
146
194
  }
147
- const res = await fetch(`${API_URL}/repos/${opts.owner}/${opts.repo}/pulls`, {
148
- method: "POST",
149
- headers: {
150
- Authorization: `Bearer ${getToken()}`,
151
- Accept: "application/vnd.github+json",
152
- "Content-Type": "application/json"
153
- },
154
- body: JSON.stringify({
155
- title: opts.title,
156
- body: opts.body,
157
- head: opts.head,
158
- base: opts.base
159
- }),
160
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
195
+ const worktreePath = join(repoRoot, WORKTREES_DIR, branchName);
196
+ const { stdout: worktreeList } = await execa2("git", ["worktree", "list", "--porcelain"], {
197
+ cwd: repoRoot,
198
+ reject: false
161
199
  });
162
- if (!res.ok) {
163
- const text2 = await res.text();
164
- throw new Error(`GitHub API error (${res.status}): ${text2}`);
200
+ if (worktreeList.includes(worktreePath)) {
201
+ await execa2("git", ["worktree", "remove", worktreePath, "--force"], { cwd: repoRoot });
202
+ await execa2("git", ["worktree", "prune"], { cwd: repoRoot });
165
203
  }
166
- const data = await res.json();
167
- return { number: data.number, html_url: data.html_url };
168
- }
169
- async function createPullRequestWithGhCli(opts) {
170
- const result = await execa("gh", [
171
- "pr",
172
- "create",
173
- "--repo",
174
- `${opts.owner}/${opts.repo}`,
175
- "--head",
176
- opts.head,
177
- "--base",
178
- opts.base,
179
- "--title",
180
- opts.title,
181
- "--body",
182
- opts.body
183
- ]);
184
- const url = result.stdout.trim();
185
- const prNumberMatch = url.match(/\/pull\/(\d+)/);
186
- const number = prNumberMatch ? Number.parseInt(prNumberMatch[1] ?? "0", 10) : 0;
187
- return { number, html_url: url };
188
- }
189
- async function getRepoInfo(cwd) {
190
- const { stdout: remoteUrl } = await execa("git", ["remote", "get-url", "origin"], { cwd });
191
- let owner;
192
- let repo;
193
- const sshMatch = remoteUrl.match(/git@github\.com:(.+?)\/(.+?)(?:\.git)?$/);
194
- const httpsMatch = remoteUrl.match(/github\.com\/(.+?)\/(.+?)(?:\.git)?$/);
195
- if (sshMatch) {
196
- owner = sshMatch[1] ?? "";
197
- repo = sshMatch[2] ?? "";
198
- } else if (httpsMatch) {
199
- owner = httpsMatch[1] ?? "";
200
- repo = httpsMatch[2] ?? "";
201
- } else {
202
- throw new Error(`Cannot parse GitHub owner/repo from remote URL: ${remoteUrl}`);
203
- }
204
- const { stdout: branch } = await execa("git", ["branch", "--show-current"], { cwd });
205
- const { stdout: defaultBranch } = await execa(
206
- "git",
207
- ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"],
208
- { cwd, reject: false }
209
- ).then(
210
- (r) => r,
211
- () => ({ stdout: "origin/main" })
212
- );
213
- return {
214
- owner,
215
- repo,
216
- branch: branch.trim(),
217
- defaultBranch: defaultBranch.replace("origin/", "").trim()
218
- };
204
+ await execa2("git", ["branch", "-D", branchName], { cwd: repoRoot });
205
+ return true;
206
+ }
207
+ async function createWorktree(repoRoot, branchName, baseBranch) {
208
+ const worktreePath = join(repoRoot, WORKTREES_DIR, branchName);
209
+ await cleanupOrphanedWorktree(repoRoot, branchName);
210
+ await execa2("git", ["fetch", "origin", baseBranch], { cwd: repoRoot });
211
+ await execa2("git", ["worktree", "add", "-b", branchName, worktreePath, `origin/${baseBranch}`], {
212
+ cwd: repoRoot
213
+ });
214
+ return worktreePath;
215
+ }
216
+ async function removeWorktree(repoRoot, worktreePath) {
217
+ await execa2("git", ["worktree", "remove", worktreePath, "--force"], {
218
+ cwd: repoRoot
219
+ });
220
+ await execa2("git", ["worktree", "prune"], { cwd: repoRoot });
221
+ }
222
+ function ensureWorktreeGitignore(repoRoot) {
223
+ const gitignorePath = join(repoRoot, ".gitignore");
224
+ if (!existsSync2(gitignorePath)) {
225
+ appendFileSync(gitignorePath, `${WORKTREES_DIR}
226
+ `);
227
+ return;
228
+ }
229
+ const content = readFileSync2(gitignorePath, "utf-8");
230
+ if (!content.split("\n").some((line) => line.trim() === WORKTREES_DIR)) {
231
+ const separator = content.endsWith("\n") ? "" : "\n";
232
+ appendFileSync(gitignorePath, `${separator}${WORKTREES_DIR}
233
+ `);
234
+ }
235
+ }
236
+ function determineRepoPath(repos, issue2, workspace) {
237
+ if (repos.length === 0) return void 0;
238
+ if (issue2.repo) {
239
+ const match = repos.find((r) => r.name === issue2.repo);
240
+ if (match) return join(workspace, match.path);
241
+ }
242
+ for (const r of repos) {
243
+ if (r.match && issue2.title.startsWith(r.match)) {
244
+ return join(workspace, r.path);
245
+ }
246
+ }
247
+ const first = repos[0];
248
+ return first ? join(workspace, first.path) : void 0;
219
249
  }
220
250
 
221
- // src/logger.ts
222
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
251
+ // src/loop.ts
252
+ import { appendFileSync as appendFileSync10, existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync8 } from "fs";
253
+ import { join as join11, resolve as resolve5 } from "path";
254
+ import { execa as execa3 } from "execa";
255
+
256
+ // src/output/logger.ts
257
+ import { appendFileSync as appendFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
223
258
  import { dirname } from "path";
224
259
  import pc from "picocolors";
225
260
  var logFilePath = null;
@@ -228,9 +263,15 @@ var jsonEvents = [];
228
263
  function setOutputMode(mode) {
229
264
  outputMode = mode;
230
265
  }
266
+ function getOutputMode() {
267
+ return outputMode;
268
+ }
269
+ function shouldPrintToConsole() {
270
+ return outputMode !== "quiet" && outputMode !== "tui";
271
+ }
231
272
  function initLogFile(path) {
232
273
  const dir = dirname(path);
233
- if (!existsSync2(dir)) {
274
+ if (!existsSync3(dir)) {
234
275
  mkdirSync2(dir, { recursive: true });
235
276
  }
236
277
  writeFileSync2(path, `[${timestamp()}] Log started
@@ -242,7 +283,7 @@ function timestamp() {
242
283
  }
243
284
  function writeToFile(level, message) {
244
285
  if (logFilePath) {
245
- appendFileSync(logFilePath, `[${timestamp()}] [${level}] ${message}
286
+ appendFileSync2(logFilePath, `[${timestamp()}] [${level}] ${message}
246
287
  `);
247
288
  }
248
289
  }
@@ -256,7 +297,7 @@ function log(message) {
256
297
  emitJson("info", message);
257
298
  return;
258
299
  }
259
- if (outputMode !== "quiet") {
300
+ if (shouldPrintToConsole()) {
260
301
  console.log(`${pc.cyan("[lisa]")} ${pc.dim(timestamp())} ${message}`);
261
302
  }
262
303
  writeToFile("info", message);
@@ -266,7 +307,7 @@ function warn(message) {
266
307
  emitJson("warn", message);
267
308
  return;
268
309
  }
269
- if (outputMode !== "quiet") {
310
+ if (shouldPrintToConsole()) {
270
311
  console.error(`${pc.yellow("[lisa]")} ${pc.dim(timestamp())} ${message}`);
271
312
  }
272
313
  writeToFile("warn", message);
@@ -284,7 +325,7 @@ function ok(message) {
284
325
  emitJson("ok", message);
285
326
  return;
286
327
  }
287
- if (outputMode !== "quiet") {
328
+ if (shouldPrintToConsole()) {
288
329
  console.log(`${pc.green("[lisa]")} ${pc.dim(timestamp())} ${message}`);
289
330
  }
290
331
  writeToFile("ok", message);
@@ -330,197 +371,64 @@ function banner() {
330
371
  `));
331
372
  }
332
373
 
333
- // src/loop.ts
334
- import { appendFileSync as appendFileSync8, existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync6 } from "fs";
335
- import { join as join9, resolve as resolve5 } from "path";
336
- import { execa as execa3 } from "execa";
337
-
338
- // src/lifecycle.ts
339
- import { spawn } from "child_process";
340
- import { createConnection } from "net";
341
- import { resolve as resolve2 } from "path";
342
- var managedResources = [];
343
- var cleanupRegistered = false;
344
- function isPortInUse(port) {
345
- return new Promise((resolve6) => {
346
- const socket = createConnection({ port }, () => {
347
- socket.destroy();
348
- resolve6(true);
349
- });
350
- socket.on("error", () => {
351
- socket.destroy();
352
- resolve6(false);
353
- });
354
- });
374
+ // src/output/terminal.ts
375
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
376
+ var SPINNER_INTERVAL_MS = 80;
377
+ var spinnerTimer = null;
378
+ var spinnerFrame = 0;
379
+ function isTTY() {
380
+ return process.stdout.isTTY === true;
355
381
  }
356
- function waitForPort(port, timeoutMs) {
357
- return new Promise((resolve6) => {
358
- const deadline = Date.now() + timeoutMs;
359
- const check = () => {
360
- if (Date.now() > deadline) {
361
- resolve6(false);
362
- return;
363
- }
364
- isPortInUse(port).then((inUse) => {
365
- if (inUse) {
366
- resolve6(true);
367
- } else {
368
- setTimeout(check, 500);
369
- }
370
- });
371
- };
372
- check();
373
- });
382
+ function writeOSC(title) {
383
+ process.stdout.write(`\x1B]0;${title}\x07`);
374
384
  }
375
- function spawnResource(config2, baseCwd) {
376
- const cwd = config2.cwd ? resolve2(baseCwd, config2.cwd) : baseCwd;
377
- const child = spawn("sh", ["-c", config2.up], {
378
- cwd,
379
- stdio: "ignore",
380
- detached: true
381
- });
382
- child.unref();
383
- return child;
385
+ function setTitle(title) {
386
+ if (!isTTY()) return;
387
+ writeOSC(title);
384
388
  }
385
- function runSetupCommand(command, cwd) {
386
- return new Promise((resolve6, reject) => {
387
- const child = spawn("sh", ["-c", command], {
388
- cwd,
389
- stdio: "inherit"
390
- });
391
- child.on("close", (code) => {
392
- if (code === 0) {
393
- resolve6();
394
- } else {
395
- reject(new Error(`Setup command failed with exit code ${code}: ${command}`));
396
- }
397
- });
398
- child.on("error", (err) => {
399
- reject(new Error(`Setup command error: ${err.message}`));
400
- });
401
- });
389
+ function startSpinner(message) {
390
+ if (!isTTY()) return;
391
+ stopSpinner();
392
+ spinnerFrame = 0;
393
+ writeOSC(`${SPINNER_FRAMES[0]} Lisa \u2014 ${message}`);
394
+ spinnerTimer = setInterval(() => {
395
+ spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
396
+ writeOSC(`${SPINNER_FRAMES[spinnerFrame]} Lisa \u2014 ${message}`);
397
+ }, SPINNER_INTERVAL_MS);
402
398
  }
403
- async function startResources(repo, baseCwd) {
404
- const lifecycle = repo.lifecycle;
405
- if (!lifecycle) return true;
406
- registerCleanup();
407
- for (const resource of lifecycle.resources) {
408
- const alreadyRunning = await isPortInUse(resource.check_port);
409
- if (alreadyRunning) {
410
- ok(`Resource "${resource.name}" already running on port ${resource.check_port}`);
411
- continue;
412
- }
413
- log(`Starting resource "${resource.name}" on port ${resource.check_port}...`);
414
- const child = spawnResource(resource, baseCwd);
415
- managedResources.push({
416
- name: resource.name,
417
- config: resource,
418
- process: child
419
- });
420
- const timeoutMs = (resource.startup_timeout || 30) * 1e3;
421
- const ready = await waitForPort(resource.check_port, timeoutMs);
422
- if (!ready) {
423
- error(
424
- `Resource "${resource.name}" failed to start within ${resource.startup_timeout}s`
425
- );
426
- await stopResources();
427
- return false;
428
- }
429
- ok(`Resource "${resource.name}" is ready on port ${resource.check_port}`);
399
+ function stopSpinner(message) {
400
+ if (spinnerTimer) {
401
+ clearInterval(spinnerTimer);
402
+ spinnerTimer = null;
430
403
  }
431
- for (const command of lifecycle.setup) {
432
- log(`Running setup: ${command}`);
433
- try {
434
- await runSetupCommand(command, baseCwd);
435
- ok(`Setup complete: ${command}`);
436
- } catch (err) {
437
- error(`Setup failed: ${err instanceof Error ? err.message : String(err)}`);
438
- await stopResources();
439
- return false;
440
- }
404
+ if (!isTTY()) return;
405
+ if (message) {
406
+ writeOSC(message);
441
407
  }
442
- return true;
443
408
  }
444
- async function stopResources() {
445
- for (const managed of managedResources) {
446
- const { name, config: config2, process: child } = managed;
447
- log(`Stopping resource "${name}"...`);
448
- try {
449
- if (config2.down === "auto") {
450
- if (child?.pid) {
451
- try {
452
- process.kill(-child.pid, "SIGTERM");
453
- } catch {
454
- }
455
- }
456
- } else {
457
- await new Promise((resolve6) => {
458
- const down = spawn("sh", ["-c", config2.down], {
459
- stdio: "ignore"
460
- });
461
- down.on("close", () => resolve6());
462
- down.on("error", () => resolve6());
463
- });
464
- }
465
- ok(`Resource "${name}" stopped`);
466
- } catch (err) {
467
- warn(
468
- `Failed to stop resource "${name}": ${err instanceof Error ? err.message : String(err)}`
469
- );
470
- }
471
- }
472
- managedResources.length = 0;
409
+ function notify() {
410
+ if (!isTTY()) return;
411
+ process.stdout.write("\x07");
473
412
  }
474
- function registerCleanup() {
475
- if (cleanupRegistered) return;
476
- cleanupRegistered = true;
477
- const cleanup = () => {
478
- for (const managed of managedResources) {
479
- const { config: config2, process: child } = managed;
480
- try {
481
- if (config2.down === "auto") {
482
- if (child?.pid) {
483
- process.kill(-child.pid, "SIGTERM");
484
- }
485
- }
486
- } catch {
487
- }
488
- }
489
- };
490
- process.on("exit", cleanup);
491
- process.on("SIGINT", () => {
492
- cleanup();
493
- process.exit(130);
494
- });
495
- process.on("SIGTERM", () => {
496
- cleanup();
497
- process.exit(143);
498
- });
499
- }
500
-
501
- // src/pr-body.ts
502
- function sanitizePrBody(raw) {
503
- let text2 = raw.trim();
504
- if (!text2) return "";
505
- text2 = text2.replace(/<[^>]*>/g, "");
506
- text2 = text2.replace(/^(\s*)\* /gm, "$1- ");
507
- if (!text2.includes("\n")) {
508
- const sentences = text2.match(/[^.!?]+[.!?]+/g);
509
- if (sentences && sentences.length > 1) {
510
- text2 = sentences.map((s) => `- ${s.trim()}`).join("\n");
511
- }
512
- }
513
- return text2.trim();
413
+ function resetTitle() {
414
+ if (!isTTY()) return;
415
+ writeOSC("");
514
416
  }
515
417
 
516
418
  // src/prompt.ts
517
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
518
- import { join, resolve as resolve3 } from "path";
419
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
420
+ import { join as join2, resolve as resolve3 } from "path";
421
+ function detectPackageManager(cwd) {
422
+ if (existsSync4(join2(cwd, "bun.lockb")) || existsSync4(join2(cwd, "bun.lock"))) return "bun";
423
+ if (existsSync4(join2(cwd, "pnpm-lock.yaml"))) return "pnpm";
424
+ if (existsSync4(join2(cwd, "yarn.lock"))) return "yarn";
425
+ return "npm";
426
+ }
519
427
  function detectTestRunner(cwd) {
520
- const packageJsonPath = join(cwd, "package.json");
521
- if (!existsSync3(packageJsonPath)) return null;
428
+ const packageJsonPath = join2(cwd, "package.json");
429
+ if (!existsSync4(packageJsonPath)) return null;
522
430
  try {
523
- const content = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
431
+ const content = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
524
432
  const deps = { ...content.dependencies, ...content.devDependencies };
525
433
  if ("vitest" in deps) return "vitest";
526
434
  if ("jest" in deps) return "jest";
@@ -529,20 +437,21 @@ function detectTestRunner(cwd) {
529
437
  return null;
530
438
  }
531
439
  }
532
- function buildImplementPrompt(issue, config2, testRunner) {
440
+ function buildImplementPrompt(issue2, config2, testRunner, pm) {
533
441
  if (config2.workflow === "worktree") {
534
- return buildWorktreePrompt(issue, testRunner);
442
+ return buildWorktreePrompt(issue2, testRunner, pm, config2.base_branch);
535
443
  }
536
- return buildBranchPrompt(issue, config2, testRunner);
444
+ return buildBranchPrompt(issue2, config2, testRunner, pm);
537
445
  }
538
- function buildTestInstructions(testRunner) {
446
+ function buildTestInstructions(testRunner, pm = "npm") {
539
447
  if (!testRunner) return "";
448
+ const testCmd = pm === "bun" ? "bun run test" : `${pm} run test`;
540
449
  return `
541
450
  **MANDATORY \u2014 Unit Tests:**
542
451
  This project uses **${testRunner}** as its test runner.
543
452
  - You MUST write unit tests (\`*.test.ts\`) for every new file or module you create.
544
453
  - Tests should cover the main functionality, edge cases, and error scenarios.
545
- - Run \`npm run test\` and ensure ALL tests pass before committing.
454
+ - Run \`${testCmd}\` and ensure ALL tests pass before committing.
546
455
  - Do NOT skip writing tests \u2014 the PR will be blocked if tests are missing or failing.
547
456
  `;
548
457
  }
@@ -579,37 +488,24 @@ Do NOT update README.md for:
579
488
  If an update is needed, keep the existing README style and structure. Include the README change in the same commit as the implementation.
580
489
  `;
581
490
  }
582
- function buildPrBodyInstructions() {
583
- return `The \`prBody\` MUST follow this exact markdown structure:
584
- \`\`\`
585
- - **What**: one-line summary of the change
586
- - **Why**: motivation or issue context
587
- - **Key changes**:
588
- - \`src/foo.ts\` \u2014 added X functionality
589
- - \`src/bar.ts\` \u2014 refactored Y to support Z
590
- - **Testing**: what was validated (e.g. "all unit tests pass", "manually tested endpoint")
591
- \`\`\`
592
- Write in English. Do NOT write a wall of text \u2014 structure the summary using the template above.`;
593
- }
594
- function buildWorktreePrompt(issue, testRunner) {
595
- const testBlock = buildTestInstructions(testRunner ?? null);
491
+ function buildWorktreePrompt(issue2, testRunner, pm, baseBranch) {
492
+ const testBlock = buildTestInstructions(testRunner ?? null, pm);
596
493
  const readmeBlock = buildReadmeInstructions();
597
494
  const hookBlock = buildPreCommitHookInstructions();
598
- return `You are an autonomous implementation agent. Your job is to implement a single
599
- issue, validate it, commit, and push the branch.
495
+ return `You are an autonomous implementation agent. Your job is to implement an issue end-to-end: code, push, PR, and tracker update.
600
496
 
601
497
  You are already inside the correct repository worktree on the correct branch.
602
498
  Do NOT create a new branch \u2014 just work on the current one.
603
499
 
604
500
  ## Issue
605
501
 
606
- - **ID:** ${issue.id}
607
- - **Title:** ${issue.title}
608
- - **URL:** ${issue.url}
502
+ - **ID:** ${issue2.id}
503
+ - **Title:** ${issue2.title}
504
+ - **URL:** ${issue2.url}
609
505
 
610
506
  ### Description
611
507
 
612
- ${issue.description}
508
+ ${issue2.description}
613
509
 
614
510
  ## Instructions
615
511
 
@@ -625,55 +521,62 @@ ${testBlock}${readmeBlock}${hookBlock}
625
521
  - Fix any errors before proceeding.
626
522
 
627
523
  3. **Commit**: Make atomic commits with conventional commit messages.
628
- **Branch name must be in English.** The branch was pre-created with an auto-generated name.
629
- If that name contains non-English words, rename it before committing:
630
- \`git branch -m <current-name> feat/${issue.id.toLowerCase()}-short-english-slug\`
631
- Do NOT push \u2014 the caller handles pushing.
524
+ **Branch name must be in English.** If the current branch name contains non-English words,
525
+ rename it before committing using the single-argument form:
526
+ \`git branch -m feat/${issue2.id.toLowerCase()}-short-english-slug\`
632
527
  **IMPORTANT \u2014 Language rules:**
633
528
  - All commit messages MUST be in English.
634
529
  - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
635
530
 
636
- 4. **Write manifest**: Create \`.lisa-manifest.json\` in the **current directory** with JSON:
531
+ 4. **Push**: Push the branch to origin:
532
+ \`git push -u origin <branch-name>\`
533
+ If the push fails due to a pre-push hook, read the error, fix the root cause, amend the commit, and retry. Do NOT use \`--no-verify\`.
534
+
535
+ 5. **Create PR**: Create a pull request using the GitHub CLI:
536
+ \`gh pr create --title "<conventional-commit-title>" --body "<markdown-summary>"${baseBranch ? ` --base ${baseBranch}` : ""}\`
537
+ Capture the PR URL from the output.
538
+
539
+ 6. **Update tracker**: Call the lisa CLI to mark the issue as done:
540
+ \`lisa issue done ${issue2.id} --pr-url <pr-url>\`
541
+ Wait 1 second before calling this command.
542
+
543
+ 7. **Write manifest**: Create \`.lisa-manifest.json\` in the **current directory** with JSON:
637
544
  \`\`\`json
638
- {"branch": "<final English branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<markdown-formatted English summary>"}
545
+ {"branch": "<final English branch name>", "prUrl": "<pull request URL>"}
639
546
  \`\`\`
640
- ${buildPrBodyInstructions()}
641
547
  Do NOT commit this file.
642
548
 
643
549
  ## Rules
644
550
 
645
551
  - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
646
552
  - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
647
- - Do NOT push \u2014 the caller handles that.
648
553
  - Do NOT install new dependencies unless the issue explicitly requires it.
649
554
  - If you get stuck or the issue is unclear, STOP and explain why.
650
555
  - One issue only. Do not pick up additional issues.
651
- - If the repo has a CLAUDE.md, read it first and follow its conventions.
652
- - Do NOT create pull requests \u2014 the caller handles that.
653
- - Do NOT update the issue tracker \u2014 the caller handles that.`;
556
+ - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
654
557
  }
655
- function buildBranchPrompt(issue, config2, testRunner) {
558
+ function buildBranchPrompt(issue2, config2, testRunner, pm) {
656
559
  const workspace = resolve3(config2.workspace);
657
560
  const repoEntries = config2.repos.map(
658
561
  (r) => ` - If it says "Repo: ${r.name}" or title starts with "${r.match}" \u2192 \`${resolve3(workspace, r.path)}\` (base branch: \`${r.base_branch}\`)`
659
562
  ).join("\n");
660
- const baseBranchInstruction = config2.repos.length > 0 ? "From the repo's base branch (listed above)" : `From \`${config2.base_branch}\``;
661
- const testBlock = buildTestInstructions(testRunner ?? null);
563
+ const baseBranch = config2.base_branch;
564
+ const baseBranchInstruction = config2.repos.length > 0 ? "From the repo's base branch (listed above)" : `From \`${baseBranch}\``;
565
+ const testBlock = buildTestInstructions(testRunner ?? null, pm);
662
566
  const readmeBlock = buildReadmeInstructions();
663
567
  const hookBlock = buildPreCommitHookInstructions();
664
- const manifestPath = join(workspace, ".lisa-manifest.json");
665
- return `You are an autonomous implementation agent. Your job is to implement a single
666
- issue, validate it, commit, and push the branch.
568
+ const manifestPath = join2(workspace, ".lisa-manifest.json");
569
+ return `You are an autonomous implementation agent. Your job is to implement an issue end-to-end: code, push, PR, and tracker update.
667
570
 
668
571
  ## Issue
669
572
 
670
- - **ID:** ${issue.id}
671
- - **Title:** ${issue.title}
672
- - **URL:** ${issue.url}
573
+ - **ID:** ${issue2.id}
574
+ - **Title:** ${issue2.title}
575
+ - **URL:** ${issue2.url}
673
576
 
674
577
  ### Description
675
578
 
676
- ${issue.description}
579
+ ${issue2.description}
677
580
 
678
581
  ## Instructions
679
582
 
@@ -682,9 +585,9 @@ ${repoEntries}
682
585
  - If it references multiple repos, pick the PRIMARY one (the one with the most files listed).
683
586
 
684
587
  2. **Create a branch**: ${baseBranchInstruction}, create a branch with an **English** slug:
685
- \`feat/${issue.id.toLowerCase()}-short-english-description\`
588
+ \`feat/${issue2.id.toLowerCase()}-short-english-description\`
686
589
  The description MUST be in English \u2014 translate or summarize the issue title if it's in another language.
687
- Example: "Implementar rate limiting na API" \u2192 \`feat/${issue.id.toLowerCase()}-add-rate-limiting-to-api\`
590
+ Example: "Implementar rate limiting na API" \u2192 \`feat/${issue2.id.toLowerCase()}-add-rate-limiting-to-api\`
688
591
 
689
592
  3. **Implement**: Follow the issue description exactly:
690
593
  - Read all relevant files listed in the description first (if present)
@@ -698,16 +601,25 @@ ${testBlock}${readmeBlock}${hookBlock}
698
601
  - Fix any errors before proceeding.
699
602
 
700
603
  5. **Commit & Push**: Make atomic commits with conventional commit messages.
701
- Push the branch to origin.
604
+ Push the branch to origin:
605
+ \`git push -u origin <branch-name>\`
606
+ If the push fails due to a pre-push hook, read the error, fix the root cause, amend the commit, and retry. Do NOT use \`--no-verify\`.
702
607
  **IMPORTANT \u2014 Language rules:**
703
608
  - All commit messages MUST be in English.
704
609
  - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
705
610
 
706
- 6. **Write manifest**: Before finishing, create \`${manifestPath}\` with JSON:
611
+ 6. **Create PR**: Create a pull request using the GitHub CLI:
612
+ \`gh pr create --title "<conventional-commit-title>" --body "<markdown-summary>" --base ${baseBranch}\`
613
+ Capture the PR URL from the output.
614
+
615
+ 7. **Update tracker**: Call the lisa CLI to mark the issue as done:
616
+ \`lisa issue done ${issue2.id} --pr-url <pr-url>\`
617
+ Wait 1 second before calling this command.
618
+
619
+ 8. **Write manifest**: Before finishing, create \`${manifestPath}\` with JSON:
707
620
  \`\`\`json
708
- {"repoPath": "<absolute path to this repo>", "branch": "<branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<markdown-formatted English summary>"}
621
+ {"repoPath": "<absolute path to this repo>", "branch": "<branch name>", "prUrl": "<pull request URL>"}
709
622
  \`\`\`
710
- ${buildPrBodyInstructions()}
711
623
  Do NOT commit this file.
712
624
 
713
625
  ## Rules
@@ -718,57 +630,27 @@ ${testBlock}${readmeBlock}${hookBlock}
718
630
  - Do NOT install new dependencies unless the issue explicitly requires it.
719
631
  - If you get stuck or the issue is unclear, STOP and explain why.
720
632
  - One issue only. Do not pick up additional issues.
721
- - If the repo has a CLAUDE.md, read it first and follow its conventions.
722
- - Do NOT create pull requests \u2014 the caller handles that.
723
- - Do NOT update the issue tracker \u2014 the caller handles that.`;
724
- }
725
- function buildPushRecoveryPrompt(hookErrors) {
726
- return `The previous \`git push\` failed because a pre-push hook rejected the push.
727
- Here is the full error output:
728
-
729
- \`\`\`
730
- ${hookErrors}
731
- \`\`\`
732
-
733
- ## Instructions
734
-
735
- 1. **Read the errors** above carefully and identify the root cause.
736
- 2. **Fix the issue** \u2014 common fixes include:
737
- - Run linters/formatters (e.g. \`npm run lint -- --fix\`, \`npm run format\`)
738
- - Run code generation (e.g. \`npx prisma generate\`, \`npm run codegen\`)
739
- - Fix type errors in the source files
740
- - Fix failing tests
741
- 3. **Amend the commit** so the fix is included:
742
- \`\`\`
743
- git add -A && git commit --amend --no-edit
744
- \`\`\`
745
- 4. **Do NOT push** \u2014 the caller handles pushing after you finish.
746
- 5. **Do NOT create pull requests** \u2014 the caller handles that.
747
- 6. **Do NOT update the issue tracker** \u2014 the caller handles that.
748
-
749
- Focus only on fixing the hook errors. Do not make unrelated changes.`;
633
+ - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
750
634
  }
751
- function buildNativeWorktreePrompt(issue, repoPath, testRunner) {
752
- const testBlock = buildTestInstructions(testRunner ?? null);
635
+ function buildNativeWorktreePrompt(issue2, repoPath, testRunner, pm, baseBranch) {
636
+ const testBlock = buildTestInstructions(testRunner ?? null, pm);
753
637
  const readmeBlock = buildReadmeInstructions();
754
638
  const hookBlock = buildPreCommitHookInstructions();
755
- const prBodyBlock = buildPrBodyInstructions();
756
- const manifestLocation = repoPath ? `\`${join(repoPath, ".lisa-manifest.json")}\`` : "`.lisa-manifest.json` in the **current directory**";
757
- return `You are an autonomous implementation agent. Your job is to implement a single
758
- issue, validate it, and commit.
639
+ const manifestLocation = repoPath ? `\`${join2(repoPath, ".lisa-manifest.json")}\`` : "`.lisa-manifest.json` in the **current directory**";
640
+ return `You are an autonomous implementation agent. Your job is to implement an issue end-to-end: code, push, PR, and tracker update.
759
641
 
760
642
  You are working inside a git worktree that was automatically created for this task.
761
643
  Work on the current branch \u2014 it was created for you.
762
644
 
763
645
  ## Issue
764
646
 
765
- - **ID:** ${issue.id}
766
- - **Title:** ${issue.title}
767
- - **URL:** ${issue.url}
647
+ - **ID:** ${issue2.id}
648
+ - **Title:** ${issue2.title}
649
+ - **URL:** ${issue2.url}
768
650
 
769
651
  ### Description
770
652
 
771
- ${issue.description}
653
+ ${issue2.description}
772
654
 
773
655
  ## Instructions
774
656
 
@@ -785,51 +667,58 @@ ${testBlock}${readmeBlock}${hookBlock}
785
667
 
786
668
  3. **Commit**: Make atomic commits with conventional commit messages.
787
669
  **Branch name must be in English.** If the current branch name contains non-English words,
788
- rename it: \`git branch -m <current-name> feat/${issue.id.toLowerCase()}-short-english-slug\`
789
- Do NOT push \u2014 the caller handles pushing.
670
+ rename it: \`git branch -m <current-name> feat/${issue2.id.toLowerCase()}-short-english-slug\`
790
671
  **IMPORTANT \u2014 Language rules:**
791
672
  - All commit messages MUST be in English.
792
673
  - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
793
674
 
794
- 4. **Write manifest**: Create ${manifestLocation} with JSON:
675
+ 4. **Push**: Push the branch to origin:
676
+ \`git push -u origin <branch-name>\`
677
+ If the push fails due to a pre-push hook, read the error, fix the root cause, amend the commit, and retry. Do NOT use \`--no-verify\`.
678
+
679
+ 5. **Create PR**: Create a pull request using the GitHub CLI:
680
+ \`gh pr create --title "<conventional-commit-title>" --body "<markdown-summary>"${baseBranch ? ` --base ${baseBranch}` : ""}\`
681
+ Capture the PR URL from the output.
682
+
683
+ 6. **Update tracker**: Call the lisa CLI to mark the issue as done:
684
+ \`lisa issue done ${issue2.id} --pr-url <pr-url>\`
685
+ Wait 1 second before calling this command.
686
+
687
+ 7. **Write manifest**: Create ${manifestLocation} with JSON:
795
688
  \`\`\`json
796
- {"branch": "<final English branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<markdown-formatted English summary>"}
689
+ {"branch": "<final English branch name>", "prUrl": "<pull request URL>"}
797
690
  \`\`\`
798
- ${prBodyBlock}
799
691
  Do NOT commit this file.
800
692
 
801
693
  ## Rules
802
694
 
803
695
  - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
804
696
  - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
805
- - Do NOT push \u2014 the caller handles that.
806
697
  - Do NOT install new dependencies unless the issue explicitly requires it.
807
698
  - If you get stuck or the issue is unclear, STOP and explain why.
808
699
  - One issue only. Do not pick up additional issues.
809
- - If the repo has a CLAUDE.md, read it first and follow its conventions.
810
- - Do NOT create pull requests \u2014 the caller handles that.
811
- - Do NOT update the issue tracker \u2014 the caller handles that.`;
700
+ - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
812
701
  }
813
- function buildPlanningPrompt(issue, config2) {
702
+ function buildPlanningPrompt(issue2, config2) {
814
703
  const workspace = resolve3(config2.workspace);
815
704
  const repoBlock = config2.repos.map((r) => {
816
705
  const absPath = resolve3(workspace, r.path);
817
706
  return `- **${r.name}**: \`${absPath}\` (base branch: \`${r.base_branch}\`)`;
818
707
  }).join("\n");
819
- const planPath = join(workspace, ".lisa-plan.json");
708
+ const planPath = join2(workspace, ".lisa-plan.json");
820
709
  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.
821
710
 
822
711
  **Do NOT implement anything.** Only analyze the issue and produce the plan file.
823
712
 
824
713
  ## Issue
825
714
 
826
- - **ID:** ${issue.id}
827
- - **Title:** ${issue.title}
828
- - **URL:** ${issue.url}
715
+ - **ID:** ${issue2.id}
716
+ - **Title:** ${issue2.title}
717
+ - **URL:** ${issue2.url}
829
718
 
830
719
  ### Description
831
720
 
832
- ${issue.description}
721
+ ${issue2.description}
833
722
 
834
723
  ## Available Repositories
835
724
 
@@ -861,14 +750,12 @@ ${repoBlock}
861
750
  - The \`scope\` field should be a concise English description of what needs to be done in that specific repo.
862
751
  - Order matters: lower order numbers execute first.
863
752
  - Do NOT implement anything. Do NOT create branches, write code, or commit.
864
- - Do NOT push, create pull requests, or update the issue tracker.
865
753
  - If only one repo is affected, the plan should have a single step.`;
866
754
  }
867
- function buildScopedImplementPrompt(issue, step, previousResults, testRunner) {
868
- const testBlock = buildTestInstructions(testRunner ?? null);
755
+ function buildScopedImplementPrompt(issue2, step, previousResults, testRunner, pm, isLastStep = false, baseBranch) {
756
+ const testBlock = buildTestInstructions(testRunner ?? null, pm);
869
757
  const readmeBlock = buildReadmeInstructions();
870
758
  const hookBlock = buildPreCommitHookInstructions();
871
- const prBodyBlock = buildPrBodyInstructions();
872
759
  const previousBlock = previousResults.length > 0 ? `
873
760
  ## Previous Steps
874
761
 
@@ -878,6 +765,11 @@ ${previousResults.map((r) => `- **${r.repoPath}**: branch \`${r.branch}\`${r.prU
878
765
 
879
766
  Use this context if the current step depends on changes from previous steps.
880
767
  ` : "";
768
+ const trackerStep = isLastStep ? `
769
+ 6. **Update tracker**: Call \`lisa issue done ${issue2.id} --pr-url <pr-url>\` (wait 1 second before calling).
770
+ ` : `
771
+ 6. **Skip tracker update**: This is not the last step. The caller handles the tracker update after all steps complete.
772
+ `;
881
773
  return `You are an autonomous implementation agent. Your job is to implement a specific part of an issue in a single repository.
882
774
 
883
775
  You are working inside a git worktree that was automatically created for this task.
@@ -885,13 +777,13 @@ Work on the current branch \u2014 it was created for you.
885
777
 
886
778
  ## Issue
887
779
 
888
- - **ID:** ${issue.id}
889
- - **Title:** ${issue.title}
890
- - **URL:** ${issue.url}
780
+ - **ID:** ${issue2.id}
781
+ - **Title:** ${issue2.title}
782
+ - **URL:** ${issue2.url}
891
783
 
892
784
  ### Description
893
785
 
894
- ${issue.description}
786
+ ${issue2.description}
895
787
 
896
788
  ## Your Scope
897
789
 
@@ -915,46 +807,49 @@ ${testBlock}${readmeBlock}${hookBlock}
915
807
 
916
808
  3. **Commit**: Make atomic commits with conventional commit messages.
917
809
  **Branch name must be in English.** If the current branch name contains non-English words,
918
- rename it: \`git branch -m <current-name> feat/${issue.id.toLowerCase()}-short-english-slug\`
919
- Do NOT push \u2014 the caller handles pushing.
810
+ rename it: \`git branch -m <current-name> feat/${issue2.id.toLowerCase()}-short-english-slug\`
920
811
  **IMPORTANT \u2014 Language rules:**
921
812
  - All commit messages MUST be in English.
922
813
  - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
923
814
 
924
- 4. **Write manifest**: Create \`.lisa-manifest.json\` in the **current directory** with JSON:
815
+ 4. **Push**: Push the branch to origin:
816
+ \`git push -u origin <branch-name>\`
817
+ If the push fails due to a pre-push hook, read the error, fix the root cause, amend the commit, and retry. Do NOT use \`--no-verify\`.
818
+
819
+ 5. **Create PR**: Create a pull request using the GitHub CLI:
820
+ \`gh pr create --title "<conventional-commit-title>" --body "<markdown-summary>"${baseBranch ? ` --base ${baseBranch}` : ""}\`
821
+ Capture the PR URL from the output.
822
+ ${trackerStep}
823
+ 7. **Write manifest**: Create \`.lisa-manifest.json\` in the **current directory** with JSON:
925
824
  \`\`\`json
926
- {"branch": "<final English branch name>", "prTitle": "<English PR title, conventional commit format>", "prBody": "<markdown-formatted English summary>"}
825
+ {"branch": "<final English branch name>", "prUrl": "<pull request URL>"}
927
826
  \`\`\`
928
- ${prBodyBlock}
929
827
  Do NOT commit this file.
930
828
 
931
829
  ## Rules
932
830
 
933
831
  - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
934
832
  - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
935
- - Do NOT push \u2014 the caller handles that.
936
833
  - Do NOT install new dependencies unless the issue explicitly requires it.
937
834
  - If you get stuck or the issue is unclear, STOP and explain why.
938
835
  - One scope only. Do not pick up additional work outside your scope.
939
- - If the repo has a CLAUDE.md, read it first and follow its conventions.
940
- - Do NOT create pull requests \u2014 the caller handles that.
941
- - Do NOT update the issue tracker \u2014 the caller handles that.`;
836
+ - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
942
837
  }
943
838
 
944
- // src/guardrails.ts
945
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
946
- import { dirname as dirname2, join as join2 } from "path";
839
+ // src/session/guardrails.ts
840
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
841
+ import { dirname as dirname2, join as join3 } from "path";
947
842
  var GUARDRAILS_FILE = ".lisa/guardrails.md";
948
843
  var MAX_ENTRIES = 20;
949
844
  var CONTEXT_LINES = 20;
950
845
  function guardrailsPath(dir) {
951
- return join2(dir, GUARDRAILS_FILE);
846
+ return join3(dir, GUARDRAILS_FILE);
952
847
  }
953
848
  function readGuardrails(dir) {
954
849
  const path = guardrailsPath(dir);
955
- if (!existsSync4(path)) return "";
850
+ if (!existsSync5(path)) return "";
956
851
  try {
957
- return readFileSync3(path, "utf-8");
852
+ return readFileSync4(path, "utf-8");
958
853
  } catch {
959
854
  return "";
960
855
  }
@@ -984,10 +879,10 @@ function extractErrorType(output) {
984
879
  function appendEntry(dir, entry) {
985
880
  const path = guardrailsPath(dir);
986
881
  const guardrailsDir = dirname2(path);
987
- if (!existsSync4(guardrailsDir)) {
882
+ if (!existsSync5(guardrailsDir)) {
988
883
  mkdirSync3(guardrailsDir, { recursive: true });
989
884
  }
990
- const existing = existsSync4(path) ? readFileSync3(path, "utf-8") : "";
885
+ const existing = existsSync5(path) ? readFileSync4(path, "utf-8") : "";
991
886
  const newEntryText = formatEntry(entry);
992
887
  let content;
993
888
  if (!existing.trim()) {
@@ -1033,13 +928,13 @@ function splitEntries(content) {
1033
928
  });
1034
929
  }
1035
930
 
1036
- // src/providers/claude.ts
1037
- import { execSync, spawn as spawn2 } from "child_process";
1038
- import { appendFileSync as appendFileSync2, mkdtempSync, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
931
+ // src/providers/aider.ts
932
+ import { execSync, spawn } from "child_process";
933
+ import { appendFileSync as appendFileSync3, mkdtempSync, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
1039
934
  import { tmpdir } from "os";
1040
- import { join as join3 } from "path";
935
+ import { join as join4 } from "path";
1041
936
 
1042
- // src/overseer.ts
937
+ // src/session/overseer.ts
1043
938
  import { execFile } from "child_process";
1044
939
  import { promisify } from "util";
1045
940
  var execFileAsync = promisify(execFile);
@@ -1109,13 +1004,93 @@ function startOverseer(proc, cwd, config2, getSnapshot = getGitSnapshot) {
1109
1004
  };
1110
1005
  }
1111
1006
 
1007
+ // src/providers/aider.ts
1008
+ var AiderProvider = class {
1009
+ name = "aider";
1010
+ async isAvailable() {
1011
+ try {
1012
+ execSync("aider --version", { stdio: "ignore" });
1013
+ return true;
1014
+ } catch {
1015
+ return false;
1016
+ }
1017
+ }
1018
+ async run(prompt, opts) {
1019
+ const start = Date.now();
1020
+ const tmpDir = mkdtempSync(join4(tmpdir(), "lisa-"));
1021
+ const promptFile = join4(tmpDir, "prompt.md");
1022
+ writeFileSync4(promptFile, prompt, "utf-8");
1023
+ try {
1024
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
1025
+ const proc = spawn(
1026
+ "sh",
1027
+ ["-c", `aider --message "$(cat '${promptFile}')" --yes-always ${modelFlag}`],
1028
+ {
1029
+ cwd: opts.cwd,
1030
+ stdio: ["ignore", "pipe", "pipe"]
1031
+ }
1032
+ );
1033
+ if (proc.pid) opts.onProcess?.(proc.pid);
1034
+ const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1035
+ const chunks = [];
1036
+ proc.stdout.on("data", (chunk) => {
1037
+ const text2 = chunk.toString();
1038
+ if (getOutputMode() !== "tui") process.stdout.write(text2);
1039
+ chunks.push(text2);
1040
+ try {
1041
+ appendFileSync3(opts.logFile, text2);
1042
+ } catch {
1043
+ }
1044
+ });
1045
+ proc.stderr.on("data", (chunk) => {
1046
+ const text2 = chunk.toString();
1047
+ if (getOutputMode() !== "tui") process.stderr.write(text2);
1048
+ try {
1049
+ appendFileSync3(opts.logFile, text2);
1050
+ } catch {
1051
+ }
1052
+ });
1053
+ const exitCode = await new Promise((resolve6) => {
1054
+ proc.on("close", (code) => {
1055
+ overseer?.stop();
1056
+ resolve6(code ?? 1);
1057
+ });
1058
+ });
1059
+ if (overseer?.wasKilled()) {
1060
+ chunks.push(STUCK_MESSAGE);
1061
+ }
1062
+ return {
1063
+ success: exitCode === 0 && !overseer?.wasKilled(),
1064
+ output: chunks.join(""),
1065
+ duration: Date.now() - start
1066
+ };
1067
+ } catch (err) {
1068
+ return {
1069
+ success: false,
1070
+ output: err instanceof Error ? err.message : String(err),
1071
+ duration: Date.now() - start
1072
+ };
1073
+ } finally {
1074
+ try {
1075
+ unlinkSync(promptFile);
1076
+ } catch {
1077
+ }
1078
+ }
1079
+ }
1080
+ };
1081
+
1112
1082
  // src/providers/claude.ts
1083
+ import { execSync as execSync2, spawn as spawn2 } from "child_process";
1084
+ import { appendFileSync as appendFileSync4, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
1085
+ import { tmpdir as tmpdir2 } from "os";
1086
+ import { join as join5 } from "path";
1113
1087
  var ClaudeProvider = class {
1114
1088
  name = "claude";
1115
- supportsNativeWorktree = true;
1089
+ supportsNativeWorktree = false;
1090
+ // --worktree flag requires a TTY and hangs in non-interactive mode
1116
1091
  async isAvailable() {
1117
1092
  try {
1118
- execSync("claude --version", { stdio: "ignore" });
1093
+ execSync2("claude --version", { stdio: "ignore" });
1119
1094
  return true;
1120
1095
  } catch {
1121
1096
  return false;
@@ -1123,38 +1098,36 @@ var ClaudeProvider = class {
1123
1098
  }
1124
1099
  async run(prompt, opts) {
1125
1100
  const start = Date.now();
1126
- const tmpDir = mkdtempSync(join3(tmpdir(), "lisa-"));
1127
- const promptFile = join3(tmpDir, "prompt.md");
1128
- writeFileSync4(promptFile, prompt, "utf-8");
1101
+ const tmpDir = mkdtempSync2(join5(tmpdir2(), "lisa-"));
1102
+ const promptFile = join5(tmpDir, "prompt.md");
1103
+ writeFileSync5(promptFile, prompt, "utf-8");
1129
1104
  try {
1130
1105
  const flags = ["-p", "--dangerously-skip-permissions"];
1131
1106
  if (opts.model) {
1132
1107
  flags.push("--model", opts.model);
1133
1108
  }
1134
- if (opts.useNativeWorktree) {
1135
- flags.push("--worktree");
1136
- }
1137
1109
  const proc = spawn2("sh", ["-c", `claude ${flags.join(" ")} "$(cat '${promptFile}')"`], {
1138
1110
  cwd: opts.cwd,
1139
1111
  stdio: ["ignore", "pipe", "pipe"],
1140
1112
  env: { ...process.env, CLAUDECODE: void 0 }
1141
1113
  });
1114
+ if (proc.pid) opts.onProcess?.(proc.pid);
1142
1115
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1143
1116
  const chunks = [];
1144
1117
  proc.stdout.on("data", (chunk) => {
1145
1118
  const text2 = chunk.toString();
1146
- process.stdout.write(text2);
1119
+ if (getOutputMode() !== "tui") process.stdout.write(text2);
1147
1120
  chunks.push(text2);
1148
1121
  try {
1149
- appendFileSync2(opts.logFile, text2);
1122
+ appendFileSync4(opts.logFile, text2);
1150
1123
  } catch {
1151
1124
  }
1152
1125
  });
1153
1126
  proc.stderr.on("data", (chunk) => {
1154
1127
  const text2 = chunk.toString();
1155
- process.stderr.write(text2);
1128
+ if (getOutputMode() !== "tui") process.stderr.write(text2);
1156
1129
  try {
1157
- appendFileSync2(opts.logFile, text2);
1130
+ appendFileSync4(opts.logFile, text2);
1158
1131
  } catch {
1159
1132
  }
1160
1133
  });
@@ -1180,7 +1153,7 @@ var ClaudeProvider = class {
1180
1153
  };
1181
1154
  } finally {
1182
1155
  try {
1183
- unlinkSync(promptFile);
1156
+ unlinkSync2(promptFile);
1184
1157
  } catch {
1185
1158
  }
1186
1159
  }
@@ -1188,15 +1161,15 @@ var ClaudeProvider = class {
1188
1161
  };
1189
1162
 
1190
1163
  // src/providers/copilot.ts
1191
- import { execSync as execSync2, spawn as spawn3 } from "child_process";
1192
- import { appendFileSync as appendFileSync3, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
1193
- import { tmpdir as tmpdir2 } from "os";
1194
- import { join as join4 } from "path";
1164
+ import { execSync as execSync3, spawn as spawn3 } from "child_process";
1165
+ import { appendFileSync as appendFileSync5, mkdtempSync as mkdtempSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6 } from "fs";
1166
+ import { tmpdir as tmpdir3 } from "os";
1167
+ import { join as join6 } from "path";
1195
1168
  var CopilotProvider = class {
1196
1169
  name = "copilot";
1197
1170
  async isAvailable() {
1198
1171
  try {
1199
- execSync2("copilot version", { stdio: "ignore" });
1172
+ execSync3("copilot version", { stdio: "ignore" });
1200
1173
  return true;
1201
1174
  } catch {
1202
1175
  return false;
@@ -1204,30 +1177,31 @@ var CopilotProvider = class {
1204
1177
  }
1205
1178
  async run(prompt, opts) {
1206
1179
  const start = Date.now();
1207
- const tmpDir = mkdtempSync2(join4(tmpdir2(), "lisa-"));
1208
- const promptFile = join4(tmpDir, "prompt.md");
1209
- writeFileSync5(promptFile, prompt, "utf-8");
1180
+ const tmpDir = mkdtempSync3(join6(tmpdir3(), "lisa-"));
1181
+ const promptFile = join6(tmpDir, "prompt.md");
1182
+ writeFileSync6(promptFile, prompt, "utf-8");
1210
1183
  try {
1211
1184
  const proc = spawn3("sh", ["-c", `copilot --allow-all -p "$(cat '${promptFile}')"`], {
1212
1185
  cwd: opts.cwd,
1213
1186
  stdio: ["ignore", "pipe", "pipe"]
1214
1187
  });
1188
+ if (proc.pid) opts.onProcess?.(proc.pid);
1215
1189
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1216
1190
  const chunks = [];
1217
1191
  proc.stdout.on("data", (chunk) => {
1218
1192
  const text2 = chunk.toString();
1219
- process.stdout.write(text2);
1193
+ if (getOutputMode() !== "tui") process.stdout.write(text2);
1220
1194
  chunks.push(text2);
1221
1195
  try {
1222
- appendFileSync3(opts.logFile, text2);
1196
+ appendFileSync5(opts.logFile, text2);
1223
1197
  } catch {
1224
1198
  }
1225
1199
  });
1226
1200
  proc.stderr.on("data", (chunk) => {
1227
1201
  const text2 = chunk.toString();
1228
- process.stderr.write(text2);
1202
+ if (getOutputMode() !== "tui") process.stderr.write(text2);
1229
1203
  try {
1230
- appendFileSync3(opts.logFile, text2);
1204
+ appendFileSync5(opts.logFile, text2);
1231
1205
  } catch {
1232
1206
  }
1233
1207
  });
@@ -1253,7 +1227,7 @@ var CopilotProvider = class {
1253
1227
  };
1254
1228
  } finally {
1255
1229
  try {
1256
- unlinkSync2(promptFile);
1230
+ unlinkSync3(promptFile);
1257
1231
  } catch {
1258
1232
  }
1259
1233
  }
@@ -1261,14 +1235,14 @@ var CopilotProvider = class {
1261
1235
  };
1262
1236
 
1263
1237
  // src/providers/cursor.ts
1264
- import { execSync as execSync3, spawn as spawn4 } from "child_process";
1265
- import { appendFileSync as appendFileSync4, mkdtempSync as mkdtempSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6 } from "fs";
1266
- import { tmpdir as tmpdir3 } from "os";
1267
- import { join as join5 } from "path";
1238
+ import { execSync as execSync4, spawn as spawn4 } from "child_process";
1239
+ import { appendFileSync as appendFileSync6, mkdtempSync as mkdtempSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync7 } from "fs";
1240
+ import { tmpdir as tmpdir4 } from "os";
1241
+ import { join as join7 } from "path";
1268
1242
  function findCursorBinary() {
1269
1243
  for (const bin of ["agent", "cursor-agent"]) {
1270
1244
  try {
1271
- execSync3(`${bin} --version`, { stdio: "ignore" });
1245
+ execSync4(`${bin} --version`, { stdio: "ignore" });
1272
1246
  return bin;
1273
1247
  } catch {
1274
1248
  }
@@ -1290,34 +1264,36 @@ var CursorProvider = class {
1290
1264
  duration: Date.now() - start
1291
1265
  };
1292
1266
  }
1293
- const tmpDir = mkdtempSync3(join5(tmpdir3(), "lisa-"));
1294
- const promptFile = join5(tmpDir, "prompt.md");
1295
- writeFileSync6(promptFile, prompt, "utf-8");
1267
+ const tmpDir = mkdtempSync4(join7(tmpdir4(), "lisa-"));
1268
+ const promptFile = join7(tmpDir, "prompt.md");
1269
+ writeFileSync7(promptFile, prompt, "utf-8");
1296
1270
  try {
1271
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
1297
1272
  const proc = spawn4(
1298
1273
  "sh",
1299
- ["-c", `${bin} -p "$(cat '${promptFile}')" --output-format text --force`],
1274
+ ["-c", `${bin} -p "$(cat '${promptFile}')" --output-format text --force ${modelFlag}`],
1300
1275
  {
1301
1276
  cwd: opts.cwd,
1302
1277
  stdio: ["ignore", "pipe", "pipe"]
1303
1278
  }
1304
1279
  );
1280
+ if (proc.pid) opts.onProcess?.(proc.pid);
1305
1281
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1306
1282
  const chunks = [];
1307
1283
  proc.stdout.on("data", (chunk) => {
1308
1284
  const text2 = chunk.toString();
1309
- process.stdout.write(text2);
1285
+ if (getOutputMode() !== "tui") process.stdout.write(text2);
1310
1286
  chunks.push(text2);
1311
1287
  try {
1312
- appendFileSync4(opts.logFile, text2);
1288
+ appendFileSync6(opts.logFile, text2);
1313
1289
  } catch {
1314
1290
  }
1315
1291
  });
1316
1292
  proc.stderr.on("data", (chunk) => {
1317
1293
  const text2 = chunk.toString();
1318
- process.stderr.write(text2);
1294
+ if (getOutputMode() !== "tui") process.stderr.write(text2);
1319
1295
  try {
1320
- appendFileSync4(opts.logFile, text2);
1296
+ appendFileSync6(opts.logFile, text2);
1321
1297
  } catch {
1322
1298
  }
1323
1299
  });
@@ -1343,7 +1319,7 @@ var CursorProvider = class {
1343
1319
  };
1344
1320
  } finally {
1345
1321
  try {
1346
- unlinkSync3(promptFile);
1322
+ unlinkSync4(promptFile);
1347
1323
  } catch {
1348
1324
  }
1349
1325
  }
@@ -1351,15 +1327,15 @@ var CursorProvider = class {
1351
1327
  };
1352
1328
 
1353
1329
  // src/providers/gemini.ts
1354
- import { execSync as execSync4, spawn as spawn5 } from "child_process";
1355
- import { appendFileSync as appendFileSync5, mkdtempSync as mkdtempSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync7 } from "fs";
1356
- import { tmpdir as tmpdir4 } from "os";
1357
- import { join as join6 } from "path";
1330
+ import { execSync as execSync5, spawn as spawn5 } from "child_process";
1331
+ import { appendFileSync as appendFileSync7, mkdtempSync as mkdtempSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "fs";
1332
+ import { tmpdir as tmpdir5 } from "os";
1333
+ import { join as join8 } from "path";
1358
1334
  var GeminiProvider = class {
1359
1335
  name = "gemini";
1360
1336
  async isAvailable() {
1361
1337
  try {
1362
- execSync4("gemini --version", { stdio: "ignore" });
1338
+ execSync5("gemini --version", { stdio: "ignore" });
1363
1339
  return true;
1364
1340
  } catch {
1365
1341
  return false;
@@ -1367,31 +1343,32 @@ var GeminiProvider = class {
1367
1343
  }
1368
1344
  async run(prompt, opts) {
1369
1345
  const start = Date.now();
1370
- const tmpDir = mkdtempSync4(join6(tmpdir4(), "lisa-"));
1371
- const promptFile = join6(tmpDir, "prompt.md");
1372
- writeFileSync7(promptFile, prompt, "utf-8");
1346
+ const tmpDir = mkdtempSync5(join8(tmpdir5(), "lisa-"));
1347
+ const promptFile = join8(tmpDir, "prompt.md");
1348
+ writeFileSync8(promptFile, prompt, "utf-8");
1373
1349
  try {
1374
1350
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1375
1351
  const proc = spawn5("sh", ["-c", `gemini --yolo ${modelFlag} -p "$(cat '${promptFile}')"`], {
1376
1352
  cwd: opts.cwd,
1377
1353
  stdio: ["ignore", "pipe", "pipe"]
1378
1354
  });
1355
+ if (proc.pid) opts.onProcess?.(proc.pid);
1379
1356
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1380
1357
  const chunks = [];
1381
1358
  proc.stdout.on("data", (chunk) => {
1382
1359
  const text2 = chunk.toString();
1383
- process.stdout.write(text2);
1360
+ if (getOutputMode() !== "tui") process.stdout.write(text2);
1384
1361
  chunks.push(text2);
1385
1362
  try {
1386
- appendFileSync5(opts.logFile, text2);
1363
+ appendFileSync7(opts.logFile, text2);
1387
1364
  } catch {
1388
1365
  }
1389
1366
  });
1390
1367
  proc.stderr.on("data", (chunk) => {
1391
1368
  const text2 = chunk.toString();
1392
- process.stderr.write(text2);
1369
+ if (getOutputMode() !== "tui") process.stderr.write(text2);
1393
1370
  try {
1394
- appendFileSync5(opts.logFile, text2);
1371
+ appendFileSync7(opts.logFile, text2);
1395
1372
  } catch {
1396
1373
  }
1397
1374
  });
@@ -1417,7 +1394,82 @@ var GeminiProvider = class {
1417
1394
  };
1418
1395
  } finally {
1419
1396
  try {
1420
- unlinkSync4(promptFile);
1397
+ unlinkSync5(promptFile);
1398
+ } catch {
1399
+ }
1400
+ }
1401
+ }
1402
+ };
1403
+
1404
+ // src/providers/goose.ts
1405
+ import { execSync as execSync6, spawn as spawn6 } from "child_process";
1406
+ import { appendFileSync as appendFileSync8, mkdtempSync as mkdtempSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "fs";
1407
+ import { tmpdir as tmpdir6 } from "os";
1408
+ import { join as join9 } from "path";
1409
+ var GooseProvider = class {
1410
+ name = "goose";
1411
+ async isAvailable() {
1412
+ try {
1413
+ execSync6("goose --version", { stdio: "ignore" });
1414
+ return true;
1415
+ } catch {
1416
+ return false;
1417
+ }
1418
+ }
1419
+ async run(prompt, opts) {
1420
+ const start = Date.now();
1421
+ const tmpDir = mkdtempSync6(join9(tmpdir6(), "lisa-"));
1422
+ const promptFile = join9(tmpDir, "prompt.md");
1423
+ writeFileSync9(promptFile, prompt, "utf-8");
1424
+ try {
1425
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
1426
+ const proc = spawn6("sh", ["-c", `goose run ${modelFlag} --text "$(cat '${promptFile}')"`], {
1427
+ cwd: opts.cwd,
1428
+ stdio: ["ignore", "pipe", "pipe"]
1429
+ });
1430
+ if (proc.pid) opts.onProcess?.(proc.pid);
1431
+ const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1432
+ const chunks = [];
1433
+ proc.stdout.on("data", (chunk) => {
1434
+ const text2 = chunk.toString();
1435
+ if (getOutputMode() !== "tui") process.stdout.write(text2);
1436
+ chunks.push(text2);
1437
+ try {
1438
+ appendFileSync8(opts.logFile, text2);
1439
+ } catch {
1440
+ }
1441
+ });
1442
+ proc.stderr.on("data", (chunk) => {
1443
+ const text2 = chunk.toString();
1444
+ if (getOutputMode() !== "tui") process.stderr.write(text2);
1445
+ try {
1446
+ appendFileSync8(opts.logFile, text2);
1447
+ } catch {
1448
+ }
1449
+ });
1450
+ const exitCode = await new Promise((resolve6) => {
1451
+ proc.on("close", (code) => {
1452
+ overseer?.stop();
1453
+ resolve6(code ?? 1);
1454
+ });
1455
+ });
1456
+ if (overseer?.wasKilled()) {
1457
+ chunks.push(STUCK_MESSAGE);
1458
+ }
1459
+ return {
1460
+ success: exitCode === 0 && !overseer?.wasKilled(),
1461
+ output: chunks.join(""),
1462
+ duration: Date.now() - start
1463
+ };
1464
+ } catch (err) {
1465
+ return {
1466
+ success: false,
1467
+ output: err instanceof Error ? err.message : String(err),
1468
+ duration: Date.now() - start
1469
+ };
1470
+ } finally {
1471
+ try {
1472
+ unlinkSync6(promptFile);
1421
1473
  } catch {
1422
1474
  }
1423
1475
  }
@@ -1425,15 +1477,15 @@ var GeminiProvider = class {
1425
1477
  };
1426
1478
 
1427
1479
  // src/providers/opencode.ts
1428
- import { execSync as execSync5, spawn as spawn6 } from "child_process";
1429
- import { appendFileSync as appendFileSync6, mkdtempSync as mkdtempSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "fs";
1430
- import { tmpdir as tmpdir5 } from "os";
1431
- import { join as join7 } from "path";
1480
+ import { execSync as execSync7, spawn as spawn7 } from "child_process";
1481
+ import { appendFileSync as appendFileSync9, mkdtempSync as mkdtempSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync10 } from "fs";
1482
+ import { tmpdir as tmpdir7 } from "os";
1483
+ import { join as join10 } from "path";
1432
1484
  var OpenCodeProvider = class {
1433
1485
  name = "opencode";
1434
1486
  async isAvailable() {
1435
1487
  try {
1436
- execSync5("opencode --version", { stdio: "ignore" });
1488
+ execSync7("opencode --version", { stdio: "ignore" });
1437
1489
  return true;
1438
1490
  } catch {
1439
1491
  return false;
@@ -1441,30 +1493,31 @@ var OpenCodeProvider = class {
1441
1493
  }
1442
1494
  async run(prompt, opts) {
1443
1495
  const start = Date.now();
1444
- const tmpDir = mkdtempSync5(join7(tmpdir5(), "lisa-"));
1445
- const promptFile = join7(tmpDir, "prompt.md");
1446
- writeFileSync8(promptFile, prompt, "utf-8");
1496
+ const tmpDir = mkdtempSync7(join10(tmpdir7(), "lisa-"));
1497
+ const promptFile = join10(tmpDir, "prompt.md");
1498
+ writeFileSync10(promptFile, prompt, "utf-8");
1447
1499
  try {
1448
- const proc = spawn6("sh", ["-c", `opencode run "$(cat '${promptFile}')"`], {
1500
+ const proc = spawn7("sh", ["-c", `opencode run "$(cat '${promptFile}')"`], {
1449
1501
  cwd: opts.cwd,
1450
1502
  stdio: ["ignore", "pipe", "pipe"]
1451
1503
  });
1504
+ if (proc.pid) opts.onProcess?.(proc.pid);
1452
1505
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1453
1506
  const chunks = [];
1454
1507
  proc.stdout.on("data", (chunk) => {
1455
1508
  const text2 = chunk.toString();
1456
- process.stdout.write(text2);
1509
+ if (getOutputMode() !== "tui") process.stdout.write(text2);
1457
1510
  chunks.push(text2);
1458
1511
  try {
1459
- appendFileSync6(opts.logFile, text2);
1512
+ appendFileSync9(opts.logFile, text2);
1460
1513
  } catch {
1461
1514
  }
1462
1515
  });
1463
1516
  proc.stderr.on("data", (chunk) => {
1464
1517
  const text2 = chunk.toString();
1465
- process.stderr.write(text2);
1518
+ if (getOutputMode() !== "tui") process.stderr.write(text2);
1466
1519
  try {
1467
- appendFileSync6(opts.logFile, text2);
1520
+ appendFileSync9(opts.logFile, text2);
1468
1521
  } catch {
1469
1522
  }
1470
1523
  });
@@ -1490,7 +1543,7 @@ var OpenCodeProvider = class {
1490
1543
  };
1491
1544
  } finally {
1492
1545
  try {
1493
- unlinkSync5(promptFile);
1546
+ unlinkSync7(promptFile);
1494
1547
  } catch {
1495
1548
  }
1496
1549
  }
@@ -1503,7 +1556,9 @@ var providers = {
1503
1556
  gemini: () => new GeminiProvider(),
1504
1557
  opencode: () => new OpenCodeProvider(),
1505
1558
  copilot: () => new CopilotProvider(),
1506
- cursor: () => new CursorProvider()
1559
+ cursor: () => new CursorProvider(),
1560
+ goose: () => new GooseProvider(),
1561
+ aider: () => new AiderProvider()
1507
1562
  };
1508
1563
  async function getAvailableProviders() {
1509
1564
  const all = Object.values(providers).map((f) => f());
@@ -1634,28 +1689,685 @@ function formatAttemptsReport(attempts) {
1634
1689
  return lines.join("\n");
1635
1690
  }
1636
1691
 
1637
- // src/sources/linear.ts
1638
- var API_URL2 = "https://api.linear.app/graphql";
1639
- var REQUEST_TIMEOUT_MS2 = 3e4;
1640
- function getApiKey() {
1641
- const key = process.env.LINEAR_API_KEY;
1642
- if (!key) throw new Error("LINEAR_API_KEY is not set");
1643
- return key;
1644
- }
1645
- async function gql(query, variables) {
1646
- const res = await fetch(API_URL2, {
1647
- method: "POST",
1648
- headers: {
1649
- "Content-Type": "application/json",
1650
- Authorization: getApiKey()
1651
- },
1652
- body: JSON.stringify({ query, variables }),
1653
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
1692
+ // src/session/lifecycle.ts
1693
+ import { spawn as spawn8 } from "child_process";
1694
+ import { createConnection } from "net";
1695
+ import { resolve as resolve4 } from "path";
1696
+ var managedResources = [];
1697
+ var cleanupRegistered = false;
1698
+ function isPortInUse(port) {
1699
+ return new Promise((resolve6) => {
1700
+ const socket = createConnection({ port }, () => {
1701
+ socket.destroy();
1702
+ resolve6(true);
1703
+ });
1704
+ socket.on("error", () => {
1705
+ socket.destroy();
1706
+ resolve6(false);
1707
+ });
1654
1708
  });
1655
- if (!res.ok) {
1656
- const text2 = await res.text();
1657
- throw new Error(`Linear API error (${res.status}): ${text2}`);
1658
- }
1709
+ }
1710
+ function waitForPort(port, timeoutMs) {
1711
+ return new Promise((resolve6) => {
1712
+ const deadline = Date.now() + timeoutMs;
1713
+ const check = () => {
1714
+ if (Date.now() > deadline) {
1715
+ resolve6(false);
1716
+ return;
1717
+ }
1718
+ isPortInUse(port).then((inUse) => {
1719
+ if (inUse) {
1720
+ resolve6(true);
1721
+ } else {
1722
+ setTimeout(check, 500);
1723
+ }
1724
+ });
1725
+ };
1726
+ check();
1727
+ });
1728
+ }
1729
+ function spawnResource(config2, baseCwd) {
1730
+ const cwd = config2.cwd ? resolve4(baseCwd, config2.cwd) : baseCwd;
1731
+ const child = spawn8("sh", ["-c", config2.up], {
1732
+ cwd,
1733
+ stdio: "ignore",
1734
+ detached: true
1735
+ });
1736
+ child.unref();
1737
+ return child;
1738
+ }
1739
+ function runSetupCommand(command, cwd) {
1740
+ return new Promise((resolve6, reject) => {
1741
+ const child = spawn8("sh", ["-c", command], {
1742
+ cwd,
1743
+ stdio: "inherit"
1744
+ });
1745
+ child.on("close", (code) => {
1746
+ if (code === 0) {
1747
+ resolve6();
1748
+ } else {
1749
+ reject(new Error(`Setup command failed with exit code ${code}: ${command}`));
1750
+ }
1751
+ });
1752
+ child.on("error", (err) => {
1753
+ reject(new Error(`Setup command error: ${err.message}`));
1754
+ });
1755
+ });
1756
+ }
1757
+ async function startResources(repo, baseCwd) {
1758
+ const lifecycle = repo.lifecycle;
1759
+ if (!lifecycle) return true;
1760
+ registerCleanup();
1761
+ for (const resource of lifecycle.resources) {
1762
+ const alreadyRunning = await isPortInUse(resource.check_port);
1763
+ if (alreadyRunning) {
1764
+ ok(`Resource "${resource.name}" already running on port ${resource.check_port}`);
1765
+ continue;
1766
+ }
1767
+ log(`Starting resource "${resource.name}" on port ${resource.check_port}...`);
1768
+ const child = spawnResource(resource, baseCwd);
1769
+ managedResources.push({
1770
+ name: resource.name,
1771
+ config: resource,
1772
+ process: child
1773
+ });
1774
+ const timeoutMs = (resource.startup_timeout || 30) * 1e3;
1775
+ const ready = await waitForPort(resource.check_port, timeoutMs);
1776
+ if (!ready) {
1777
+ error(
1778
+ `Resource "${resource.name}" failed to start within ${resource.startup_timeout}s`
1779
+ );
1780
+ await stopResources();
1781
+ return false;
1782
+ }
1783
+ ok(`Resource "${resource.name}" is ready on port ${resource.check_port}`);
1784
+ }
1785
+ for (const command of lifecycle.setup) {
1786
+ log(`Running setup: ${command}`);
1787
+ try {
1788
+ await runSetupCommand(command, baseCwd);
1789
+ ok(`Setup complete: ${command}`);
1790
+ } catch (err) {
1791
+ error(`Setup failed: ${err instanceof Error ? err.message : String(err)}`);
1792
+ await stopResources();
1793
+ return false;
1794
+ }
1795
+ }
1796
+ return true;
1797
+ }
1798
+ async function stopResources() {
1799
+ for (const managed of managedResources) {
1800
+ const { name, config: config2, process: child } = managed;
1801
+ log(`Stopping resource "${name}"...`);
1802
+ try {
1803
+ if (config2.down === "auto") {
1804
+ if (child?.pid) {
1805
+ try {
1806
+ process.kill(-child.pid, "SIGTERM");
1807
+ } catch {
1808
+ }
1809
+ }
1810
+ } else {
1811
+ await new Promise((resolve6) => {
1812
+ const down = spawn8("sh", ["-c", config2.down], {
1813
+ stdio: "ignore"
1814
+ });
1815
+ down.on("close", () => resolve6());
1816
+ down.on("error", () => resolve6());
1817
+ });
1818
+ }
1819
+ ok(`Resource "${name}" stopped`);
1820
+ } catch (err) {
1821
+ warn(
1822
+ `Failed to stop resource "${name}": ${err instanceof Error ? err.message : String(err)}`
1823
+ );
1824
+ }
1825
+ }
1826
+ managedResources.length = 0;
1827
+ }
1828
+ function registerCleanup() {
1829
+ if (cleanupRegistered) return;
1830
+ cleanupRegistered = true;
1831
+ const cleanup = () => {
1832
+ for (const managed of managedResources) {
1833
+ const { config: config2, process: child } = managed;
1834
+ try {
1835
+ if (config2.down === "auto") {
1836
+ if (child?.pid) {
1837
+ process.kill(-child.pid, "SIGTERM");
1838
+ }
1839
+ }
1840
+ } catch {
1841
+ }
1842
+ }
1843
+ };
1844
+ process.on("exit", cleanup);
1845
+ process.on("SIGINT", () => {
1846
+ cleanup();
1847
+ process.exit(130);
1848
+ });
1849
+ process.on("SIGTERM", () => {
1850
+ cleanup();
1851
+ process.exit(143);
1852
+ });
1853
+ }
1854
+
1855
+ // src/sources/github-issues.ts
1856
+ var API_URL = "https://api.github.com";
1857
+ var REQUEST_TIMEOUT_MS = 3e4;
1858
+ var PRIORITY_LABELS = ["p1", "p2", "p3"];
1859
+ function getAuthHeaders() {
1860
+ const token = process.env.GITHUB_TOKEN;
1861
+ if (!token) throw new Error("GITHUB_TOKEN must be set");
1862
+ return {
1863
+ Authorization: `Bearer ${token}`,
1864
+ Accept: "application/vnd.github+json",
1865
+ "X-GitHub-Api-Version": "2022-11-28"
1866
+ };
1867
+ }
1868
+ async function githubFetch(method, path, body) {
1869
+ const url = `${API_URL}${path}`;
1870
+ const headers = {
1871
+ ...getAuthHeaders(),
1872
+ "Content-Type": "application/json"
1873
+ };
1874
+ const res = await fetch(url, {
1875
+ method,
1876
+ headers,
1877
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
1878
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
1879
+ });
1880
+ if (!res.ok) {
1881
+ const text2 = await res.text();
1882
+ throw new Error(`GitHub API error (${res.status}): ${text2}`);
1883
+ }
1884
+ if (method === "DELETE" || res.status === 204) return void 0;
1885
+ return await res.json();
1886
+ }
1887
+ async function githubGet(path) {
1888
+ return githubFetch("GET", path);
1889
+ }
1890
+ async function githubPost(path, body) {
1891
+ return githubFetch("POST", path, body);
1892
+ }
1893
+ async function githubPatch(path, body) {
1894
+ return githubFetch("PATCH", path, body);
1895
+ }
1896
+ async function githubDelete(path) {
1897
+ await githubFetch("DELETE", path);
1898
+ }
1899
+ function priorityRank(labels) {
1900
+ const names = labels.map((l) => l.name.toLowerCase());
1901
+ for (let i = 0; i < PRIORITY_LABELS.length; i++) {
1902
+ const p = PRIORITY_LABELS[i];
1903
+ if (p && names.includes(p)) return i;
1904
+ }
1905
+ return PRIORITY_LABELS.length;
1906
+ }
1907
+ function parseOwnerRepo(team) {
1908
+ const parts = team.split("/");
1909
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
1910
+ throw new Error(`Invalid owner/repo format: "${team}". Expected "owner/repo".`);
1911
+ }
1912
+ return { owner: parts[0], repo: parts[1] };
1913
+ }
1914
+ function parseGitHubIssueNumber(id) {
1915
+ const urlMatch = id.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
1916
+ if (urlMatch?.[1] && urlMatch?.[2] && urlMatch?.[3]) {
1917
+ return { owner: urlMatch[1], repo: urlMatch[2], number: urlMatch[3] };
1918
+ }
1919
+ const hashIdx = id.lastIndexOf("#");
1920
+ if (hashIdx !== -1) {
1921
+ const ref = id.slice(0, hashIdx);
1922
+ const num = id.slice(hashIdx + 1);
1923
+ const { owner, repo } = parseOwnerRepo(ref);
1924
+ return { owner, repo, number: num };
1925
+ }
1926
+ return { owner: "", repo: "", number: id };
1927
+ }
1928
+ function makeIssueId(owner, repo, number) {
1929
+ return `${owner}/${repo}#${number}`;
1930
+ }
1931
+ var GitHubIssuesSource = class {
1932
+ name = "github-issues";
1933
+ async fetchNextIssue(config2) {
1934
+ const { owner, repo } = parseOwnerRepo(config2.team);
1935
+ const label = encodeURIComponent(config2.label);
1936
+ const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
1937
+ const issues = await githubGet(path);
1938
+ if (issues.length === 0) return null;
1939
+ const sorted = [...issues].sort((a, b) => {
1940
+ const pa = priorityRank(a.labels);
1941
+ const pb = priorityRank(b.labels);
1942
+ if (pa !== pb) return pa - pb;
1943
+ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
1944
+ });
1945
+ const issue2 = sorted[0];
1946
+ if (!issue2) return null;
1947
+ return {
1948
+ id: makeIssueId(owner, repo, issue2.number),
1949
+ title: issue2.title,
1950
+ description: issue2.body ?? "",
1951
+ url: issue2.html_url
1952
+ };
1953
+ }
1954
+ async fetchIssueById(id) {
1955
+ const ref = parseGitHubIssueNumber(id);
1956
+ try {
1957
+ const issue2 = await githubGet(
1958
+ `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`
1959
+ );
1960
+ return {
1961
+ id: makeIssueId(ref.owner, ref.repo, issue2.number),
1962
+ title: issue2.title,
1963
+ description: issue2.body ?? "",
1964
+ url: issue2.html_url
1965
+ };
1966
+ } catch {
1967
+ return null;
1968
+ }
1969
+ }
1970
+ async updateStatus(issueId, labelToAdd) {
1971
+ const ref = parseGitHubIssueNumber(issueId);
1972
+ await githubPost(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
1973
+ labels: [labelToAdd]
1974
+ });
1975
+ }
1976
+ async attachPullRequest(issueId, prUrl) {
1977
+ const ref = parseGitHubIssueNumber(issueId);
1978
+ await githubPost(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/comments`, {
1979
+ body: `Pull request: ${prUrl}`
1980
+ });
1981
+ }
1982
+ async completeIssue(issueId, _status, labelToRemove) {
1983
+ const ref = parseGitHubIssueNumber(issueId);
1984
+ await githubPatch(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`, {
1985
+ state: "closed"
1986
+ });
1987
+ if (labelToRemove) {
1988
+ await this.removeLabel(issueId, labelToRemove);
1989
+ }
1990
+ }
1991
+ async listIssues(config2) {
1992
+ const { owner, repo } = parseOwnerRepo(config2.team);
1993
+ const label = encodeURIComponent(config2.label);
1994
+ const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
1995
+ const issues = await githubGet(path);
1996
+ return issues.map((issue2) => ({
1997
+ id: makeIssueId(owner, repo, issue2.number),
1998
+ title: issue2.title,
1999
+ description: issue2.body ?? "",
2000
+ url: issue2.html_url
2001
+ }));
2002
+ }
2003
+ async removeLabel(issueId, labelToRemove) {
2004
+ const ref = parseGitHubIssueNumber(issueId);
2005
+ try {
2006
+ await githubDelete(
2007
+ `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(labelToRemove)}`
2008
+ );
2009
+ } catch {
2010
+ }
2011
+ }
2012
+ };
2013
+
2014
+ // src/sources/gitlab-issues.ts
2015
+ var DEFAULT_BASE_URL = "https://gitlab.com";
2016
+ var REQUEST_TIMEOUT_MS2 = 3e4;
2017
+ var PRIORITY_LABELS2 = ["p1", "p2", "p3"];
2018
+ function getBaseUrl() {
2019
+ return (process.env.GITLAB_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
2020
+ }
2021
+ function getAuthHeaders2() {
2022
+ const token = process.env.GITLAB_TOKEN;
2023
+ if (!token) throw new Error("GITLAB_TOKEN must be set");
2024
+ return { "PRIVATE-TOKEN": token };
2025
+ }
2026
+ async function gitlabFetch(method, path, body) {
2027
+ const url = `${getBaseUrl()}/api/v4${path}`;
2028
+ const headers = {
2029
+ ...getAuthHeaders2(),
2030
+ "Content-Type": "application/json"
2031
+ };
2032
+ const res = await fetch(url, {
2033
+ method,
2034
+ headers,
2035
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
2036
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
2037
+ });
2038
+ if (!res.ok) {
2039
+ const text2 = await res.text();
2040
+ throw new Error(`GitLab API error (${res.status}): ${text2}`);
2041
+ }
2042
+ if (method === "DELETE" || res.status === 204) return void 0;
2043
+ return await res.json();
2044
+ }
2045
+ async function gitlabGet(path) {
2046
+ return gitlabFetch("GET", path);
2047
+ }
2048
+ async function gitlabPost(path, body) {
2049
+ return gitlabFetch("POST", path, body);
2050
+ }
2051
+ async function gitlabPut(path, body) {
2052
+ return gitlabFetch("PUT", path, body);
2053
+ }
2054
+ function priorityRank2(labels) {
2055
+ for (let i = 0; i < PRIORITY_LABELS2.length; i++) {
2056
+ const p = PRIORITY_LABELS2[i];
2057
+ if (p && labels.some((l) => l.toLowerCase() === p)) return i;
2058
+ }
2059
+ return PRIORITY_LABELS2.length;
2060
+ }
2061
+ function makeIssueId2(project, iid) {
2062
+ return `${project}#${iid}`;
2063
+ }
2064
+ function splitIssueId(id) {
2065
+ const hashIdx = id.lastIndexOf("#");
2066
+ if (hashIdx === -1) {
2067
+ return { project: "", iid: id };
2068
+ }
2069
+ return { project: id.slice(0, hashIdx), iid: id.slice(hashIdx + 1) };
2070
+ }
2071
+ var GitLabIssuesSource = class {
2072
+ name = "gitlab-issues";
2073
+ async fetchNextIssue(config2) {
2074
+ const project = parseGitLabProject(config2.team);
2075
+ const label = encodeURIComponent(config2.label);
2076
+ const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
2077
+ const issues = await gitlabGet(path);
2078
+ if (issues.length === 0) return null;
2079
+ const sorted = [...issues].sort((a, b) => {
2080
+ const pa = priorityRank2(a.labels);
2081
+ const pb = priorityRank2(b.labels);
2082
+ if (pa !== pb) return pa - pb;
2083
+ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
2084
+ });
2085
+ const issue2 = sorted[0];
2086
+ if (!issue2) return null;
2087
+ return {
2088
+ id: makeIssueId2(config2.team, issue2.iid),
2089
+ title: issue2.title,
2090
+ description: issue2.description ?? "",
2091
+ url: issue2.web_url
2092
+ };
2093
+ }
2094
+ async fetchIssueById(id) {
2095
+ const ref = parseGitLabIssueRef(id);
2096
+ try {
2097
+ const project = parseGitLabProject(ref.project);
2098
+ const issue2 = await gitlabGet(`/projects/${project}/issues/${ref.iid}`);
2099
+ return {
2100
+ id: makeIssueId2(ref.project, issue2.iid),
2101
+ title: issue2.title,
2102
+ description: issue2.description ?? "",
2103
+ url: issue2.web_url
2104
+ };
2105
+ } catch {
2106
+ return null;
2107
+ }
2108
+ }
2109
+ async updateStatus(issueId, labelToAdd) {
2110
+ const { project, iid } = splitIssueId(issueId);
2111
+ const encodedProject = parseGitLabProject(project);
2112
+ const issue2 = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
2113
+ const labels = [.../* @__PURE__ */ new Set([...issue2.labels, labelToAdd])];
2114
+ await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, { labels: labels.join(",") });
2115
+ }
2116
+ async attachPullRequest(issueId, prUrl) {
2117
+ const { project, iid } = splitIssueId(issueId);
2118
+ const encodedProject = parseGitLabProject(project);
2119
+ await gitlabPost(`/projects/${encodedProject}/issues/${iid}/notes`, {
2120
+ body: `Pull request: ${prUrl}`
2121
+ });
2122
+ }
2123
+ async completeIssue(issueId, _status, labelToRemove) {
2124
+ const { project, iid } = splitIssueId(issueId);
2125
+ const encodedProject = parseGitLabProject(project);
2126
+ const issue2 = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
2127
+ const labels = labelToRemove ? issue2.labels.filter((l) => l.toLowerCase() !== labelToRemove.toLowerCase()) : issue2.labels;
2128
+ await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
2129
+ state_event: "close",
2130
+ labels: labels.join(",")
2131
+ });
2132
+ }
2133
+ async listIssues(config2) {
2134
+ const project = parseGitLabProject(config2.team);
2135
+ const label = encodeURIComponent(config2.label);
2136
+ const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
2137
+ const issues = await gitlabGet(path);
2138
+ return issues.map((issue2) => ({
2139
+ id: makeIssueId2(config2.team, issue2.iid),
2140
+ title: issue2.title,
2141
+ description: issue2.description ?? "",
2142
+ url: issue2.web_url
2143
+ }));
2144
+ }
2145
+ async removeLabel(issueId, labelToRemove) {
2146
+ const { project, iid } = splitIssueId(issueId);
2147
+ const encodedProject = parseGitLabProject(project);
2148
+ const issue2 = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
2149
+ const filtered = issue2.labels.filter((l) => l.toLowerCase() !== labelToRemove.toLowerCase());
2150
+ if (filtered.length === issue2.labels.length) return;
2151
+ await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
2152
+ labels: filtered.join(",")
2153
+ });
2154
+ }
2155
+ };
2156
+ function parseGitLabProject(input) {
2157
+ if (/^\d+$/.test(input)) return input;
2158
+ return encodeURIComponent(input);
2159
+ }
2160
+ function parseGitLabIssueRef(input) {
2161
+ const urlMatch = input.match(/gitlab(?:\.com|[^/]*)\/(.+?)\/-\/issues\/(\d+)/);
2162
+ if (urlMatch?.[1] && urlMatch?.[2]) {
2163
+ return { project: urlMatch[1], iid: urlMatch[2] };
2164
+ }
2165
+ const hashIdx = input.lastIndexOf("#");
2166
+ if (hashIdx !== -1) {
2167
+ return { project: input.slice(0, hashIdx), iid: input.slice(hashIdx + 1) };
2168
+ }
2169
+ return { project: "", iid: input };
2170
+ }
2171
+
2172
+ // src/sources/jira.ts
2173
+ var REQUEST_TIMEOUT_MS3 = 3e4;
2174
+ var PRIORITY_RANK = {
2175
+ highest: 1,
2176
+ high: 2,
2177
+ medium: 3,
2178
+ low: 4,
2179
+ lowest: 5
2180
+ };
2181
+ function getBaseUrl2() {
2182
+ const url = process.env.JIRA_BASE_URL;
2183
+ if (!url) throw new Error("JIRA_BASE_URL is not set");
2184
+ return url.replace(/\/$/, "");
2185
+ }
2186
+ function getAuthHeader() {
2187
+ const email = process.env.JIRA_EMAIL;
2188
+ const token = process.env.JIRA_API_TOKEN;
2189
+ if (!email || !token) throw new Error("JIRA_EMAIL and JIRA_API_TOKEN must be set");
2190
+ const credentials = Buffer.from(`${email}:${token}`).toString("base64");
2191
+ return `Basic ${credentials}`;
2192
+ }
2193
+ async function jiraFetch(method, path, body) {
2194
+ const url = `${getBaseUrl2()}/rest/api/3${path}`;
2195
+ const res = await fetch(url, {
2196
+ method,
2197
+ headers: {
2198
+ Authorization: getAuthHeader(),
2199
+ "Content-Type": "application/json",
2200
+ Accept: "application/json"
2201
+ },
2202
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
2203
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
2204
+ });
2205
+ if (!res.ok) {
2206
+ const text2 = await res.text();
2207
+ throw new Error(`Jira API error (${res.status}): ${text2}`);
2208
+ }
2209
+ if (res.status === 204) return void 0;
2210
+ return await res.json();
2211
+ }
2212
+ async function jiraGet(path) {
2213
+ return jiraFetch("GET", path);
2214
+ }
2215
+ async function jiraPost(path, body) {
2216
+ return jiraFetch("POST", path, body);
2217
+ }
2218
+ async function jiraPut(path, body) {
2219
+ return jiraFetch("PUT", path, body);
2220
+ }
2221
+ function priorityRank3(issue2) {
2222
+ const name = issue2.fields.priority?.name?.toLowerCase() ?? "";
2223
+ return PRIORITY_RANK[name] ?? Number.MAX_SAFE_INTEGER;
2224
+ }
2225
+ function extractDescription(description) {
2226
+ if (!description) return "";
2227
+ if (typeof description === "string") return description;
2228
+ if (typeof description === "object") {
2229
+ return extractAdfText(description);
2230
+ }
2231
+ return "";
2232
+ }
2233
+ function extractAdfText(node) {
2234
+ if (node.type === "text" && typeof node.text === "string") return node.text;
2235
+ const parts = [];
2236
+ if (Array.isArray(node.content)) {
2237
+ for (const child of node.content) {
2238
+ const text2 = extractAdfText(child);
2239
+ if (text2) parts.push(text2);
2240
+ }
2241
+ }
2242
+ return parts.join("\n");
2243
+ }
2244
+ function issueUrl(baseUrl, key) {
2245
+ return `${baseUrl}/browse/${key}`;
2246
+ }
2247
+ var JiraSource = class {
2248
+ name = "jira";
2249
+ async fetchNextIssue(config2) {
2250
+ const jql = encodeURIComponent(
2251
+ `project = "${config2.team}" AND labels = "${config2.label}" AND status = "${config2.pick_from}" ORDER BY priority ASC, created ASC`
2252
+ );
2253
+ const fields = "summary,description,priority,status,labels";
2254
+ const data = await jiraGet(
2255
+ `/search?jql=${jql}&fields=${fields}&maxResults=50`
2256
+ );
2257
+ const issues = data.issues ?? [];
2258
+ if (issues.length === 0) return null;
2259
+ const sorted = [...issues].sort((a, b) => priorityRank3(a) - priorityRank3(b));
2260
+ const issue2 = sorted[0];
2261
+ if (!issue2) return null;
2262
+ const baseUrl = getBaseUrl2();
2263
+ return {
2264
+ id: issue2.key,
2265
+ title: issue2.fields.summary,
2266
+ description: extractDescription(issue2.fields.description),
2267
+ url: issueUrl(baseUrl, issue2.key)
2268
+ };
2269
+ }
2270
+ async fetchIssueById(id) {
2271
+ const key = parseJiraIdentifier(id);
2272
+ try {
2273
+ const issue2 = await jiraGet(
2274
+ `/issue/${key}?fields=summary,description,priority,status,labels`
2275
+ );
2276
+ const baseUrl = getBaseUrl2();
2277
+ return {
2278
+ id: issue2.key,
2279
+ title: issue2.fields.summary,
2280
+ description: extractDescription(issue2.fields.description),
2281
+ url: issueUrl(baseUrl, issue2.key)
2282
+ };
2283
+ } catch {
2284
+ return null;
2285
+ }
2286
+ }
2287
+ async updateStatus(issueId, statusName) {
2288
+ const key = parseJiraIdentifier(issueId);
2289
+ const data = await jiraGet(`/issue/${key}/transitions`);
2290
+ const transition = data.transitions.find(
2291
+ (t) => t.name.toLowerCase() === statusName.toLowerCase()
2292
+ );
2293
+ if (!transition) {
2294
+ const available = data.transitions.map((t) => t.name).join(", ");
2295
+ throw new Error(`Jira transition "${statusName}" not found. Available: ${available}`);
2296
+ }
2297
+ await jiraPost(`/issue/${key}/transitions`, { transition: { id: transition.id } });
2298
+ }
2299
+ async attachPullRequest(issueId, prUrl) {
2300
+ const key = parseJiraIdentifier(issueId);
2301
+ await jiraPost(`/issue/${key}/remotelink`, {
2302
+ object: {
2303
+ url: prUrl,
2304
+ title: "Pull Request",
2305
+ icon: {
2306
+ url16x16: "https://github.com/favicon.ico",
2307
+ title: "GitHub"
2308
+ }
2309
+ }
2310
+ });
2311
+ }
2312
+ async completeIssue(issueId, statusName, labelToRemove) {
2313
+ await this.updateStatus(issueId, statusName);
2314
+ if (labelToRemove) {
2315
+ await this.removeLabel(issueId, labelToRemove);
2316
+ }
2317
+ }
2318
+ async listIssues(config2) {
2319
+ const jql = encodeURIComponent(
2320
+ `project = "${config2.team}" AND labels = "${config2.label}" AND status = "${config2.pick_from}" ORDER BY priority ASC, created ASC`
2321
+ );
2322
+ const fields = "summary,description,priority,status,labels";
2323
+ const data = await jiraGet(
2324
+ `/search?jql=${jql}&fields=${fields}&maxResults=100`
2325
+ );
2326
+ const baseUrl = getBaseUrl2();
2327
+ return (data.issues ?? []).map((issue2) => ({
2328
+ id: issue2.key,
2329
+ title: issue2.fields.summary,
2330
+ description: extractDescription(issue2.fields.description),
2331
+ url: issueUrl(baseUrl, issue2.key)
2332
+ }));
2333
+ }
2334
+ async removeLabel(issueId, labelName) {
2335
+ const key = parseJiraIdentifier(issueId);
2336
+ const issue2 = await jiraGet(`/issue/${key}?fields=labels`);
2337
+ const currentLabels = issue2.fields.labels ?? [];
2338
+ const filtered = currentLabels.filter((l) => l.toLowerCase() !== labelName.toLowerCase());
2339
+ if (filtered.length === currentLabels.length) return;
2340
+ await jiraPut(`/issue/${key}`, { fields: { labels: filtered } });
2341
+ }
2342
+ };
2343
+ function parseJiraIdentifier(input) {
2344
+ const urlMatch = input.match(/\/browse\/([A-Z][A-Z0-9_]+-\d+)/);
2345
+ if (urlMatch?.[1]) return urlMatch[1];
2346
+ return input;
2347
+ }
2348
+
2349
+ // src/sources/linear.ts
2350
+ var API_URL2 = "https://api.linear.app/graphql";
2351
+ var REQUEST_TIMEOUT_MS4 = 3e4;
2352
+ function getApiKey() {
2353
+ const key = process.env.LINEAR_API_KEY;
2354
+ if (!key) throw new Error("LINEAR_API_KEY is not set");
2355
+ return key;
2356
+ }
2357
+ async function gql(query, variables) {
2358
+ const res = await fetch(API_URL2, {
2359
+ method: "POST",
2360
+ headers: {
2361
+ "Content-Type": "application/json",
2362
+ Authorization: getApiKey()
2363
+ },
2364
+ body: JSON.stringify({ query, variables }),
2365
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS4)
2366
+ });
2367
+ if (!res.ok) {
2368
+ const text2 = await res.text();
2369
+ throw new Error(`Linear API error (${res.status}): ${text2}`);
2370
+ }
1659
2371
  const json = await res.json();
1660
2372
  if (json.errors?.length) {
1661
2373
  throw new Error(`Linear GraphQL error: ${json.errors.map((e) => e.message).join(", ")}`);
@@ -1706,12 +2418,12 @@ var LinearSource = class {
1706
2418
  if (issues.length === 0) return null;
1707
2419
  const unblocked = [];
1708
2420
  const blocked = [];
1709
- for (const issue2 of issues) {
1710
- const activeBlockers = issue2.inverseRelations.nodes.filter((r) => r.type === "blocks").filter((r) => r.issue.state.type !== "completed" && r.issue.state.type !== "canceled").map((r) => r.issue.identifier);
2421
+ for (const issue3 of issues) {
2422
+ const activeBlockers = issue3.inverseRelations.nodes.filter((r) => r.type === "blocks").filter((r) => r.issue.state.type !== "completed" && r.issue.state.type !== "canceled").map((r) => r.issue.identifier);
1711
2423
  if (activeBlockers.length === 0) {
1712
- unblocked.push(issue2);
2424
+ unblocked.push(issue3);
1713
2425
  } else {
1714
- blocked.push({ identifier: issue2.identifier, blockers: activeBlockers });
2426
+ blocked.push({ identifier: issue3.identifier, blockers: activeBlockers });
1715
2427
  }
1716
2428
  }
1717
2429
  if (unblocked.length === 0) {
@@ -1728,13 +2440,13 @@ var LinearSource = class {
1728
2440
  const pb = b.priority === 0 ? 5 : b.priority;
1729
2441
  return pa - pb;
1730
2442
  });
1731
- const issue = unblocked[0];
1732
- if (!issue) return null;
2443
+ const issue2 = unblocked[0];
2444
+ if (!issue2) return null;
1733
2445
  return {
1734
- id: issue.identifier,
1735
- title: issue.title,
1736
- description: issue.description || "",
1737
- url: issue.url
2446
+ id: issue2.identifier,
2447
+ title: issue2.title,
2448
+ description: issue2.description || "",
2449
+ url: issue2.url
1738
2450
  };
1739
2451
  }
1740
2452
  async fetchIssueById(id) {
@@ -1850,6 +2562,40 @@ var LinearSource = class {
1850
2562
  );
1851
2563
  }
1852
2564
  }
2565
+ async listIssues(config2) {
2566
+ const data = await gql(
2567
+ `query($teamName: String!, $projectName: String!, $labelName: String!, $statusName: String!) {
2568
+ issues(
2569
+ filter: {
2570
+ team: { name: { eq: $teamName } }
2571
+ project: { name: { eq: $projectName } }
2572
+ labels: { name: { eq: $labelName } }
2573
+ state: { name: { eq: $statusName } }
2574
+ }
2575
+ first: 100
2576
+ ) {
2577
+ nodes {
2578
+ identifier
2579
+ title
2580
+ description
2581
+ url
2582
+ }
2583
+ }
2584
+ }`,
2585
+ {
2586
+ teamName: config2.team,
2587
+ projectName: config2.project,
2588
+ labelName: config2.label,
2589
+ statusName: config2.pick_from
2590
+ }
2591
+ );
2592
+ return data.issues.nodes.map((issue2) => ({
2593
+ id: issue2.identifier,
2594
+ title: issue2.title,
2595
+ description: issue2.description || "",
2596
+ url: issue2.url
2597
+ }));
2598
+ }
1853
2599
  async removeLabel(issueId, labelName) {
1854
2600
  const issueData = await gql(
1855
2601
  `query($identifier: String!) {
@@ -1887,10 +2633,386 @@ function parseLinearIdentifier(input) {
1887
2633
  return input;
1888
2634
  }
1889
2635
 
2636
+ // src/sources/plane.ts
2637
+ var DEFAULT_BASE_URL2 = "https://api.plane.so";
2638
+ var REQUEST_TIMEOUT_MS5 = 3e4;
2639
+ function getBaseUrl3() {
2640
+ return (process.env.PLANE_BASE_URL ?? DEFAULT_BASE_URL2).replace(/\/$/, "");
2641
+ }
2642
+ function getAppUrl() {
2643
+ const base = process.env.PLANE_BASE_URL ?? DEFAULT_BASE_URL2;
2644
+ if (base === DEFAULT_BASE_URL2 || base.replace(/\/$/, "") === DEFAULT_BASE_URL2) {
2645
+ return "https://app.plane.so";
2646
+ }
2647
+ return base.replace(/\/$/, "");
2648
+ }
2649
+ function getAuthHeaders3() {
2650
+ const token = process.env.PLANE_API_TOKEN;
2651
+ if (!token) throw new Error("PLANE_API_TOKEN must be set");
2652
+ return { "X-Api-Key": token };
2653
+ }
2654
+ async function planeFetch(method, path, body) {
2655
+ const url = `${getBaseUrl3()}/api/v1${path}`;
2656
+ const headers = {
2657
+ ...getAuthHeaders3(),
2658
+ "Content-Type": "application/json"
2659
+ };
2660
+ const res = await fetch(url, {
2661
+ method,
2662
+ headers,
2663
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
2664
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS5)
2665
+ });
2666
+ if (!res.ok) {
2667
+ const text2 = await res.text();
2668
+ throw new Error(`Plane API error (${res.status}): ${text2}`);
2669
+ }
2670
+ if (method === "DELETE" || res.status === 204) return void 0;
2671
+ return await res.json();
2672
+ }
2673
+ async function planeGet(path) {
2674
+ return planeFetch("GET", path);
2675
+ }
2676
+ async function planePatch(path, body) {
2677
+ return planeFetch("PATCH", path, body);
2678
+ }
2679
+ async function planePost(path, body) {
2680
+ return planeFetch("POST", path, body);
2681
+ }
2682
+ async function fetchAll(path) {
2683
+ const data = await planeGet(path);
2684
+ if (Array.isArray(data)) return data;
2685
+ return data.results ?? [];
2686
+ }
2687
+ var PRIORITY_ORDER = {
2688
+ urgent: 1,
2689
+ high: 2,
2690
+ medium: 3,
2691
+ low: 4,
2692
+ none: 5
2693
+ };
2694
+ function priorityRank4(priority) {
2695
+ return PRIORITY_ORDER[priority.toLowerCase()] ?? 5;
2696
+ }
2697
+ async function resolveProjectId(workspaceSlug, projectIdentifier) {
2698
+ const projects = await fetchAll(`/workspaces/${workspaceSlug}/projects/`);
2699
+ const project = projects.find(
2700
+ (p) => p.identifier.toLowerCase() === projectIdentifier.toLowerCase() || p.name.toLowerCase() === projectIdentifier.toLowerCase() || p.id === projectIdentifier
2701
+ );
2702
+ if (!project) {
2703
+ const available = projects.map((p) => `${p.name} (${p.identifier})`).join(", ");
2704
+ throw new Error(`Plane project "${projectIdentifier}" not found. Available: ${available}`);
2705
+ }
2706
+ return project.id;
2707
+ }
2708
+ async function resolveStateId(workspaceSlug, projectId, stateName) {
2709
+ const states = await fetchAll(
2710
+ `/workspaces/${workspaceSlug}/projects/${projectId}/states/`
2711
+ );
2712
+ const state = states.find((s) => s.name.toLowerCase() === stateName.toLowerCase());
2713
+ if (!state) {
2714
+ const available = states.map((s) => s.name).join(", ");
2715
+ throw new Error(`Plane state "${stateName}" not found. Available: ${available}`);
2716
+ }
2717
+ return state.id;
2718
+ }
2719
+ async function resolveLabelId(workspaceSlug, projectId, labelName) {
2720
+ const labels = await fetchAll(
2721
+ `/workspaces/${workspaceSlug}/projects/${projectId}/labels/`
2722
+ );
2723
+ const label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
2724
+ if (!label) {
2725
+ const available = labels.map((l) => l.name).join(", ");
2726
+ throw new Error(`Plane label "${labelName}" not found. Available: ${available}`);
2727
+ }
2728
+ return label.id;
2729
+ }
2730
+ async function fetchLabels(workspaceSlug, projectId) {
2731
+ return fetchAll(`/workspaces/${workspaceSlug}/projects/${projectId}/labels/`);
2732
+ }
2733
+ function makeIssueId3(workspaceSlug, projectId, issueId) {
2734
+ return `${workspaceSlug}::${projectId}::${issueId}`;
2735
+ }
2736
+ function parseIssueId(id) {
2737
+ const urlMatch = id.match(/\/([^/]+)\/projects\/([^/]+)\/issues\/([^/?#]+)/);
2738
+ if (urlMatch?.[1] && urlMatch?.[2] && urlMatch?.[3]) {
2739
+ return { workspaceSlug: urlMatch[1], projectId: urlMatch[2], issueId: urlMatch[3] };
2740
+ }
2741
+ const parts = id.split("::");
2742
+ if (parts.length === 3 && parts[0] && parts[1] && parts[2]) {
2743
+ return { workspaceSlug: parts[0], projectId: parts[1], issueId: parts[2] };
2744
+ }
2745
+ throw new Error(
2746
+ `Cannot parse Plane issue ID: "${id}". Expected URL or "workspace::projectId::issueId" format.`
2747
+ );
2748
+ }
2749
+ var PlaneSource = class {
2750
+ name = "plane";
2751
+ async fetchNextIssue(config2) {
2752
+ const workspaceSlug = config2.team;
2753
+ const projectId = await resolveProjectId(workspaceSlug, config2.project);
2754
+ const stateId = await resolveStateId(workspaceSlug, projectId, config2.pick_from);
2755
+ const labelId = await resolveLabelId(workspaceSlug, projectId, config2.label);
2756
+ const data = await planeGet(
2757
+ `/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${stateId}&per_page=100`
2758
+ );
2759
+ const matching = data.results.filter((i) => i.label_ids.includes(labelId));
2760
+ if (matching.length === 0) return null;
2761
+ const sorted = [...matching].sort(
2762
+ (a, b) => priorityRank4(a.priority) - priorityRank4(b.priority)
2763
+ );
2764
+ const issue2 = sorted[0];
2765
+ if (!issue2) return null;
2766
+ const webUrl = `${getAppUrl()}/${workspaceSlug}/projects/${projectId}/issues/${issue2.id}`;
2767
+ return {
2768
+ id: makeIssueId3(workspaceSlug, projectId, issue2.id),
2769
+ title: issue2.name,
2770
+ description: issue2.description_stripped ?? "",
2771
+ url: webUrl
2772
+ };
2773
+ }
2774
+ async fetchIssueById(id) {
2775
+ try {
2776
+ const { workspaceSlug, projectId, issueId } = parseIssueId(id);
2777
+ const issue2 = await planeGet(
2778
+ `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`
2779
+ );
2780
+ const webUrl = `${getAppUrl()}/${workspaceSlug}/projects/${projectId}/issues/${issue2.id}`;
2781
+ return {
2782
+ id: makeIssueId3(workspaceSlug, projectId, issue2.id),
2783
+ title: issue2.name,
2784
+ description: issue2.description_stripped ?? "",
2785
+ url: webUrl
2786
+ };
2787
+ } catch {
2788
+ return null;
2789
+ }
2790
+ }
2791
+ async updateStatus(issueId, stateName) {
2792
+ const { workspaceSlug, projectId, issueId: planeIssueId } = parseIssueId(issueId);
2793
+ const stateId = await resolveStateId(workspaceSlug, projectId, stateName);
2794
+ await planePatch(
2795
+ `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${planeIssueId}/`,
2796
+ { state: stateId }
2797
+ );
2798
+ }
2799
+ async attachPullRequest(issueId, prUrl) {
2800
+ const { workspaceSlug, projectId, issueId: planeIssueId } = parseIssueId(issueId);
2801
+ await planePost(
2802
+ `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${planeIssueId}/comments/`,
2803
+ { comment_html: `<p>Pull request: <a href="${prUrl}">${prUrl}</a></p>` }
2804
+ );
2805
+ }
2806
+ async completeIssue(issueId, stateName, labelToRemove) {
2807
+ await this.updateStatus(issueId, stateName);
2808
+ if (labelToRemove) {
2809
+ await this.removeLabel(issueId, labelToRemove);
2810
+ }
2811
+ }
2812
+ async listIssues(config2) {
2813
+ const workspaceSlug = config2.team;
2814
+ const projectId = await resolveProjectId(workspaceSlug, config2.project);
2815
+ const stateId = await resolveStateId(workspaceSlug, projectId, config2.pick_from);
2816
+ const labelId = await resolveLabelId(workspaceSlug, projectId, config2.label);
2817
+ const data = await planeGet(
2818
+ `/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${stateId}&per_page=100`
2819
+ );
2820
+ return data.results.filter((i) => i.label_ids.includes(labelId)).map((i) => {
2821
+ const webUrl = `${getAppUrl()}/${workspaceSlug}/projects/${projectId}/issues/${i.id}`;
2822
+ return {
2823
+ id: makeIssueId3(workspaceSlug, projectId, i.id),
2824
+ title: i.name,
2825
+ description: i.description_stripped ?? "",
2826
+ url: webUrl
2827
+ };
2828
+ });
2829
+ }
2830
+ async removeLabel(issueId, labelName) {
2831
+ const { workspaceSlug, projectId, issueId: planeIssueId } = parseIssueId(issueId);
2832
+ const issue2 = await planeGet(
2833
+ `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${planeIssueId}/`
2834
+ );
2835
+ const labels = await fetchLabels(workspaceSlug, projectId);
2836
+ const labelObj = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
2837
+ if (!labelObj || !issue2.label_ids.includes(labelObj.id)) return;
2838
+ const updatedLabelIds = issue2.label_ids.filter((lid) => lid !== labelObj.id);
2839
+ await planePatch(
2840
+ `/workspaces/${workspaceSlug}/projects/${projectId}/issues/${planeIssueId}/`,
2841
+ { label_ids: updatedLabelIds }
2842
+ );
2843
+ }
2844
+ };
2845
+
2846
+ // src/sources/shortcut.ts
2847
+ var API_BASE_URL = "https://api.app.shortcut.com";
2848
+ var REQUEST_TIMEOUT_MS6 = 3e4;
2849
+ function getAuthHeaders4() {
2850
+ const token = process.env.SHORTCUT_API_TOKEN;
2851
+ if (!token) throw new Error("SHORTCUT_API_TOKEN must be set");
2852
+ return { "Shortcut-Token": token, "Content-Type": "application/json" };
2853
+ }
2854
+ async function shortcutFetch(method, path, body) {
2855
+ const url = `${API_BASE_URL}${path}`;
2856
+ const res = await fetch(url, {
2857
+ method,
2858
+ headers: getAuthHeaders4(),
2859
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
2860
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS6)
2861
+ });
2862
+ if (!res.ok) {
2863
+ const text2 = await res.text();
2864
+ throw new Error(`Shortcut API error (${res.status}): ${text2}`);
2865
+ }
2866
+ if (method === "DELETE" || res.status === 204) return void 0;
2867
+ return await res.json();
2868
+ }
2869
+ async function shortcutGet(path) {
2870
+ return shortcutFetch("GET", path);
2871
+ }
2872
+ async function shortcutPost(path, body) {
2873
+ return shortcutFetch("POST", path, body);
2874
+ }
2875
+ async function shortcutPut(path, body) {
2876
+ return shortcutFetch("PUT", path, body);
2877
+ }
2878
+ async function resolveWorkflowStateId(stateName) {
2879
+ const workflows = await shortcutGet("/api/v3/workflows");
2880
+ for (const workflow of workflows) {
2881
+ const state = workflow.states.find((s) => s.name.toLowerCase() === stateName.toLowerCase());
2882
+ if (state) return state.id;
2883
+ }
2884
+ const allStates = workflows.flatMap((w) => w.states.map((s) => s.name));
2885
+ throw new Error(
2886
+ `Shortcut workflow state "${stateName}" not found. Available: ${allStates.join(", ")}`
2887
+ );
2888
+ }
2889
+ async function resolveAllWorkflowStateIds(stateName) {
2890
+ const workflows = await shortcutGet("/api/v3/workflows");
2891
+ const ids = [];
2892
+ for (const workflow of workflows) {
2893
+ for (const state of workflow.states) {
2894
+ if (state.name.toLowerCase() === stateName.toLowerCase()) {
2895
+ ids.push(state.id);
2896
+ }
2897
+ }
2898
+ }
2899
+ if (ids.length === 0) {
2900
+ const allStates = workflows.flatMap((w) => w.states.map((s) => s.name));
2901
+ throw new Error(
2902
+ `Shortcut workflow state "${stateName}" not found. Available: ${allStates.join(", ")}`
2903
+ );
2904
+ }
2905
+ return ids;
2906
+ }
2907
+ async function resolveLabelId2(labelName) {
2908
+ const labels = await shortcutGet("/api/v3/labels");
2909
+ const label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase() && !l.archived);
2910
+ if (!label) {
2911
+ const available = labels.filter((l) => !l.archived).map((l) => l.name).join(", ");
2912
+ throw new Error(`Shortcut label "${labelName}" not found. Available: ${available}`);
2913
+ }
2914
+ return label.id;
2915
+ }
2916
+ function priorityRank5(priority) {
2917
+ if (priority === null) return Number.MAX_SAFE_INTEGER;
2918
+ return priority;
2919
+ }
2920
+ var ShortcutSource = class {
2921
+ name = "shortcut";
2922
+ async fetchNextIssue(config2) {
2923
+ const stateIds = await resolveAllWorkflowStateIds(config2.pick_from);
2924
+ const labelId = await resolveLabelId2(config2.label);
2925
+ const searchResult = await shortcutPost("/api/v3/stories/search", {
2926
+ workflow_state_ids: stateIds,
2927
+ label_ids: [labelId],
2928
+ archived: false
2929
+ });
2930
+ const stories = searchResult.data ?? [];
2931
+ if (stories.length === 0) return null;
2932
+ const sorted = [...stories].sort((a, b) => {
2933
+ const pa = priorityRank5(a.priority);
2934
+ const pb = priorityRank5(b.priority);
2935
+ if (pa !== pb) return pa - pb;
2936
+ return a.position - b.position;
2937
+ });
2938
+ const story = sorted[0];
2939
+ if (!story) return null;
2940
+ return {
2941
+ id: String(story.id),
2942
+ title: story.name,
2943
+ description: story.description ?? "",
2944
+ url: story.app_url
2945
+ };
2946
+ }
2947
+ async fetchIssueById(id) {
2948
+ const storyId = parseShortcutIdentifier(id);
2949
+ try {
2950
+ const story = await shortcutGet(`/api/v3/stories/${storyId}`);
2951
+ return {
2952
+ id: String(story.id),
2953
+ title: story.name,
2954
+ description: story.description ?? "",
2955
+ url: story.app_url
2956
+ };
2957
+ } catch {
2958
+ return null;
2959
+ }
2960
+ }
2961
+ async updateStatus(storyId, stateName) {
2962
+ const stateId = await resolveWorkflowStateId(stateName);
2963
+ await shortcutPut(`/api/v3/stories/${storyId}`, {
2964
+ workflow_state_id: stateId
2965
+ });
2966
+ }
2967
+ async attachPullRequest(storyId, prUrl) {
2968
+ await shortcutPost(`/api/v3/stories/${storyId}/comments`, {
2969
+ text: `Pull request: ${prUrl}`
2970
+ });
2971
+ }
2972
+ async completeIssue(storyId, stateName, labelToRemove) {
2973
+ await this.updateStatus(storyId, stateName);
2974
+ if (labelToRemove) {
2975
+ await this.removeLabel(storyId, labelToRemove);
2976
+ }
2977
+ }
2978
+ async listIssues(config2) {
2979
+ const stateIds = await resolveAllWorkflowStateIds(config2.pick_from);
2980
+ const labelId = await resolveLabelId2(config2.label);
2981
+ const searchResult = await shortcutPost("/api/v3/stories/search", {
2982
+ workflow_state_ids: stateIds,
2983
+ label_ids: [labelId],
2984
+ archived: false
2985
+ });
2986
+ return (searchResult.data ?? []).map((story) => ({
2987
+ id: String(story.id),
2988
+ title: story.name,
2989
+ description: story.description ?? "",
2990
+ url: story.app_url
2991
+ }));
2992
+ }
2993
+ async removeLabel(storyId, labelName) {
2994
+ const story = await shortcutGet(`/api/v3/stories/${storyId}`);
2995
+ const labels = await shortcutGet("/api/v3/labels");
2996
+ const label = labels.find(
2997
+ (l) => l.name.toLowerCase() === labelName.toLowerCase() && !l.archived
2998
+ );
2999
+ if (!label || !story.label_ids.includes(label.id)) return;
3000
+ const updatedLabelIds = story.label_ids.filter((lid) => lid !== label.id);
3001
+ await shortcutPut(`/api/v3/stories/${storyId}`, {
3002
+ label_ids: updatedLabelIds
3003
+ });
3004
+ }
3005
+ };
3006
+ function parseShortcutIdentifier(input) {
3007
+ const urlMatch = input.match(/\/story\/(\d+)/);
3008
+ if (urlMatch?.[1]) return urlMatch[1];
3009
+ return input;
3010
+ }
3011
+
1890
3012
  // src/sources/trello.ts
1891
3013
  var API_URL3 = "https://api.trello.com/1";
1892
- var REQUEST_TIMEOUT_MS3 = 3e4;
1893
- function getAuthHeaders() {
3014
+ var REQUEST_TIMEOUT_MS7 = 3e4;
3015
+ function getAuthHeaders5() {
1894
3016
  const key = process.env.TRELLO_API_KEY;
1895
3017
  const token = process.env.TRELLO_TOKEN;
1896
3018
  if (!key || !token) throw new Error("TRELLO_API_KEY and TRELLO_TOKEN must be set");
@@ -1903,8 +3025,8 @@ async function trelloFetch(method, path, params = "") {
1903
3025
  const url = `${API_URL3}${path}${sep}${params}`;
1904
3026
  const res = await fetch(url, {
1905
3027
  method,
1906
- headers: getAuthHeaders(),
1907
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
3028
+ headers: getAuthHeaders5(),
3029
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS7)
1908
3030
  });
1909
3031
  if (!res.ok) {
1910
3032
  const text2 = await res.text();
@@ -1998,210 +3120,58 @@ var TrelloSource = class {
1998
3120
  await this.removeLabel(cardId, labelToRemove);
1999
3121
  }
2000
3122
  }
2001
- async removeLabel(cardId, labelName) {
2002
- const card = await trelloGet(
2003
- `/cards/${cardId}`,
2004
- "fields=idBoard,idLabels"
2005
- );
2006
- const label = await findLabelByName(card.idBoard, labelName);
2007
- if (!card.idLabels.includes(label.id)) return;
2008
- await trelloDelete(`/cards/${cardId}/idLabels/${label.id}`);
2009
- }
2010
- };
2011
- function parseTrelloIdentifier(input) {
2012
- const urlMatch = input.match(/\/c\/([a-zA-Z0-9]+)/);
2013
- if (urlMatch?.[1]) return urlMatch[1];
2014
- return input;
2015
- }
2016
-
2017
- // src/sources/index.ts
2018
- var sources = {
2019
- linear: () => new LinearSource(),
2020
- trello: () => new TrelloSource()
2021
- };
2022
- function createSource(name) {
2023
- const factory = sources[name];
2024
- if (!factory) {
2025
- throw new Error(`Unknown source: ${name}. Available: ${Object.keys(sources).join(", ")}`);
2026
- }
2027
- return factory();
2028
- }
2029
-
2030
- // src/terminal.ts
2031
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2032
- var SPINNER_INTERVAL_MS = 80;
2033
- var spinnerTimer = null;
2034
- var spinnerFrame = 0;
2035
- function isTTY() {
2036
- return process.stdout.isTTY === true;
2037
- }
2038
- function writeOSC(title) {
2039
- process.stdout.write(`\x1B]0;${title}\x07`);
2040
- }
2041
- function setTitle(title) {
2042
- if (!isTTY()) return;
2043
- writeOSC(title);
2044
- }
2045
- function startSpinner(message) {
2046
- if (!isTTY()) return;
2047
- stopSpinner();
2048
- spinnerFrame = 0;
2049
- writeOSC(`${SPINNER_FRAMES[0]} Lisa \u2014 ${message}`);
2050
- spinnerTimer = setInterval(() => {
2051
- spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
2052
- writeOSC(`${SPINNER_FRAMES[spinnerFrame]} Lisa \u2014 ${message}`);
2053
- }, SPINNER_INTERVAL_MS);
2054
- }
2055
- function stopSpinner(message) {
2056
- if (spinnerTimer) {
2057
- clearInterval(spinnerTimer);
2058
- spinnerTimer = null;
2059
- }
2060
- if (!isTTY()) return;
2061
- if (message) {
2062
- writeOSC(message);
2063
- }
2064
- }
2065
- function notify() {
2066
- if (!isTTY()) return;
2067
- process.stdout.write("\x07");
2068
- }
2069
- function resetTitle() {
2070
- if (!isTTY()) return;
2071
- writeOSC("");
2072
- }
2073
-
2074
- // src/worktree.ts
2075
- import { appendFileSync as appendFileSync7, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
2076
- import { join as join8, resolve as resolve4 } from "path";
2077
- import { execa as execa2 } from "execa";
2078
- var WORKTREES_DIR = ".worktrees";
2079
- function generateBranchName(issueId, title) {
2080
- const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").substring(0, 40);
2081
- return `feat/${issueId.toLowerCase()}-${slug}`;
2082
- }
2083
- async function cleanupOrphanedWorktree(repoRoot, branchName) {
2084
- const { stdout: branchList } = await execa2("git", ["branch", "--list", branchName], {
2085
- cwd: repoRoot,
2086
- reject: false
2087
- });
2088
- if (!branchList.trim()) {
2089
- return false;
2090
- }
2091
- const worktreePath = join8(repoRoot, WORKTREES_DIR, branchName);
2092
- const { stdout: worktreeList } = await execa2("git", ["worktree", "list", "--porcelain"], {
2093
- cwd: repoRoot,
2094
- reject: false
2095
- });
2096
- if (worktreeList.includes(worktreePath)) {
2097
- await execa2("git", ["worktree", "remove", worktreePath, "--force"], { cwd: repoRoot });
2098
- await execa2("git", ["worktree", "prune"], { cwd: repoRoot });
2099
- }
2100
- await execa2("git", ["branch", "-D", branchName], { cwd: repoRoot });
2101
- return true;
2102
- }
2103
- async function createWorktree(repoRoot, branchName, baseBranch) {
2104
- const worktreePath = join8(repoRoot, WORKTREES_DIR, branchName);
2105
- await cleanupOrphanedWorktree(repoRoot, branchName);
2106
- await execa2("git", ["fetch", "origin", baseBranch], { cwd: repoRoot });
2107
- await execa2("git", ["worktree", "add", "-b", branchName, worktreePath, `origin/${baseBranch}`], {
2108
- cwd: repoRoot
2109
- });
2110
- return worktreePath;
2111
- }
2112
- async function removeWorktree(repoRoot, worktreePath) {
2113
- await execa2("git", ["worktree", "remove", worktreePath, "--force"], {
2114
- cwd: repoRoot
2115
- });
2116
- await execa2("git", ["worktree", "prune"], { cwd: repoRoot });
2117
- }
2118
- function ensureWorktreeGitignore(repoRoot) {
2119
- const gitignorePath = join8(repoRoot, ".gitignore");
2120
- if (!existsSync5(gitignorePath)) {
2121
- appendFileSync7(gitignorePath, `${WORKTREES_DIR}
2122
- `);
2123
- return;
2124
- }
2125
- const content = readFileSync4(gitignorePath, "utf-8");
2126
- if (!content.split("\n").some((line) => line.trim() === WORKTREES_DIR)) {
2127
- const separator = content.endsWith("\n") ? "" : "\n";
2128
- appendFileSync7(gitignorePath, `${separator}${WORKTREES_DIR}
2129
- `);
2130
- }
2131
- }
2132
- async function findBranchByIssueId(repoRoot, issueId) {
2133
- const needle = issueId.toLowerCase();
2134
- const { stdout: local } = await execa2(
2135
- "git",
2136
- ["for-each-ref", "--sort=-committerdate", "--format=%(refname:short)", "refs/heads/"],
2137
- { cwd: repoRoot }
2138
- );
2139
- const localMatch = local.split("\n").map((b) => b.trim()).filter(Boolean).find((b) => b.toLowerCase().includes(needle));
2140
- if (localMatch) return localMatch;
2141
- const { stdout: remote } = await execa2(
2142
- "git",
2143
- ["for-each-ref", "--sort=-committerdate", "--format=%(refname:short)", "refs/remotes/origin/"],
2144
- { cwd: repoRoot }
2145
- );
2146
- const remoteMatch = remote.split("\n").map((b) => b.trim()).filter(Boolean).find((b) => b.toLowerCase().includes(needle));
2147
- if (remoteMatch) return remoteMatch.replace("origin/", "");
2148
- const { stdout: lsRemote } = await execa2("git", ["ls-remote", "--heads", "origin"], {
2149
- cwd: repoRoot
2150
- });
2151
- const lsMatch = lsRemote.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => l.split(" ")[1]?.replace("refs/heads/", "") ?? "").find((b) => b.toLowerCase().includes(needle));
2152
- if (lsMatch) return lsMatch;
2153
- return void 0;
2154
- }
2155
- function determineRepoPath(repos, issue, workspace) {
2156
- if (repos.length === 0) return void 0;
2157
- if (issue.repo) {
2158
- const match = repos.find((r) => r.name === issue.repo);
2159
- if (match) return join8(workspace, match.path);
2160
- }
2161
- for (const r of repos) {
2162
- if (r.match && issue.title.startsWith(r.match)) {
2163
- return join8(workspace, r.path);
2164
- }
2165
- }
2166
- const first = repos[0];
2167
- return first ? join8(workspace, first.path) : void 0;
2168
- }
2169
- async function detectFeatureBranches(repos, issueId, workspace, globalBaseBranch) {
2170
- const entries = repos.length > 0 ? repos.map((r) => ({ path: resolve4(workspace, r.path), baseBranch: r.base_branch })) : [{ path: workspace, baseBranch: globalBaseBranch }];
2171
- const needle = issueId.toLowerCase();
2172
- const results = [];
2173
- const matched = /* @__PURE__ */ new Set();
2174
- const currentBranches = [];
2175
- for (const entry of entries) {
2176
- try {
2177
- const { stdout } = await execa2("git", ["branch", "--show-current"], { cwd: entry.path });
2178
- const current = stdout.trim();
2179
- currentBranches.push({ ...entry, current });
2180
- if (current?.toLowerCase().includes(needle)) {
2181
- results.push({ repoPath: entry.path, branch: current });
2182
- matched.add(entry.path);
2183
- }
2184
- } catch {
2185
- }
3123
+ async listIssues(config2) {
3124
+ const board = await findBoardByName(config2.team);
3125
+ const list = await findListByName(board.id, config2.pick_from);
3126
+ const label = await findLabelByName(board.id, config2.label);
3127
+ const cards = await trelloGet(
3128
+ `/lists/${list.id}/cards`,
3129
+ "fields=name,desc,url,idLabels,idList"
3130
+ );
3131
+ return cards.filter((c) => c.idLabels.includes(label.id)).map((c) => ({
3132
+ id: c.id,
3133
+ title: c.name,
3134
+ description: c.desc || "",
3135
+ url: c.url
3136
+ }));
2186
3137
  }
2187
- for (const entry of currentBranches) {
2188
- if (!matched.has(entry.path) && entry.current && entry.current !== entry.baseBranch) {
2189
- results.push({ repoPath: entry.path, branch: entry.current });
2190
- matched.add(entry.path);
2191
- }
3138
+ async removeLabel(cardId, labelName) {
3139
+ const card = await trelloGet(
3140
+ `/cards/${cardId}`,
3141
+ "fields=idBoard,idLabels"
3142
+ );
3143
+ const label = await findLabelByName(card.idBoard, labelName);
3144
+ if (!card.idLabels.includes(label.id)) return;
3145
+ await trelloDelete(`/cards/${cardId}/idLabels/${label.id}`);
2192
3146
  }
2193
- for (const entry of entries) {
2194
- if (matched.has(entry.path)) continue;
2195
- const branch = await findBranchByIssueId(entry.path, issueId);
2196
- if (branch) {
2197
- results.push({ repoPath: entry.path, branch });
2198
- }
3147
+ };
3148
+ function parseTrelloIdentifier(input) {
3149
+ const urlMatch = input.match(/\/c\/([a-zA-Z0-9]+)/);
3150
+ if (urlMatch?.[1]) return urlMatch[1];
3151
+ return input;
3152
+ }
3153
+
3154
+ // src/sources/index.ts
3155
+ var sources = {
3156
+ linear: () => new LinearSource(),
3157
+ trello: () => new TrelloSource(),
3158
+ plane: () => new PlaneSource(),
3159
+ shortcut: () => new ShortcutSource(),
3160
+ "gitlab-issues": () => new GitLabIssuesSource(),
3161
+ "github-issues": () => new GitHubIssuesSource(),
3162
+ jira: () => new JiraSource()
3163
+ };
3164
+ function createSource(name) {
3165
+ const factory = sources[name];
3166
+ if (!factory) {
3167
+ throw new Error(`Unknown source: ${name}. Available: ${Object.keys(sources).join(", ")}`);
2199
3168
  }
2200
- return results;
3169
+ return factory();
2201
3170
  }
2202
3171
 
2203
3172
  // src/loop.ts
2204
3173
  var activeCleanup = null;
3174
+ var activeProviderPid = null;
2205
3175
  var shuttingDown = false;
2206
3176
  function resolveModels(config2) {
2207
3177
  if (!config2.models || config2.models.length === 0) {
@@ -2215,44 +3185,23 @@ function resolveModels(config2) {
2215
3185
  );
2216
3186
  }
2217
3187
  }
3188
+ if (config2.provider === "cursor") {
3189
+ const hasAuto = config2.models.some((m) => m.toLowerCase() === "auto");
3190
+ if (!hasAuto) {
3191
+ warn(
3192
+ "Cursor Free plan detected (or model not set to 'auto'). Forcing 'auto' model. Set model to 'auto' explicitly in .lisa/config.yaml to silence this warning."
3193
+ );
3194
+ return [{ provider: config2.provider, model: "auto" }];
3195
+ }
3196
+ }
2218
3197
  return config2.models.map((m) => ({
2219
3198
  provider: config2.provider,
2220
3199
  model: m === config2.provider ? void 0 : m
2221
3200
  }));
2222
3201
  }
2223
- function buildPrBody(providerUsed, description) {
2224
- const lines = [];
2225
- if (description) {
2226
- const sanitized = sanitizePrBody(description);
2227
- if (sanitized) {
2228
- lines.push("## Summary", "", sanitized, "");
2229
- }
2230
- }
2231
- lines.push(
2232
- "---",
2233
- "",
2234
- `Implemented by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerUsed}**.`
2235
- );
2236
- return lines.join("\n");
2237
- }
2238
- var PR_TITLE_FILE = ".pr-title";
2239
- function readPrTitle(cwd) {
2240
- try {
2241
- const title = readFileSync5(join9(cwd, PR_TITLE_FILE), "utf-8").trim().split("\n")[0]?.trim();
2242
- return title || null;
2243
- } catch {
2244
- return null;
2245
- }
2246
- }
2247
- function cleanupPrTitle(cwd) {
2248
- try {
2249
- unlinkSync6(join9(cwd, PR_TITLE_FILE));
2250
- } catch {
2251
- }
2252
- }
2253
3202
  var PLAN_FILE = ".lisa-plan.json";
2254
3203
  function readLisaPlan(dir) {
2255
- const planPath = join9(dir, PLAN_FILE);
3204
+ const planPath = join11(dir, PLAN_FILE);
2256
3205
  if (!existsSync6(planPath)) return null;
2257
3206
  try {
2258
3207
  return JSON.parse(readFileSync5(planPath, "utf-8").trim());
@@ -2262,13 +3211,13 @@ function readLisaPlan(dir) {
2262
3211
  }
2263
3212
  function cleanupPlan(dir) {
2264
3213
  try {
2265
- unlinkSync6(join9(dir, PLAN_FILE));
3214
+ unlinkSync8(join11(dir, PLAN_FILE));
2266
3215
  } catch {
2267
3216
  }
2268
3217
  }
2269
3218
  var MANIFEST_FILE = ".lisa-manifest.json";
2270
3219
  function readLisaManifest(dir) {
2271
- const manifestPath = join9(dir, MANIFEST_FILE);
3220
+ const manifestPath = join11(dir, MANIFEST_FILE);
2272
3221
  if (!existsSync6(manifestPath)) return null;
2273
3222
  try {
2274
3223
  return JSON.parse(readFileSync5(manifestPath, "utf-8").trim());
@@ -2278,62 +3227,10 @@ function readLisaManifest(dir) {
2278
3227
  }
2279
3228
  function cleanupManifest(dir) {
2280
3229
  try {
2281
- unlinkSync6(join9(dir, MANIFEST_FILE));
3230
+ unlinkSync8(join11(dir, MANIFEST_FILE));
2282
3231
  } catch {
2283
3232
  }
2284
3233
  }
2285
- var MAX_PUSH_RETRIES = 2;
2286
- var HOOK_ERROR_PATTERNS = [
2287
- /husky - pre-push/i,
2288
- /husky - pre-commit/i,
2289
- /pre-push hook/i,
2290
- /pre-commit hook/i,
2291
- /hook declined/i,
2292
- /hook.*failed/i,
2293
- /hook.*exited with/i,
2294
- /hook.*returned.*exit code/i
2295
- ];
2296
- function isHookError(errorMessage) {
2297
- return HOOK_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
2298
- }
2299
- async function pushWithRecovery(opts) {
2300
- for (let attempt = 0; attempt <= MAX_PUSH_RETRIES; attempt++) {
2301
- try {
2302
- await execa3("git", ["push", "-u", "origin", opts.branch], { cwd: opts.cwd });
2303
- return { success: true };
2304
- } catch (err) {
2305
- const errorMessage = err instanceof Error ? err.message : String(err);
2306
- if (!isHookError(errorMessage)) {
2307
- return { success: false, error: errorMessage };
2308
- }
2309
- if (attempt >= MAX_PUSH_RETRIES) {
2310
- return {
2311
- success: false,
2312
- error: `Push hook failed after ${MAX_PUSH_RETRIES} recovery attempts: ${errorMessage}`
2313
- };
2314
- }
2315
- warn(
2316
- `Push hook failed (attempt ${attempt + 1}/${MAX_PUSH_RETRIES}). Re-invoking provider to fix...`
2317
- );
2318
- const recoveryPrompt = buildPushRecoveryPrompt(errorMessage);
2319
- const result = await runWithFallback(opts.models, recoveryPrompt, {
2320
- logFile: opts.logFile,
2321
- cwd: opts.cwd,
2322
- guardrailsDir: opts.guardrailsDir,
2323
- issueId: opts.issueId,
2324
- overseer: opts.overseer
2325
- });
2326
- if (!result.success) {
2327
- return {
2328
- success: false,
2329
- error: `Provider failed to fix push hook errors: ${result.output}`
2330
- };
2331
- }
2332
- ok("Provider finished recovery. Retrying push...");
2333
- }
2334
- }
2335
- return { success: false, error: "Push recovery exhausted retries" };
2336
- }
2337
3234
  function installSignalHandlers() {
2338
3235
  const cleanup = async (signal) => {
2339
3236
  if (shuttingDown) {
@@ -2344,6 +3241,12 @@ function installSignalHandlers() {
2344
3241
  stopSpinner();
2345
3242
  resetTitle();
2346
3243
  warn(`Received ${signal}. Reverting active issue...`);
3244
+ if (activeProviderPid) {
3245
+ try {
3246
+ process.kill(activeProviderPid, "SIGTERM");
3247
+ } catch {
3248
+ }
3249
+ }
2347
3250
  if (activeCleanup) {
2348
3251
  const { issueId, previousStatus, source } = activeCleanup;
2349
3252
  try {
@@ -2409,6 +3312,15 @@ async function runLoop(config2, opts) {
2409
3312
  if (!opts.dryRun) {
2410
3313
  await recoverOrphanIssues(source, config2);
2411
3314
  }
3315
+ if (kanbanEmitter.listenerCount("issue:queued") > 0) {
3316
+ try {
3317
+ const allIssues = await source.listIssues(config2.source_config);
3318
+ for (const issue2 of allIssues) {
3319
+ kanbanEmitter.emit("issue:queued", issue2);
3320
+ }
3321
+ } catch {
3322
+ }
3323
+ }
2412
3324
  let session = 0;
2413
3325
  while (true) {
2414
3326
  session++;
@@ -2441,9 +3353,9 @@ async function runLoop(config2, opts) {
2441
3353
  log("[dry-run] Then implement, push, create PR, and update issue status");
2442
3354
  break;
2443
3355
  }
2444
- let issue;
3356
+ let issue2;
2445
3357
  try {
2446
- issue = opts.issueId ? await source.fetchIssueById(opts.issueId) : await source.fetchNextIssue(config2.source_config);
3358
+ issue2 = opts.issueId ? await source.fetchIssueById(opts.issueId) : await source.fetchNextIssue(config2.source_config);
2447
3359
  } catch (err) {
2448
3360
  stopSpinner();
2449
3361
  error(`Failed to fetch issues: ${err instanceof Error ? err.message : String(err)}`);
@@ -2453,7 +3365,7 @@ async function runLoop(config2, opts) {
2453
3365
  continue;
2454
3366
  }
2455
3367
  stopSpinner();
2456
- if (!issue) {
3368
+ if (!issue2) {
2457
3369
  if (opts.issueId) {
2458
3370
  error(`Issue '${opts.issueId}' not found.`);
2459
3371
  } else {
@@ -2461,28 +3373,29 @@ async function runLoop(config2, opts) {
2461
3373
  }
2462
3374
  break;
2463
3375
  }
2464
- ok(`Picked up: ${issue.id} \u2014 ${issue.title}`);
2465
- setTitle(`Lisa \u2014 ${issue.id}`);
3376
+ ok(`Picked up: ${issue2.id} \u2014 ${issue2.title}`);
3377
+ setTitle(`Lisa \u2014 ${issue2.id}`);
2466
3378
  const previousStatus = config2.source_config.pick_from;
2467
3379
  try {
2468
3380
  const inProgress = config2.source_config.in_progress;
2469
- await source.updateStatus(issue.id, inProgress);
2470
- ok(`Moved ${issue.id} to "${inProgress}"`);
3381
+ kanbanEmitter.emit("issue:started", issue2.id);
3382
+ await source.updateStatus(issue2.id, inProgress);
3383
+ ok(`Moved ${issue2.id} to "${inProgress}"`);
2471
3384
  } catch (err) {
2472
3385
  warn(`Failed to update status: ${err instanceof Error ? err.message : String(err)}`);
2473
3386
  }
2474
- activeCleanup = { issueId: issue.id, previousStatus, source };
3387
+ activeCleanup = { issueId: issue2.id, previousStatus, source };
2475
3388
  let sessionResult;
2476
3389
  try {
2477
- sessionResult = config2.workflow === "worktree" ? await runWorktreeSession(config2, issue, logFile, session, models) : await runBranchSession(config2, issue, logFile, session, models);
3390
+ sessionResult = config2.workflow === "worktree" ? await runWorktreeSession(config2, issue2, logFile, session, models) : await runBranchSession(config2, issue2, logFile, session, models);
2478
3391
  } catch (err) {
2479
3392
  stopSpinner();
2480
3393
  error(
2481
- `Unhandled error in session for ${issue.id}: ${err instanceof Error ? err.message : String(err)}`
3394
+ `Unhandled error in session for ${issue2.id}: ${err instanceof Error ? err.message : String(err)}`
2482
3395
  );
2483
3396
  try {
2484
- await source.updateStatus(issue.id, previousStatus);
2485
- ok(`Reverted ${issue.id} to "${previousStatus}"`);
3397
+ await source.updateStatus(issue2.id, previousStatus);
3398
+ ok(`Reverted ${issue2.id} to "${previousStatus}"`);
2486
3399
  } catch (revertErr) {
2487
3400
  error(
2488
3401
  `Failed to revert status: ${revertErr instanceof Error ? revertErr.message : String(revertErr)}`
@@ -2497,11 +3410,12 @@ async function runLoop(config2, opts) {
2497
3410
  continue;
2498
3411
  }
2499
3412
  if (!sessionResult.success) {
2500
- error(`All models failed for ${issue.id}. Reverting to "${previousStatus}".`);
3413
+ error(`All models failed for ${issue2.id}. Reverting to "${previousStatus}".`);
2501
3414
  logAttemptHistory(sessionResult);
2502
3415
  try {
2503
- await source.updateStatus(issue.id, previousStatus);
2504
- ok(`Reverted ${issue.id} to "${previousStatus}"`);
3416
+ await source.updateStatus(issue2.id, previousStatus);
3417
+ ok(`Reverted ${issue2.id} to "${previousStatus}"`);
3418
+ kanbanEmitter.emit("issue:reverted", issue2.id);
2505
3419
  } catch (err) {
2506
3420
  error(
2507
3421
  `Failed to revert status: ${err instanceof Error ? err.message : String(err)}`
@@ -2527,11 +3441,12 @@ async function runLoop(config2, opts) {
2527
3441
  ok(`Completed with provider: ${sessionResult.providerUsed}`);
2528
3442
  if (sessionResult.prUrls.length === 0) {
2529
3443
  warn(
2530
- `Session succeeded but no PRs created for ${issue.id}. Reverting to "${previousStatus}".`
3444
+ `Session succeeded but no PRs created for ${issue2.id}. Reverting to "${previousStatus}".`
2531
3445
  );
2532
3446
  try {
2533
- await source.updateStatus(issue.id, previousStatus);
2534
- ok(`Reverted ${issue.id} to "${previousStatus}"`);
3447
+ await source.updateStatus(issue2.id, previousStatus);
3448
+ ok(`Reverted ${issue2.id} to "${previousStatus}"`);
3449
+ kanbanEmitter.emit("issue:reverted", issue2.id);
2535
3450
  } catch (err) {
2536
3451
  error(
2537
3452
  `Failed to revert status: ${err instanceof Error ? err.message : String(err)}`
@@ -2550,8 +3465,8 @@ async function runLoop(config2, opts) {
2550
3465
  }
2551
3466
  for (const prUrl of sessionResult.prUrls) {
2552
3467
  try {
2553
- await source.attachPullRequest(issue.id, prUrl);
2554
- ok(`Attached PR to ${issue.id}`);
3468
+ await source.attachPullRequest(issue2.id, prUrl);
3469
+ ok(`Attached PR to ${issue2.id}`);
2555
3470
  } catch (err) {
2556
3471
  warn(`Failed to attach PR: ${err instanceof Error ? err.message : String(err)}`);
2557
3472
  }
@@ -2559,16 +3474,19 @@ async function runLoop(config2, opts) {
2559
3474
  try {
2560
3475
  const doneStatus = config2.source_config.done;
2561
3476
  const labelToRemove = opts.issueId ? void 0 : config2.source_config.label;
2562
- await source.completeIssue(issue.id, doneStatus, labelToRemove);
2563
- ok(`Updated ${issue.id} status to "${doneStatus}"`);
3477
+ await source.completeIssue(issue2.id, doneStatus, labelToRemove);
3478
+ ok(`Updated ${issue2.id} status to "${doneStatus}"`);
3479
+ for (const prUrl of sessionResult.prUrls) {
3480
+ kanbanEmitter.emit("issue:done", issue2.id, prUrl);
3481
+ }
2564
3482
  if (labelToRemove) {
2565
- ok(`Removed label "${labelToRemove}" from ${issue.id}`);
3483
+ ok(`Removed label "${labelToRemove}" from ${issue2.id}`);
2566
3484
  }
2567
3485
  } catch (err) {
2568
3486
  error(`Failed to complete issue: ${err instanceof Error ? err.message : String(err)}`);
2569
3487
  }
2570
3488
  activeCleanup = null;
2571
- stopSpinner(`\u2713 Lisa \u2014 ${issue.id} \u2014 PR created`);
3489
+ stopSpinner(`\u2713 Lisa \u2014 ${issue2.id} \u2014 PR created`);
2572
3490
  notify();
2573
3491
  if (opts.once) {
2574
3492
  log("Single iteration mode. Exiting.");
@@ -2594,31 +3512,17 @@ function resolveBaseBranch(config2, repoPath) {
2594
3512
  const repo = config2.repos.find((r) => resolve5(workspace, r.path) === repoPath);
2595
3513
  return repo?.base_branch ?? config2.base_branch;
2596
3514
  }
2597
- function findRepoConfig(config2, issue) {
3515
+ function findRepoConfig(config2, issue2) {
2598
3516
  if (config2.repos.length === 0) return void 0;
2599
- if (issue.repo) {
2600
- const match = config2.repos.find((r) => r.name === issue.repo);
3517
+ if (issue2.repo) {
3518
+ const match = config2.repos.find((r) => r.name === issue2.repo);
2601
3519
  if (match) return match;
2602
3520
  }
2603
3521
  for (const r of config2.repos) {
2604
- if (r.match && issue.title.startsWith(r.match)) return r;
3522
+ if (r.match && issue2.title.startsWith(r.match)) return r;
2605
3523
  }
2606
3524
  return config2.repos[0];
2607
3525
  }
2608
- async function runTestValidation(cwd) {
2609
- const testRunner = detectTestRunner(cwd);
2610
- if (!testRunner) return true;
2611
- log(`Running test validation (${testRunner} detected)...`);
2612
- try {
2613
- await execa3("npm", ["run", "test"], { cwd, stdio: "pipe" });
2614
- ok("Tests passed.");
2615
- return true;
2616
- } catch (err) {
2617
- const message = err instanceof Error ? err.message : String(err);
2618
- error(`Tests failed: ${message}`);
2619
- return false;
2620
- }
2621
- }
2622
3526
  async function findWorktreeForBranch(repoRoot, branch) {
2623
3527
  try {
2624
3528
  const { stdout } = await execa3("git", ["worktree", "list", "--porcelain"], { cwd: repoRoot });
@@ -2637,19 +3541,19 @@ async function findWorktreeForBranch(repoRoot, branch) {
2637
3541
  return null;
2638
3542
  }
2639
3543
  }
2640
- async function runWorktreeSession(config2, issue, logFile, session, models) {
3544
+ async function runWorktreeSession(config2, issue2, logFile, session, models) {
2641
3545
  if (config2.repos.length > 1) {
2642
- return runWorktreeMultiRepoSession(config2, issue, logFile, session, models);
3546
+ return runWorktreeMultiRepoSession(config2, issue2, logFile, session, models);
2643
3547
  }
2644
3548
  const workspace = resolve5(config2.workspace);
2645
- const repoPath = determineRepoPath(config2.repos, issue, workspace) ?? workspace;
3549
+ const repoPath = determineRepoPath(config2.repos, issue2, workspace) ?? workspace;
2646
3550
  const defaultBranch = resolveBaseBranch(config2, repoPath);
2647
3551
  const primaryProvider = createProvider(models[0]?.provider ?? "claude");
2648
3552
  const useNativeWorktree = primaryProvider.supportsNativeWorktree === true;
2649
3553
  if (useNativeWorktree) {
2650
3554
  return runNativeWorktreeSession(
2651
3555
  config2,
2652
- issue,
3556
+ issue2,
2653
3557
  logFile,
2654
3558
  session,
2655
3559
  models,
@@ -2657,43 +3561,47 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
2657
3561
  defaultBranch
2658
3562
  );
2659
3563
  }
2660
- return runManualWorktreeSession(config2, issue, logFile, session, models, repoPath, defaultBranch);
3564
+ return runManualWorktreeSession(config2, issue2, logFile, session, models, repoPath, defaultBranch);
2661
3565
  }
2662
- async function runNativeWorktreeSession(config2, issue, logFile, session, models, repoPath, defaultBranch) {
3566
+ async function runNativeWorktreeSession(config2, issue2, logFile, session, models, repoPath, _defaultBranch) {
2663
3567
  const failResult = (providerUsed, fallback) => ({
2664
3568
  success: false,
2665
3569
  providerUsed,
2666
3570
  prUrls: [],
2667
3571
  fallback: fallback ?? { success: false, output: "", duration: 0, providerUsed, attempts: [] }
2668
3572
  });
2669
- const repo = findRepoConfig(config2, issue);
3573
+ const repo = findRepoConfig(config2, issue2);
2670
3574
  if (repo?.lifecycle) {
2671
- startSpinner(`${issue.id} \u2014 starting resources...`);
3575
+ startSpinner(`${issue2.id} \u2014 starting resources...`);
2672
3576
  const started = await startResources(repo, repoPath);
2673
3577
  stopSpinner();
2674
3578
  if (!started) {
2675
- error(`Lifecycle startup failed for ${issue.id}. Aborting session.`);
3579
+ error(`Lifecycle startup failed for ${issue2.id}. Aborting session.`);
2676
3580
  return failResult(models[0]?.provider ?? "claude");
2677
3581
  }
2678
3582
  }
2679
3583
  const testRunner = detectTestRunner(repoPath);
2680
3584
  if (testRunner) log(`Detected test runner: ${testRunner}`);
3585
+ const pm = detectPackageManager(repoPath);
2681
3586
  cleanupManifest(repoPath);
2682
- const prompt = buildNativeWorktreePrompt(issue, repoPath, testRunner);
2683
- startSpinner(`${issue.id} \u2014 implementing (native worktree)...`);
2684
- log(`Implementing with native worktree... (log: ${logFile})`);
3587
+ const prompt = buildNativeWorktreePrompt(issue2, repoPath, testRunner, pm, _defaultBranch);
2685
3588
  initLogFile(logFile);
3589
+ startSpinner(`${issue2.id} \u2014 implementing (native worktree)...`);
3590
+ log(`Implementing with native worktree... (log: ${logFile})`);
2686
3591
  const result = await runWithFallback(models, prompt, {
2687
3592
  logFile,
2688
3593
  cwd: repoPath,
2689
3594
  guardrailsDir: repoPath,
2690
- issueId: issue.id,
3595
+ issueId: issue2.id,
2691
3596
  overseer: config2.overseer,
2692
- useNativeWorktree: true
3597
+ useNativeWorktree: true,
3598
+ onProcess: (pid) => {
3599
+ activeProviderPid = pid;
3600
+ }
2693
3601
  });
2694
3602
  stopSpinner();
2695
3603
  try {
2696
- appendFileSync8(
3604
+ appendFileSync10(
2697
3605
  logFile,
2698
3606
  `
2699
3607
  ${"=".repeat(80)}
@@ -2706,80 +3614,34 @@ ${result.output}
2706
3614
  }
2707
3615
  if (repo?.lifecycle) await stopResources();
2708
3616
  if (!result.success) {
2709
- error(`Session ${session} failed for ${issue.id}. Check ${logFile}`);
3617
+ error(`Session ${session} failed for ${issue2.id}. Check ${logFile}`);
2710
3618
  cleanupManifest(repoPath);
2711
3619
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2712
3620
  }
2713
3621
  const manifest = readLisaManifest(repoPath);
2714
- if (!manifest?.branch) {
2715
- error(`Agent did not produce a valid .lisa-manifest.json for ${issue.id}. Aborting.`);
2716
- cleanupManifest(repoPath);
2717
- return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2718
- }
2719
- const effectiveBranch = manifest.branch;
2720
- ok(`Agent created branch: ${effectiveBranch}`);
2721
- const worktreePath = await findWorktreeForBranch(repoPath, effectiveBranch);
2722
- const effectiveCwd = worktreePath ?? repoPath;
2723
- if (!worktreePath) {
2724
- warn(`No worktree found for branch ${effectiveBranch} \u2014 using repo root`);
2725
- }
2726
- startSpinner(`${issue.id} \u2014 validating tests...`);
2727
- const testsPassed = await runTestValidation(effectiveCwd);
2728
- stopSpinner();
2729
- if (!testsPassed) {
2730
- error(`Tests failed for ${issue.id}. Blocking PR creation.`);
2731
- cleanupManifest(repoPath);
2732
- if (worktreePath) await cleanupWorktree(repoPath, worktreePath);
2733
- return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2734
- }
2735
- startSpinner(`${issue.id} \u2014 pushing...`);
2736
- const pushResult = await pushWithRecovery({
2737
- branch: effectiveBranch,
2738
- cwd: effectiveCwd,
2739
- models,
2740
- logFile,
2741
- guardrailsDir: repoPath,
2742
- issueId: issue.id,
2743
- overseer: config2.overseer
2744
- });
2745
- stopSpinner();
2746
- if (!pushResult.success) {
2747
- error(`Failed to push branch to remote: ${pushResult.error}`);
2748
- cleanupManifest(repoPath);
2749
- if (worktreePath) await cleanupWorktree(repoPath, worktreePath);
2750
- return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2751
- }
2752
- startSpinner(`${issue.id} \u2014 creating PR...`);
2753
- const prTitle = manifest.prTitle ?? issue.title;
2754
- const prBody = manifest.prBody;
2755
3622
  cleanupManifest(repoPath);
2756
- const prUrls = [];
2757
- try {
2758
- const repoInfo = await getRepoInfo(effectiveCwd);
2759
- const pr = await createPullRequest(
2760
- {
2761
- owner: repoInfo.owner,
2762
- repo: repoInfo.repo,
2763
- head: effectiveBranch,
2764
- base: defaultBranch,
2765
- title: prTitle,
2766
- body: buildPrBody(result.providerUsed, prBody)
2767
- },
2768
- config2.github
3623
+ if (!manifest?.prUrl) {
3624
+ error(
3625
+ `Agent did not produce a .lisa-manifest.json with prUrl for ${issue2.id}. Aborting.`
2769
3626
  );
2770
- ok(`PR created: ${pr.html_url}`);
2771
- prUrls.push(pr.html_url);
2772
- } catch (err) {
2773
- error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
3627
+ const worktreePath2 = manifest?.branch ? await findWorktreeForBranch(repoPath, manifest.branch) : null;
3628
+ if (worktreePath2) await cleanupWorktree(repoPath, worktreePath2);
3629
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2774
3630
  }
2775
- stopSpinner();
3631
+ const worktreePath = await findWorktreeForBranch(repoPath, manifest.branch ?? "");
3632
+ ok(`PR created by provider: ${manifest.prUrl}`);
2776
3633
  if (worktreePath) await cleanupWorktree(repoPath, worktreePath);
2777
- ok(`Session ${session} complete for ${issue.id}`);
2778
- return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
3634
+ ok(`Session ${session} complete for ${issue2.id}`);
3635
+ return {
3636
+ success: true,
3637
+ providerUsed: result.providerUsed,
3638
+ prUrls: [manifest.prUrl],
3639
+ fallback: result
3640
+ };
2779
3641
  }
2780
- async function runManualWorktreeSession(config2, issue, logFile, session, models, repoPath, defaultBranch) {
2781
- const branchName = generateBranchName(issue.id, issue.title);
2782
- startSpinner(`${issue.id} \u2014 creating worktree...`);
3642
+ async function runManualWorktreeSession(config2, issue2, logFile, session, models, repoPath, defaultBranch) {
3643
+ const branchName = generateBranchName(issue2.id, issue2.title);
3644
+ startSpinner(`${issue2.id} \u2014 creating worktree...`);
2783
3645
  log(`Creating worktree for ${branchName} (base: ${defaultBranch})...`);
2784
3646
  let worktreePath;
2785
3647
  try {
@@ -2802,13 +3664,13 @@ async function runManualWorktreeSession(config2, issue, logFile, session, models
2802
3664
  }
2803
3665
  stopSpinner();
2804
3666
  ok(`Worktree created at ${worktreePath}`);
2805
- const repo = findRepoConfig(config2, issue);
3667
+ const repo = findRepoConfig(config2, issue2);
2806
3668
  if (repo?.lifecycle) {
2807
- startSpinner(`${issue.id} \u2014 starting resources...`);
3669
+ startSpinner(`${issue2.id} \u2014 starting resources...`);
2808
3670
  const started = await startResources(repo, worktreePath);
2809
3671
  stopSpinner();
2810
3672
  if (!started) {
2811
- error(`Lifecycle startup failed for ${issue.id}. Aborting session.`);
3673
+ error(`Lifecycle startup failed for ${issue2.id}. Aborting session.`);
2812
3674
  await cleanupWorktree(repoPath, worktreePath);
2813
3675
  return {
2814
3676
  success: false,
@@ -2828,20 +3690,24 @@ async function runManualWorktreeSession(config2, issue, logFile, session, models
2828
3690
  if (testRunner) {
2829
3691
  log(`Detected test runner: ${testRunner}`);
2830
3692
  }
2831
- const prompt = buildImplementPrompt(issue, config2, testRunner);
2832
- startSpinner(`${issue.id} \u2014 implementing...`);
2833
- log(`Implementing in worktree... (log: ${logFile})`);
3693
+ const pm = detectPackageManager(worktreePath);
3694
+ const prompt = buildImplementPrompt(issue2, config2, testRunner, pm);
2834
3695
  initLogFile(logFile);
3696
+ startSpinner(`${issue2.id} \u2014 implementing...`);
3697
+ log(`Implementing in worktree... (log: ${logFile})`);
2835
3698
  const result = await runWithFallback(models, prompt, {
2836
3699
  logFile,
2837
3700
  cwd: worktreePath,
2838
3701
  guardrailsDir: repoPath,
2839
- issueId: issue.id,
2840
- overseer: config2.overseer
3702
+ issueId: issue2.id,
3703
+ overseer: config2.overseer,
3704
+ onProcess: (pid) => {
3705
+ activeProviderPid = pid;
3706
+ }
2841
3707
  });
2842
3708
  stopSpinner();
2843
3709
  try {
2844
- appendFileSync8(
3710
+ appendFileSync10(
2845
3711
  logFile,
2846
3712
  `
2847
3713
  ${"=".repeat(80)}
@@ -2856,96 +3722,50 @@ ${result.output}
2856
3722
  await stopResources();
2857
3723
  }
2858
3724
  if (!result.success) {
2859
- error(`Session ${session} failed for ${issue.id}. Check ${logFile}`);
2860
- await cleanupWorktree(repoPath, worktreePath);
2861
- return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2862
- }
2863
- startSpinner(`${issue.id} \u2014 validating tests...`);
2864
- const testsPassed = await runTestValidation(worktreePath);
2865
- stopSpinner();
2866
- if (!testsPassed) {
2867
- error(`Tests failed for ${issue.id}. Blocking PR creation.`);
3725
+ error(`Session ${session} failed for ${issue2.id}. Check ${logFile}`);
2868
3726
  await cleanupWorktree(repoPath, worktreePath);
2869
3727
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2870
3728
  }
2871
3729
  const manifest = readLisaManifest(worktreePath);
2872
- let effectiveBranch = branchName;
2873
- if (manifest?.branch && manifest.branch !== branchName) {
2874
- log(`Renaming branch to English name: ${manifest.branch}`);
2875
- try {
2876
- await execa3("git", ["branch", "-m", branchName, manifest.branch], { cwd: worktreePath });
2877
- effectiveBranch = manifest.branch;
2878
- ok(`Branch renamed to ${effectiveBranch}`);
2879
- } catch (err) {
2880
- warn(
2881
- `Branch rename failed, using original: ${err instanceof Error ? err.message : String(err)}`
2882
- );
2883
- }
2884
- }
2885
- startSpinner(`${issue.id} \u2014 pushing...`);
2886
- const pushResult = await pushWithRecovery({
2887
- branch: effectiveBranch,
2888
- cwd: worktreePath,
2889
- models,
2890
- logFile,
2891
- guardrailsDir: repoPath,
2892
- issueId: issue.id,
2893
- overseer: config2.overseer
2894
- });
2895
- stopSpinner();
2896
- if (!pushResult.success) {
2897
- error(`Failed to push branch to remote: ${pushResult.error}`);
2898
- cleanupManifest(worktreePath);
2899
- await cleanupWorktree(repoPath, worktreePath);
2900
- return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2901
- }
2902
- startSpinner(`${issue.id} \u2014 creating PR...`);
2903
- const prTitle = manifest?.prTitle ?? readPrTitle(worktreePath) ?? issue.title;
2904
- const prBody = manifest?.prBody;
2905
- cleanupPrTitle(worktreePath);
2906
3730
  cleanupManifest(worktreePath);
2907
- const prUrls = [];
2908
- try {
2909
- const repoInfo = await getRepoInfo(worktreePath);
2910
- const pr = await createPullRequest(
2911
- {
2912
- owner: repoInfo.owner,
2913
- repo: repoInfo.repo,
2914
- head: effectiveBranch,
2915
- base: defaultBranch,
2916
- title: prTitle,
2917
- body: buildPrBody(result.providerUsed, prBody)
2918
- },
2919
- config2.github
3731
+ if (!manifest?.prUrl) {
3732
+ error(
3733
+ `Agent did not produce a .lisa-manifest.json with prUrl for ${issue2.id}. Aborting.`
2920
3734
  );
2921
- ok(`PR created: ${pr.html_url}`);
2922
- prUrls.push(pr.html_url);
2923
- } catch (err) {
2924
- error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
3735
+ await cleanupWorktree(repoPath, worktreePath);
3736
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
2925
3737
  }
2926
- stopSpinner();
3738
+ ok(`PR created by provider: ${manifest.prUrl}`);
2927
3739
  await cleanupWorktree(repoPath, worktreePath);
2928
- ok(`Session ${session} complete for ${issue.id}`);
2929
- return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
3740
+ ok(`Session ${session} complete for ${issue2.id}`);
3741
+ return {
3742
+ success: true,
3743
+ providerUsed: result.providerUsed,
3744
+ prUrls: [manifest.prUrl],
3745
+ fallback: result
3746
+ };
2930
3747
  }
2931
- async function runWorktreeMultiRepoSession(config2, issue, logFile, session, models) {
3748
+ async function runWorktreeMultiRepoSession(config2, issue2, logFile, session, models) {
2932
3749
  const workspace = resolve5(config2.workspace);
2933
3750
  cleanupManifest(workspace);
2934
3751
  cleanupPlan(workspace);
2935
- startSpinner(`${issue.id} \u2014 analyzing issue...`);
2936
- log(`Multi-repo planning phase for ${issue.id}`);
2937
3752
  initLogFile(logFile);
2938
- const planPrompt = buildPlanningPrompt(issue, config2);
3753
+ startSpinner(`${issue2.id} \u2014 analyzing issue...`);
3754
+ log(`Multi-repo planning phase for ${issue2.id}`);
3755
+ const planPrompt = buildPlanningPrompt(issue2, config2);
2939
3756
  const planResult = await runWithFallback(models, planPrompt, {
2940
3757
  logFile,
2941
3758
  cwd: workspace,
2942
3759
  guardrailsDir: workspace,
2943
- issueId: issue.id,
2944
- overseer: config2.overseer
3760
+ issueId: issue2.id,
3761
+ overseer: config2.overseer,
3762
+ onProcess: (pid) => {
3763
+ activeProviderPid = pid;
3764
+ }
2945
3765
  });
2946
3766
  stopSpinner();
2947
3767
  try {
2948
- appendFileSync8(
3768
+ appendFileSync10(
2949
3769
  logFile,
2950
3770
  `
2951
3771
  ${"=".repeat(80)}
@@ -2956,7 +3776,7 @@ ${planResult.output}
2956
3776
  } catch {
2957
3777
  }
2958
3778
  if (!planResult.success) {
2959
- error(`Planning phase failed for ${issue.id}. Check ${logFile}`);
3779
+ error(`Planning phase failed for ${issue2.id}. Check ${logFile}`);
2960
3780
  cleanupPlan(workspace);
2961
3781
  return {
2962
3782
  success: false,
@@ -2967,7 +3787,7 @@ ${planResult.output}
2967
3787
  }
2968
3788
  const plan = readLisaPlan(workspace);
2969
3789
  if (!plan?.steps || plan.steps.length === 0) {
2970
- error(`Agent did not produce a valid .lisa-plan.json for ${issue.id}. Aborting.`);
3790
+ error(`Agent did not produce a valid .lisa-plan.json for ${issue2.id}. Aborting.`);
2971
3791
  cleanupPlan(workspace);
2972
3792
  return {
2973
3793
  success: false,
@@ -2987,16 +3807,18 @@ ${planResult.output}
2987
3807
  let lastProvider = planResult.providerUsed;
2988
3808
  for (const [i, step] of sortedSteps.entries()) {
2989
3809
  const stepNum = i + 1;
3810
+ const isLastStep = i === sortedSteps.length - 1;
2990
3811
  divider(stepNum);
2991
3812
  log(`Step ${stepNum}/${sortedSteps.length}: ${step.repoPath} \u2014 ${step.scope}`);
2992
3813
  const stepResult = await runMultiRepoStep(
2993
3814
  config2,
2994
- issue,
3815
+ issue2,
2995
3816
  step,
2996
3817
  previousResults,
2997
3818
  logFile,
2998
3819
  models,
2999
- stepNum
3820
+ stepNum,
3821
+ isLastStep
3000
3822
  );
3001
3823
  lastFallback = stepResult.fallback;
3002
3824
  lastProvider = stepResult.providerUsed;
@@ -3018,20 +3840,20 @@ ${planResult.output}
3018
3840
  prUrl: stepResult.prUrl
3019
3841
  });
3020
3842
  }
3021
- ok(`Session ${session} complete for ${issue.id} \u2014 ${prUrls.length} PR(s) created`);
3843
+ ok(`Session ${session} complete for ${issue2.id} \u2014 ${prUrls.length} PR(s) created`);
3022
3844
  return { success: true, providerUsed: lastProvider, prUrls, fallback: lastFallback };
3023
3845
  }
3024
- async function runMultiRepoStep(config2, issue, step, previousResults, logFile, models, stepNum) {
3846
+ async function runMultiRepoStep(config2, issue2, step, previousResults, logFile, models, stepNum, isLastStep) {
3025
3847
  const repoPath = step.repoPath;
3026
3848
  const defaultBranch = resolveBaseBranch(config2, repoPath);
3027
- const branchName = generateBranchName(issue.id, issue.title);
3849
+ const branchName = generateBranchName(issue2.id, issue2.title);
3028
3850
  const failResult = (providerUsed, fallback) => ({
3029
3851
  success: false,
3030
3852
  providerUsed,
3031
3853
  branch: branchName,
3032
3854
  fallback: fallback ?? { success: false, output: "", duration: 0, providerUsed, attempts: [] }
3033
3855
  });
3034
- startSpinner(`${issue.id} step ${stepNum} \u2014 creating worktree...`);
3856
+ startSpinner(`${issue2.id} step ${stepNum} \u2014 creating worktree...`);
3035
3857
  let worktreePath;
3036
3858
  try {
3037
3859
  worktreePath = await createWorktree(repoPath, branchName, defaultBranch);
@@ -3044,18 +3866,42 @@ async function runMultiRepoStep(config2, issue, step, previousResults, logFile,
3044
3866
  ok(`Worktree created at ${worktreePath}`);
3045
3867
  const testRunner = detectTestRunner(worktreePath);
3046
3868
  if (testRunner) log(`Detected test runner: ${testRunner}`);
3047
- const prompt = buildScopedImplementPrompt(issue, step, previousResults, testRunner);
3048
- startSpinner(`${issue.id} step ${stepNum} \u2014 implementing...`);
3869
+ const pm = detectPackageManager(worktreePath);
3870
+ const repoConfig = config2.repos.find((r) => resolve5(config2.workspace, r.path) === step.repoPath);
3871
+ if (repoConfig?.lifecycle) {
3872
+ startSpinner(`${issue2.id} step ${stepNum} \u2014 starting resources...`);
3873
+ const started = await startResources(repoConfig, worktreePath);
3874
+ stopSpinner();
3875
+ if (!started) {
3876
+ error(`Lifecycle startup failed for step ${stepNum}. Aborting.`);
3877
+ await cleanupWorktree(repoPath, worktreePath);
3878
+ return failResult(models[0]?.provider ?? "claude");
3879
+ }
3880
+ }
3881
+ const prompt = buildScopedImplementPrompt(
3882
+ issue2,
3883
+ step,
3884
+ previousResults,
3885
+ testRunner,
3886
+ pm,
3887
+ isLastStep,
3888
+ defaultBranch
3889
+ );
3890
+ startSpinner(`${issue2.id} step ${stepNum} \u2014 implementing...`);
3049
3891
  const result = await runWithFallback(models, prompt, {
3050
3892
  logFile,
3051
3893
  cwd: worktreePath,
3052
3894
  guardrailsDir: repoPath,
3053
- issueId: issue.id,
3054
- overseer: config2.overseer
3895
+ issueId: issue2.id,
3896
+ overseer: config2.overseer,
3897
+ onProcess: (pid) => {
3898
+ activeProviderPid = pid;
3899
+ }
3055
3900
  });
3056
3901
  stopSpinner();
3902
+ if (repoConfig?.lifecycle) await stopResources();
3057
3903
  try {
3058
- appendFileSync8(
3904
+ appendFileSync10(
3059
3905
  logFile,
3060
3906
  `
3061
3907
  ${"=".repeat(80)}
@@ -3071,92 +3917,39 @@ ${result.output}
3071
3917
  return { ...failResult(result.providerUsed, result), branch: branchName };
3072
3918
  }
3073
3919
  const manifest = readLisaManifest(worktreePath);
3074
- let effectiveBranch = branchName;
3075
- if (manifest?.branch && manifest.branch !== branchName) {
3076
- log(`Renaming branch to: ${manifest.branch}`);
3077
- try {
3078
- await execa3("git", ["branch", "-m", branchName, manifest.branch], { cwd: worktreePath });
3079
- effectiveBranch = manifest.branch;
3080
- } catch (err) {
3081
- warn(`Branch rename failed: ${err instanceof Error ? err.message : String(err)}`);
3082
- }
3083
- }
3084
- startSpinner(`${issue.id} step ${stepNum} \u2014 validating tests...`);
3085
- const testsPassed = await runTestValidation(worktreePath);
3086
- stopSpinner();
3087
- if (!testsPassed) {
3088
- error(`Tests failed for step ${stepNum}. Blocking PR creation.`);
3089
- cleanupManifest(worktreePath);
3090
- await cleanupWorktree(repoPath, worktreePath);
3091
- return { ...failResult(result.providerUsed, result), branch: effectiveBranch };
3092
- }
3093
- startSpinner(`${issue.id} step ${stepNum} \u2014 pushing...`);
3094
- const pushResult = await pushWithRecovery({
3095
- branch: effectiveBranch,
3096
- cwd: worktreePath,
3097
- models,
3098
- logFile,
3099
- guardrailsDir: repoPath,
3100
- issueId: issue.id,
3101
- overseer: config2.overseer
3102
- });
3103
- stopSpinner();
3104
- if (!pushResult.success) {
3105
- error(`Failed to push step ${stepNum}: ${pushResult.error}`);
3106
- cleanupManifest(worktreePath);
3107
- await cleanupWorktree(repoPath, worktreePath);
3108
- return { ...failResult(result.providerUsed, result), branch: effectiveBranch };
3109
- }
3110
- startSpinner(`${issue.id} step ${stepNum} \u2014 creating PR...`);
3111
- const prTitle = manifest?.prTitle ?? issue.title;
3112
- const prBody = manifest?.prBody;
3113
3920
  cleanupManifest(worktreePath);
3114
- let prUrl;
3115
- try {
3116
- const repoInfo = await getRepoInfo(worktreePath);
3117
- const pr = await createPullRequest(
3118
- {
3119
- owner: repoInfo.owner,
3120
- repo: repoInfo.repo,
3121
- head: effectiveBranch,
3122
- base: defaultBranch,
3123
- title: prTitle,
3124
- body: buildPrBody(result.providerUsed, prBody)
3125
- },
3126
- config2.github
3127
- );
3128
- ok(`PR created: ${pr.html_url}`);
3129
- prUrl = pr.html_url;
3130
- } catch (err) {
3131
- error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
3921
+ if (!manifest?.prUrl) {
3922
+ error(`Agent did not produce a .lisa-manifest.json with prUrl for step ${stepNum}.`);
3923
+ await cleanupWorktree(repoPath, worktreePath);
3924
+ return { ...failResult(result.providerUsed, result), branch: branchName };
3132
3925
  }
3133
- stopSpinner();
3134
3926
  await cleanupWorktree(repoPath, worktreePath);
3135
- ok(`Step ${stepNum} complete: ${repoPath}`);
3927
+ ok(`Step ${stepNum} complete: ${repoPath} \u2014 PR: ${manifest.prUrl}`);
3136
3928
  return {
3137
3929
  success: true,
3138
3930
  providerUsed: result.providerUsed,
3139
- branch: effectiveBranch,
3140
- prUrl,
3931
+ branch: manifest.branch ?? branchName,
3932
+ prUrl: manifest.prUrl,
3141
3933
  fallback: result
3142
3934
  };
3143
3935
  }
3144
- async function runBranchSession(config2, issue, logFile, session, models) {
3936
+ async function runBranchSession(config2, issue2, logFile, session, models) {
3145
3937
  const workspace = resolve5(config2.workspace);
3146
3938
  cleanupManifest(workspace);
3147
3939
  const testRunner = detectTestRunner(workspace);
3148
3940
  if (testRunner) {
3149
3941
  log(`Detected test runner: ${testRunner}`);
3150
3942
  }
3151
- const prompt = buildImplementPrompt(issue, config2, testRunner);
3152
- const repo = findRepoConfig(config2, issue);
3943
+ const pm = detectPackageManager(workspace);
3944
+ const prompt = buildImplementPrompt(issue2, config2, testRunner, pm);
3945
+ const repo = findRepoConfig(config2, issue2);
3153
3946
  if (repo?.lifecycle) {
3154
- startSpinner(`${issue.id} \u2014 starting resources...`);
3947
+ startSpinner(`${issue2.id} \u2014 starting resources...`);
3155
3948
  const cwd = resolve5(workspace, repo.path);
3156
3949
  const started = await startResources(repo, cwd);
3157
3950
  stopSpinner();
3158
3951
  if (!started) {
3159
- error(`Lifecycle startup failed for ${issue.id}. Aborting session.`);
3952
+ error(`Lifecycle startup failed for ${issue2.id}. Aborting session.`);
3160
3953
  return {
3161
3954
  success: false,
3162
3955
  providerUsed: models[0]?.provider ?? "claude",
@@ -3171,19 +3964,22 @@ async function runBranchSession(config2, issue, logFile, session, models) {
3171
3964
  };
3172
3965
  }
3173
3966
  }
3174
- startSpinner(`${issue.id} \u2014 implementing...`);
3175
- log(`Implementing... (log: ${logFile})`);
3176
3967
  initLogFile(logFile);
3968
+ startSpinner(`${issue2.id} \u2014 implementing...`);
3969
+ log(`Implementing... (log: ${logFile})`);
3177
3970
  const result = await runWithFallback(models, prompt, {
3178
3971
  logFile,
3179
3972
  cwd: workspace,
3180
3973
  guardrailsDir: workspace,
3181
- issueId: issue.id,
3182
- overseer: config2.overseer
3974
+ issueId: issue2.id,
3975
+ overseer: config2.overseer,
3976
+ onProcess: (pid) => {
3977
+ activeProviderPid = pid;
3978
+ }
3183
3979
  });
3184
3980
  stopSpinner();
3185
3981
  try {
3186
- appendFileSync8(
3982
+ appendFileSync10(
3187
3983
  logFile,
3188
3984
  `
3189
3985
  ${"=".repeat(80)}
@@ -3198,64 +3994,23 @@ ${result.output}
3198
3994
  await stopResources();
3199
3995
  }
3200
3996
  if (!result.success) {
3201
- error(`Session ${session} failed for ${issue.id}. Check ${logFile}`);
3202
- return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
3203
- }
3204
- startSpinner(`${issue.id} \u2014 validating tests...`);
3205
- const testsPassed = await runTestValidation(workspace);
3206
- stopSpinner();
3207
- if (!testsPassed) {
3208
- error(`Tests failed for ${issue.id}. Blocking PR creation.`);
3209
- cleanupManifest(workspace);
3997
+ error(`Session ${session} failed for ${issue2.id}. Check ${logFile}`);
3210
3998
  return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
3211
3999
  }
3212
4000
  const manifest = readLisaManifest(workspace);
3213
- let detected;
3214
- if (manifest?.repoPath && manifest.branch) {
3215
- ok(`Using manifest: repo=${manifest.repoPath}, branch=${manifest.branch}`);
3216
- detected = [{ repoPath: manifest.repoPath, branch: manifest.branch }];
3217
- } else {
3218
- if (manifest) {
3219
- warn(`Manifest found but missing repoPath or branch \u2014 falling back to detection`);
3220
- }
3221
- detected = await detectFeatureBranches(config2.repos, issue.id, workspace, config2.base_branch);
3222
- }
3223
4001
  cleanupManifest(workspace);
3224
- if (detected.length === 0) {
3225
- error(`Could not detect feature branch for ${issue.id} \u2014 skipping PR creation`);
3226
- ok(`Session ${session} complete for ${issue.id}`);
3227
- return { success: true, providerUsed: result.providerUsed, prUrls: [], fallback: result };
3228
- }
3229
- startSpinner(`${issue.id} \u2014 creating PR...`);
3230
- const prTitle = manifest?.prTitle ?? readPrTitle(workspace) ?? issue.title;
3231
- const prBody = manifest?.prBody;
3232
- cleanupPrTitle(workspace);
3233
- const prUrls = [];
3234
- for (const { repoPath, branch } of detected) {
3235
- const baseBranch = resolveBaseBranch(config2, repoPath);
3236
- if (branch === baseBranch) continue;
3237
- try {
3238
- const repoInfo = await getRepoInfo(repoPath);
3239
- const pr = await createPullRequest(
3240
- {
3241
- owner: repoInfo.owner,
3242
- repo: repoInfo.repo,
3243
- head: branch,
3244
- base: baseBranch,
3245
- title: prTitle,
3246
- body: buildPrBody(result.providerUsed, prBody)
3247
- },
3248
- config2.github
3249
- );
3250
- ok(`PR created: ${pr.html_url}`);
3251
- prUrls.push(pr.html_url);
3252
- } catch (err) {
3253
- error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
3254
- }
4002
+ if (!manifest?.prUrl) {
4003
+ error(`Agent did not produce a .lisa-manifest.json with prUrl for ${issue2.id}.`);
4004
+ return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
3255
4005
  }
3256
- stopSpinner();
3257
- ok(`Session ${session} complete for ${issue.id}`);
3258
- return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
4006
+ ok(`PR created by provider: ${manifest.prUrl}`);
4007
+ ok(`Session ${session} complete for ${issue2.id}`);
4008
+ return {
4009
+ success: true,
4010
+ providerUsed: result.providerUsed,
4011
+ prUrls: [manifest.prUrl],
4012
+ fallback: result
4013
+ };
3259
4014
  }
3260
4015
  async function cleanupWorktree(repoRoot, worktreePath) {
3261
4016
  try {
@@ -3270,12 +4025,19 @@ function sleep(ms) {
3270
4025
  }
3271
4026
 
3272
4027
  // src/cli.ts
4028
+ function sleep2(ms) {
4029
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
4030
+ }
3273
4031
  var run = defineCommand({
3274
4032
  meta: { name: "run", description: "Run the agent loop" },
3275
4033
  args: {
3276
4034
  once: { type: "boolean", description: "Run a single iteration", default: false },
3277
4035
  limit: { type: "string", description: "Max number of issues to process", default: "0" },
3278
- "dry-run": { type: "boolean", description: "Preview without executing", default: false },
4036
+ "dry-run": {
4037
+ type: "boolean",
4038
+ description: "Preview config without executing \u2014 recommended first step to verify setup",
4039
+ default: false
4040
+ },
3279
4041
  issue: { type: "string", description: "Run a specific issue by identifier or URL" },
3280
4042
  provider: { type: "string", description: "AI provider (claude, gemini, opencode)" },
3281
4043
  source: { type: "string", description: "Issue source (linear, trello)" },
@@ -3285,8 +4047,10 @@ var run = defineCommand({
3285
4047
  quiet: { type: "boolean", description: "Suppress non-essential output", default: false }
3286
4048
  },
3287
4049
  async run({ args }) {
4050
+ const isTUI = process.stdout.isTTY && !args.json && !args.quiet;
3288
4051
  if (args.json) setOutputMode("json");
3289
4052
  else if (args.quiet) setOutputMode("quiet");
4053
+ else if (isTUI) setOutputMode("tui");
3290
4054
  banner();
3291
4055
  if (!configExists()) {
3292
4056
  console.error(pc2.red("No configuration found. Run `lisa init` first."));
@@ -3312,6 +4076,12 @@ ${missingVars.map((v) => ` ${v}`).join("\n")}`
3312
4076
  Add them to your ${shell} and run: source ${shell}`));
3313
4077
  process.exit(1);
3314
4078
  }
4079
+ if (isTUI) {
4080
+ const { render } = await import("ink");
4081
+ const { createElement } = await import("react");
4082
+ const { KanbanApp } = await import("./kanban-KIPKQ2IL.js");
4083
+ render(createElement(KanbanApp, { config: merged }), { exitOnCtrlC: false });
4084
+ }
3315
4085
  await runLoop(merged, {
3316
4086
  once: args.once || !!args.issue,
3317
4087
  limit: Number.parseInt(args.limit, 10),
@@ -3408,13 +4178,125 @@ function getVersion() {
3408
4178
  return "0.0.0";
3409
4179
  }
3410
4180
  }
4181
+ var CURSOR_FREE_PLAN_ERROR = "Free plans can only use Auto";
4182
+ async function isCursorFreePlan() {
4183
+ const { mkdtempSync: mkdtempSync8, unlinkSync: unlinkSync9, writeFileSync: writeFileSync11 } = await import("fs");
4184
+ const tmpDir = mkdtempSync8(join12(tmpdir8(), "lisa-cursor-check-"));
4185
+ const promptFile = join12(tmpDir, "prompt.txt");
4186
+ writeFileSync11(promptFile, "test", "utf-8");
4187
+ try {
4188
+ const bin = ["agent", "cursor-agent"].find((b) => {
4189
+ try {
4190
+ execSync8(`${b} --version`, { stdio: "ignore" });
4191
+ return true;
4192
+ } catch {
4193
+ return false;
4194
+ }
4195
+ });
4196
+ if (!bin) return false;
4197
+ const output = execSync8(`${bin} -p "$(cat '${promptFile}')" --output-format text`, {
4198
+ cwd: process.cwd(),
4199
+ encoding: "utf-8",
4200
+ timeout: 3e4
4201
+ });
4202
+ return output.includes(CURSOR_FREE_PLAN_ERROR);
4203
+ } catch (err) {
4204
+ const errorOutput = err instanceof Error ? err.message : String(err);
4205
+ return errorOutput.includes(CURSOR_FREE_PLAN_ERROR);
4206
+ } finally {
4207
+ try {
4208
+ unlinkSync9(promptFile);
4209
+ } catch {
4210
+ }
4211
+ try {
4212
+ execSync8(`rm -rf ${tmpDir}`, { stdio: "ignore" });
4213
+ } catch {
4214
+ }
4215
+ }
4216
+ }
4217
+ var issueGet = defineCommand({
4218
+ meta: { name: "get", description: "Fetch full issue details as JSON" },
4219
+ args: {
4220
+ id: { type: "positional", required: true, description: "Issue ID (e.g. INT-123)" }
4221
+ },
4222
+ async run({ args }) {
4223
+ await sleep2(1e3);
4224
+ const configDir = findConfigDir();
4225
+ if (!configDir) {
4226
+ console.error(JSON.stringify({ error: "No .lisa/config.yaml found in directory tree" }));
4227
+ process.exit(1);
4228
+ }
4229
+ const config2 = loadConfig(configDir);
4230
+ const source = createSource(config2.source);
4231
+ let issue2;
4232
+ try {
4233
+ issue2 = await source.fetchIssueById(args.id);
4234
+ } catch (err) {
4235
+ console.error(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
4236
+ process.exit(1);
4237
+ }
4238
+ if (!issue2) {
4239
+ console.error(JSON.stringify({ error: `Issue ${args.id} not found` }));
4240
+ process.exit(1);
4241
+ }
4242
+ console.log(JSON.stringify(issue2));
4243
+ }
4244
+ });
4245
+ var issueDone = defineCommand({
4246
+ meta: { name: "done", description: "Complete an issue: attach PR, update status, remove label" },
4247
+ args: {
4248
+ id: { type: "positional", required: true, description: "Issue ID (e.g. INT-123)" },
4249
+ "pr-url": { type: "string", required: true, description: "Pull request URL" }
4250
+ },
4251
+ async run({ args }) {
4252
+ await sleep2(1e3);
4253
+ const configDir = findConfigDir();
4254
+ if (!configDir) {
4255
+ console.error(JSON.stringify({ error: "No .lisa/config.yaml found in directory tree" }));
4256
+ process.exit(1);
4257
+ }
4258
+ const config2 = loadConfig(configDir);
4259
+ const source = createSource(config2.source);
4260
+ try {
4261
+ await source.attachPullRequest(args.id, args["pr-url"]);
4262
+ await source.completeIssue(args.id, config2.source_config.done, config2.source_config.label);
4263
+ console.log(JSON.stringify({ success: true, issueId: args.id, prUrl: args["pr-url"] }));
4264
+ } catch (err) {
4265
+ console.error(
4266
+ JSON.stringify({
4267
+ error: err instanceof Error ? err.message : String(err),
4268
+ issueId: args.id
4269
+ })
4270
+ );
4271
+ process.exit(1);
4272
+ }
4273
+ }
4274
+ });
4275
+ var issue = defineCommand({
4276
+ meta: { name: "issue", description: "Issue tracker operations for use inside worktrees" },
4277
+ subCommands: { get: issueGet, done: issueDone }
4278
+ });
4279
+ var CURSOR_MODELS = [
4280
+ "auto",
4281
+ "composer-1.5",
4282
+ "composer-1",
4283
+ "gpt-5.3-codex",
4284
+ "gpt-5.3-codex-low",
4285
+ "gpt-5.3-codex-high",
4286
+ "gpt-5.3-codex-xhigh",
4287
+ "gpt-5.3-codex-fast",
4288
+ "sonnet-4.6",
4289
+ "sonnet-4.6-thinking",
4290
+ "sonnet-4.5",
4291
+ "sonnet-4.5-thinking"
4292
+ ];
3411
4293
  var main = defineCommand({
3412
4294
  meta: {
3413
4295
  name: "lisa",
3414
4296
  version: getVersion(),
3415
4297
  description: "Deterministic autonomous issue resolver \u2014 structured AI agent loop for Linear/Trello"
3416
4298
  },
3417
- subCommands: { run, config, init, status }
4299
+ subCommands: { run, config, init, status, issue }
3418
4300
  });
3419
4301
  async function runConfigWizard() {
3420
4302
  banner();
@@ -3423,10 +4305,12 @@ async function runConfigWizard() {
3423
4305
  gemini: "Gemini CLI",
3424
4306
  opencode: "OpenCode",
3425
4307
  copilot: "GitHub Copilot CLI",
3426
- cursor: "Cursor Agent"
4308
+ cursor: "Cursor Agent",
4309
+ goose: "Goose",
4310
+ aider: "Aider"
3427
4311
  };
3428
4312
  const providerModels = {
3429
- claude: ["claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5"],
4313
+ claude: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"],
3430
4314
  gemini: ["gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro"]
3431
4315
  };
3432
4316
  const available = await getAvailableProviders();
@@ -3459,14 +4343,22 @@ After installing, run ${pc2.cyan("lisa init")} again.`
3459
4343
  providerName = selected;
3460
4344
  }
3461
4345
  let selectedModels = [];
3462
- const availableModels = providerModels[providerName];
4346
+ let availableModels = providerModels[providerName];
4347
+ if (providerName === "cursor") {
4348
+ const isFree = await isCursorFreePlan();
4349
+ if (isFree) {
4350
+ availableModels = ["auto"];
4351
+ clack.log.info("Cursor Free plan detected. Using 'auto' model only.");
4352
+ } else {
4353
+ availableModels = CURSOR_MODELS;
4354
+ }
4355
+ }
3463
4356
  if (availableModels && availableModels.length > 0) {
3464
4357
  const modelSelection = await clack.multiselect({
3465
- message: "Which models to use? (first = primary, rest = fallbacks in order)",
3466
- options: availableModels.map((m, i) => ({
4358
+ message: "Which models to use? Select in order: primary first, then fallbacks",
4359
+ options: availableModels.map((m) => ({
3467
4360
  value: m,
3468
- label: m,
3469
- hint: i === 0 ? "primary" : `fallback ${i}`
4361
+ label: m
3470
4362
  })),
3471
4363
  required: false
3472
4364
  });
@@ -3646,12 +4538,12 @@ async function detectGitHubMethod() {
3646
4538
  }
3647
4539
  async function detectGitRepos() {
3648
4540
  const cwd = process.cwd();
3649
- if (existsSync7(join10(cwd, ".git"))) {
4541
+ if (existsSync7(join12(cwd, ".git"))) {
3650
4542
  clack.log.info(`Detected git repository in current directory.`);
3651
4543
  return [];
3652
4544
  }
3653
4545
  const entries = readdirSync(cwd, { withFileTypes: true });
3654
- const gitDirs = entries.filter((e) => e.isDirectory() && existsSync7(join10(cwd, e.name, ".git"))).map((e) => e.name);
4546
+ const gitDirs = entries.filter((e) => e.isDirectory() && existsSync7(join12(cwd, e.name, ".git"))).map((e) => e.name);
3655
4547
  if (gitDirs.length === 0) {
3656
4548
  return [];
3657
4549
  }
@@ -3661,7 +4553,7 @@ async function detectGitRepos() {
3661
4553
  });
3662
4554
  if (clack.isCancel(selected)) return process.exit(0);
3663
4555
  return selected.map((dir) => ({
3664
- name: getGitRepoName(join10(cwd, dir)) ?? dir,
4556
+ name: getGitRepoName(join12(cwd, dir)) ?? dir,
3665
4557
  path: `./${dir}`,
3666
4558
  match: "",
3667
4559
  base_branch: ""
@@ -3669,7 +4561,7 @@ async function detectGitRepos() {
3669
4561
  }
3670
4562
  function detectDefaultBranch(repoPath) {
3671
4563
  try {
3672
- const ref = execSync6("git symbolic-ref refs/remotes/origin/HEAD --short", {
4564
+ const ref = execSync8("git symbolic-ref refs/remotes/origin/HEAD --short", {
3673
4565
  cwd: repoPath,
3674
4566
  encoding: "utf-8"
3675
4567
  }).trim();
@@ -3680,7 +4572,7 @@ function detectDefaultBranch(repoPath) {
3680
4572
  }
3681
4573
  function getGitRepoName(repoPath) {
3682
4574
  try {
3683
- const url = execSync6("git remote get-url origin", { cwd: repoPath, encoding: "utf-8" }).trim();
4575
+ const url = execSync8("git remote get-url origin", { cwd: repoPath, encoding: "utf-8" }).trim();
3684
4576
  const match = url.match(/\/([^/]+?)(?:\.git)?$/) ?? url.match(/:([^/]+?)(?:\.git)?$/);
3685
4577
  return match?.[1] ?? null;
3686
4578
  } catch {