@tarcisiopgs/lisa 1.7.7 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,6 +82,7 @@ Lisa follows a deterministic pipeline:
82
82
  | Cursor Agent | `cursor` | `agent` / `cursor-agent` |
83
83
  | Goose | `goose` | `goose` |
84
84
  | Aider | `aider` | `aider` |
85
+ | OpenAI Codex | `codex` | `codex` |
85
86
 
86
87
  At least one provider must be installed and available in your PATH.
87
88
 
File without changes
package/dist/index.js CHANGED
@@ -9,10 +9,10 @@ import {
9
9
  } from "./chunk-YZKNBQN6.js";
10
10
 
11
11
  // src/cli.ts
12
- import { execSync as execSync8 } from "child_process";
12
+ import { execSync as execSync9 } from "child_process";
13
13
  import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6 } from "fs";
14
- import { tmpdir as tmpdir8 } from "os";
15
- import { join as join12, resolve as resolvePath } from "path";
14
+ import { tmpdir as tmpdir9 } from "os";
15
+ import { join as join13, resolve as resolvePath } from "path";
16
16
  import * as clack from "@clack/prompts";
17
17
  import { defineCommand, runMain } from "citty";
18
18
  import pc2 from "picocolors";
@@ -171,6 +171,30 @@ function mergeWithFlags(config2, flags) {
171
171
 
172
172
  // src/git/github.ts
173
173
  import { execa } from "execa";
174
+
175
+ // src/git/pr-body.ts
176
+ var PROVIDER_ATTRIBUTION_RE = /claude\.ai|claude\s+code|gemini\s+cli|openai\s+codex|\bgoose\b|\baider\b|github\s+copilot|cursor\s+agent|\bopencode\b/i;
177
+ var AI_COAUTHOR_RE = /co-authored-by:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)/i;
178
+ function stripProviderAttribution(body) {
179
+ let result = body;
180
+ while (true) {
181
+ const sepIndex = result.lastIndexOf("\n---");
182
+ if (sepIndex === -1) break;
183
+ const section = result.slice(sepIndex);
184
+ if (PROVIDER_ATTRIBUTION_RE.test(section) || AI_COAUTHOR_RE.test(section)) {
185
+ result = result.slice(0, sepIndex).trimEnd();
186
+ } else {
187
+ break;
188
+ }
189
+ }
190
+ result = result.replace(
191
+ /\n+Co-Authored-By:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)[^\n]*/gi,
192
+ ""
193
+ );
194
+ return result.trimEnd();
195
+ }
196
+
197
+ // src/git/github.ts
174
198
  async function isGhCliAvailable() {
175
199
  try {
176
200
  await execa("gh", ["auth", "status"]);
@@ -186,7 +210,8 @@ var PROVIDER_DISPLAY_NAMES = {
186
210
  copilot: "GitHub Copilot CLI",
187
211
  cursor: "Cursor Agent",
188
212
  goose: "Goose",
189
- aider: "Aider"
213
+ aider: "Aider",
214
+ codex: "OpenAI Codex"
190
215
  };
191
216
  function formatProviderName(providerUsed) {
192
217
  const providerKey = providerUsed.split("/")[0] ?? providerUsed;
@@ -201,7 +226,7 @@ async function appendPrAttribution(prUrl, providerUsed) {
201
226
 
202
227
  ---
203
228
  \u{1F916} Resolved by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerName}**`;
204
- const newBody = (body ?? "") + attribution;
229
+ const newBody = stripProviderAttribution(body ?? "") + attribution;
205
230
  await execa("gh", ["pr", "edit", prUrl, "--body", newBody]);
206
231
  } catch {
207
232
  }
@@ -265,6 +290,28 @@ function ensureWorktreeGitignore(repoRoot) {
265
290
  `);
266
291
  }
267
292
  }
293
+ var LOGS_GITIGNORE_ENTRY = ".lisa/logs/*";
294
+ function ensureLogsGitignore(repoRoot) {
295
+ if (!existsSync2(join(repoRoot, ".git"))) {
296
+ return false;
297
+ }
298
+ const gitignorePath = join(repoRoot, ".gitignore");
299
+ if (!existsSync2(gitignorePath)) {
300
+ appendFileSync(gitignorePath, `# Lisa
301
+ ${LOGS_GITIGNORE_ENTRY}
302
+ `);
303
+ return true;
304
+ }
305
+ const content = readFileSync2(gitignorePath, "utf-8");
306
+ if (content.split("\n").some((line) => line.trim() === LOGS_GITIGNORE_ENTRY)) {
307
+ return false;
308
+ }
309
+ const separator = content.endsWith("\n") ? "" : "\n";
310
+ appendFileSync(gitignorePath, `${separator}# Lisa
311
+ ${LOGS_GITIGNORE_ENTRY}
312
+ `);
313
+ return true;
314
+ }
268
315
  function determineRepoPath(repos, issue2, workspace) {
269
316
  if (repos.length === 0) return void 0;
270
317
  if (issue2.repo) {
@@ -281,8 +328,8 @@ function determineRepoPath(repos, issue2, workspace) {
281
328
  }
282
329
 
283
330
  // src/loop.ts
284
- import { appendFileSync as appendFileSync10, existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync8 } from "fs";
285
- import { join as join11, resolve as resolve5 } from "path";
331
+ import { appendFileSync as appendFileSync11, existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync9 } from "fs";
332
+ import { join as join12, resolve as resolve5 } from "path";
286
333
  import { execa as execa3 } from "execa";
287
334
 
288
335
  // src/output/logger.ts
@@ -1129,16 +1176,16 @@ var ClaudeProvider = class {
1129
1176
  }
1130
1177
  };
1131
1178
 
1132
- // src/providers/copilot.ts
1179
+ // src/providers/codex.ts
1133
1180
  import { execSync as execSync3, spawn as spawn3 } from "child_process";
1134
1181
  import { appendFileSync as appendFileSync5, mkdtempSync as mkdtempSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6 } from "fs";
1135
1182
  import { tmpdir as tmpdir3 } from "os";
1136
1183
  import { join as join6 } from "path";
1137
- var CopilotProvider = class {
1138
- name = "copilot";
1184
+ var CodexProvider = class {
1185
+ name = "codex";
1139
1186
  async isAvailable() {
1140
1187
  try {
1141
- execSync3("copilot version", { stdio: "ignore" });
1188
+ execSync3("codex --version", { stdio: "ignore" });
1142
1189
  return true;
1143
1190
  } catch {
1144
1191
  return false;
@@ -1150,9 +1197,12 @@ var CopilotProvider = class {
1150
1197
  const promptFile = join6(tmpDir, "prompt.md");
1151
1198
  writeFileSync6(promptFile, prompt, "utf-8");
1152
1199
  try {
1153
- const proc = spawn3("sh", ["-c", `copilot --allow-all -p "$(cat '${promptFile}')"`], {
1200
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
1201
+ const cmd = `codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral ${modelFlag} "$(cat '${promptFile}')"`;
1202
+ const proc = spawn3("sh", ["-c", cmd], {
1154
1203
  cwd: opts.cwd,
1155
- stdio: ["ignore", "pipe", "pipe"]
1204
+ stdio: ["ignore", "pipe", "pipe"],
1205
+ env: { ...process.env, CODEX_QUIET_MODE: "1" }
1156
1206
  });
1157
1207
  if (proc.pid) opts.onProcess?.(proc.pid);
1158
1208
  const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
@@ -1206,15 +1256,92 @@ var CopilotProvider = class {
1206
1256
  }
1207
1257
  };
1208
1258
 
1209
- // src/providers/cursor.ts
1259
+ // src/providers/copilot.ts
1210
1260
  import { execSync as execSync4, spawn as spawn4 } from "child_process";
1211
1261
  import { appendFileSync as appendFileSync6, mkdtempSync as mkdtempSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync7 } from "fs";
1212
1262
  import { tmpdir as tmpdir4 } from "os";
1213
1263
  import { join as join7 } from "path";
1264
+ var CopilotProvider = class {
1265
+ name = "copilot";
1266
+ async isAvailable() {
1267
+ try {
1268
+ execSync4("copilot version", { stdio: "ignore" });
1269
+ return true;
1270
+ } catch {
1271
+ return false;
1272
+ }
1273
+ }
1274
+ async run(prompt, opts) {
1275
+ const start = Date.now();
1276
+ const tmpDir = mkdtempSync4(join7(tmpdir4(), "lisa-"));
1277
+ const promptFile = join7(tmpDir, "prompt.md");
1278
+ writeFileSync7(promptFile, prompt, "utf-8");
1279
+ try {
1280
+ const proc = spawn4("sh", ["-c", `copilot --allow-all -p "$(cat '${promptFile}')"`], {
1281
+ cwd: opts.cwd,
1282
+ stdio: ["ignore", "pipe", "pipe"]
1283
+ });
1284
+ if (proc.pid) opts.onProcess?.(proc.pid);
1285
+ const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
1286
+ const chunks = [];
1287
+ proc.stdout.on("data", (chunk) => {
1288
+ const text2 = chunk.toString();
1289
+ if (getOutputMode() !== "tui") process.stdout.write(text2);
1290
+ if (opts.issueId) {
1291
+ kanbanEmitter.emit("issue:output", opts.issueId, text2);
1292
+ }
1293
+ chunks.push(text2);
1294
+ try {
1295
+ appendFileSync6(opts.logFile, text2);
1296
+ } catch {
1297
+ }
1298
+ });
1299
+ proc.stderr.on("data", (chunk) => {
1300
+ const text2 = chunk.toString();
1301
+ if (getOutputMode() !== "tui") process.stderr.write(text2);
1302
+ try {
1303
+ appendFileSync6(opts.logFile, text2);
1304
+ } catch {
1305
+ }
1306
+ });
1307
+ const exitCode = await new Promise((resolve6) => {
1308
+ proc.on("close", (code) => {
1309
+ overseer?.stop();
1310
+ resolve6(code ?? 1);
1311
+ });
1312
+ });
1313
+ if (overseer?.wasKilled()) {
1314
+ chunks.push(STUCK_MESSAGE);
1315
+ }
1316
+ return {
1317
+ success: exitCode === 0 && !overseer?.wasKilled(),
1318
+ output: chunks.join(""),
1319
+ duration: Date.now() - start
1320
+ };
1321
+ } catch (err) {
1322
+ return {
1323
+ success: false,
1324
+ output: err instanceof Error ? err.message : String(err),
1325
+ duration: Date.now() - start
1326
+ };
1327
+ } finally {
1328
+ try {
1329
+ unlinkSync4(promptFile);
1330
+ } catch {
1331
+ }
1332
+ }
1333
+ }
1334
+ };
1335
+
1336
+ // src/providers/cursor.ts
1337
+ import { execSync as execSync5, spawn as spawn5 } from "child_process";
1338
+ import { appendFileSync as appendFileSync7, mkdtempSync as mkdtempSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "fs";
1339
+ import { tmpdir as tmpdir5 } from "os";
1340
+ import { join as join8 } from "path";
1214
1341
  function findCursorBinary() {
1215
1342
  for (const bin of ["agent", "cursor-agent"]) {
1216
1343
  try {
1217
- execSync4(`${bin} --version`, { stdio: "ignore" });
1344
+ execSync5(`${bin} --version`, { stdio: "ignore" });
1218
1345
  return bin;
1219
1346
  } catch {
1220
1347
  }
@@ -1236,12 +1363,12 @@ var CursorProvider = class {
1236
1363
  duration: Date.now() - start
1237
1364
  };
1238
1365
  }
1239
- const tmpDir = mkdtempSync4(join7(tmpdir4(), "lisa-"));
1240
- const promptFile = join7(tmpDir, "prompt.md");
1241
- writeFileSync7(promptFile, prompt, "utf-8");
1366
+ const tmpDir = mkdtempSync5(join8(tmpdir5(), "lisa-"));
1367
+ const promptFile = join8(tmpDir, "prompt.md");
1368
+ writeFileSync8(promptFile, prompt, "utf-8");
1242
1369
  try {
1243
1370
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1244
- const proc = spawn4(
1371
+ const proc = spawn5(
1245
1372
  "sh",
1246
1373
  ["-c", `${bin} -p "$(cat '${promptFile}')" --output-format text --force ${modelFlag}`],
1247
1374
  {
@@ -1260,7 +1387,7 @@ var CursorProvider = class {
1260
1387
  }
1261
1388
  chunks.push(text2);
1262
1389
  try {
1263
- appendFileSync6(opts.logFile, text2);
1390
+ appendFileSync7(opts.logFile, text2);
1264
1391
  } catch {
1265
1392
  }
1266
1393
  });
@@ -1268,7 +1395,7 @@ var CursorProvider = class {
1268
1395
  const text2 = chunk.toString();
1269
1396
  if (getOutputMode() !== "tui") process.stderr.write(text2);
1270
1397
  try {
1271
- appendFileSync6(opts.logFile, text2);
1398
+ appendFileSync7(opts.logFile, text2);
1272
1399
  } catch {
1273
1400
  }
1274
1401
  });
@@ -1294,7 +1421,7 @@ var CursorProvider = class {
1294
1421
  };
1295
1422
  } finally {
1296
1423
  try {
1297
- unlinkSync4(promptFile);
1424
+ unlinkSync5(promptFile);
1298
1425
  } catch {
1299
1426
  }
1300
1427
  }
@@ -1302,15 +1429,15 @@ var CursorProvider = class {
1302
1429
  };
1303
1430
 
1304
1431
  // src/providers/gemini.ts
1305
- import { execSync as execSync5, spawn as spawn5 } from "child_process";
1306
- import { appendFileSync as appendFileSync7, mkdtempSync as mkdtempSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "fs";
1307
- import { tmpdir as tmpdir5 } from "os";
1308
- import { join as join8 } from "path";
1432
+ import { execSync as execSync6, spawn as spawn6 } from "child_process";
1433
+ import { appendFileSync as appendFileSync8, mkdtempSync as mkdtempSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "fs";
1434
+ import { tmpdir as tmpdir6 } from "os";
1435
+ import { join as join9 } from "path";
1309
1436
  var GeminiProvider = class {
1310
1437
  name = "gemini";
1311
1438
  async isAvailable() {
1312
1439
  try {
1313
- execSync5("gemini --version", { stdio: "ignore" });
1440
+ execSync6("gemini --version", { stdio: "ignore" });
1314
1441
  return true;
1315
1442
  } catch {
1316
1443
  return false;
@@ -1318,12 +1445,12 @@ var GeminiProvider = class {
1318
1445
  }
1319
1446
  async run(prompt, opts) {
1320
1447
  const start = Date.now();
1321
- const tmpDir = mkdtempSync5(join8(tmpdir5(), "lisa-"));
1322
- const promptFile = join8(tmpDir, "prompt.md");
1323
- writeFileSync8(promptFile, prompt, "utf-8");
1448
+ const tmpDir = mkdtempSync6(join9(tmpdir6(), "lisa-"));
1449
+ const promptFile = join9(tmpDir, "prompt.md");
1450
+ writeFileSync9(promptFile, prompt, "utf-8");
1324
1451
  try {
1325
1452
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1326
- const proc = spawn5("sh", ["-c", `gemini --yolo ${modelFlag} -p "$(cat '${promptFile}')"`], {
1453
+ const proc = spawn6("sh", ["-c", `gemini --yolo ${modelFlag} -p "$(cat '${promptFile}')"`], {
1327
1454
  cwd: opts.cwd,
1328
1455
  stdio: ["ignore", "pipe", "pipe"]
1329
1456
  });
@@ -1338,7 +1465,7 @@ var GeminiProvider = class {
1338
1465
  }
1339
1466
  chunks.push(text2);
1340
1467
  try {
1341
- appendFileSync7(opts.logFile, text2);
1468
+ appendFileSync8(opts.logFile, text2);
1342
1469
  } catch {
1343
1470
  }
1344
1471
  });
@@ -1346,7 +1473,7 @@ var GeminiProvider = class {
1346
1473
  const text2 = chunk.toString();
1347
1474
  if (getOutputMode() !== "tui") process.stderr.write(text2);
1348
1475
  try {
1349
- appendFileSync7(opts.logFile, text2);
1476
+ appendFileSync8(opts.logFile, text2);
1350
1477
  } catch {
1351
1478
  }
1352
1479
  });
@@ -1372,7 +1499,7 @@ var GeminiProvider = class {
1372
1499
  };
1373
1500
  } finally {
1374
1501
  try {
1375
- unlinkSync5(promptFile);
1502
+ unlinkSync6(promptFile);
1376
1503
  } catch {
1377
1504
  }
1378
1505
  }
@@ -1380,15 +1507,15 @@ var GeminiProvider = class {
1380
1507
  };
1381
1508
 
1382
1509
  // src/providers/goose.ts
1383
- import { execSync as execSync6, spawn as spawn6 } from "child_process";
1384
- import { appendFileSync as appendFileSync8, mkdtempSync as mkdtempSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "fs";
1385
- import { tmpdir as tmpdir6 } from "os";
1386
- import { join as join9 } from "path";
1510
+ import { execSync as execSync7, spawn as spawn7 } from "child_process";
1511
+ import { appendFileSync as appendFileSync9, mkdtempSync as mkdtempSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync10 } from "fs";
1512
+ import { tmpdir as tmpdir7 } from "os";
1513
+ import { join as join10 } from "path";
1387
1514
  var GooseProvider = class {
1388
1515
  name = "goose";
1389
1516
  async isAvailable() {
1390
1517
  try {
1391
- execSync6("goose --version", { stdio: "ignore" });
1518
+ execSync7("goose --version", { stdio: "ignore" });
1392
1519
  return true;
1393
1520
  } catch {
1394
1521
  return false;
@@ -1396,12 +1523,12 @@ var GooseProvider = class {
1396
1523
  }
1397
1524
  async run(prompt, opts) {
1398
1525
  const start = Date.now();
1399
- const tmpDir = mkdtempSync6(join9(tmpdir6(), "lisa-"));
1400
- const promptFile = join9(tmpDir, "prompt.md");
1401
- writeFileSync9(promptFile, prompt, "utf-8");
1526
+ const tmpDir = mkdtempSync7(join10(tmpdir7(), "lisa-"));
1527
+ const promptFile = join10(tmpDir, "prompt.md");
1528
+ writeFileSync10(promptFile, prompt, "utf-8");
1402
1529
  try {
1403
1530
  const modelFlag = opts.model ? `--model ${opts.model}` : "";
1404
- const proc = spawn6("sh", ["-c", `goose run ${modelFlag} --text "$(cat '${promptFile}')"`], {
1531
+ const proc = spawn7("sh", ["-c", `goose run ${modelFlag} --text "$(cat '${promptFile}')"`], {
1405
1532
  cwd: opts.cwd,
1406
1533
  stdio: ["ignore", "pipe", "pipe"]
1407
1534
  });
@@ -1416,7 +1543,7 @@ var GooseProvider = class {
1416
1543
  }
1417
1544
  chunks.push(text2);
1418
1545
  try {
1419
- appendFileSync8(opts.logFile, text2);
1546
+ appendFileSync9(opts.logFile, text2);
1420
1547
  } catch {
1421
1548
  }
1422
1549
  });
@@ -1424,7 +1551,7 @@ var GooseProvider = class {
1424
1551
  const text2 = chunk.toString();
1425
1552
  if (getOutputMode() !== "tui") process.stderr.write(text2);
1426
1553
  try {
1427
- appendFileSync8(opts.logFile, text2);
1554
+ appendFileSync9(opts.logFile, text2);
1428
1555
  } catch {
1429
1556
  }
1430
1557
  });
@@ -1450,7 +1577,7 @@ var GooseProvider = class {
1450
1577
  };
1451
1578
  } finally {
1452
1579
  try {
1453
- unlinkSync6(promptFile);
1580
+ unlinkSync7(promptFile);
1454
1581
  } catch {
1455
1582
  }
1456
1583
  }
@@ -1458,15 +1585,15 @@ var GooseProvider = class {
1458
1585
  };
1459
1586
 
1460
1587
  // src/providers/opencode.ts
1461
- import { execSync as execSync7, spawn as spawn7 } from "child_process";
1462
- import { appendFileSync as appendFileSync9, mkdtempSync as mkdtempSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync10 } from "fs";
1463
- import { tmpdir as tmpdir7 } from "os";
1464
- import { join as join10 } from "path";
1588
+ import { execSync as execSync8, spawn as spawn8 } from "child_process";
1589
+ import { appendFileSync as appendFileSync10, mkdtempSync as mkdtempSync8, unlinkSync as unlinkSync8, writeFileSync as writeFileSync11 } from "fs";
1590
+ import { tmpdir as tmpdir8 } from "os";
1591
+ import { join as join11 } from "path";
1465
1592
  var OpenCodeProvider = class {
1466
1593
  name = "opencode";
1467
1594
  async isAvailable() {
1468
1595
  try {
1469
- execSync7("opencode --version", { stdio: "ignore" });
1596
+ execSync8("opencode --version", { stdio: "ignore" });
1470
1597
  return true;
1471
1598
  } catch {
1472
1599
  return false;
@@ -1474,11 +1601,11 @@ var OpenCodeProvider = class {
1474
1601
  }
1475
1602
  async run(prompt, opts) {
1476
1603
  const start = Date.now();
1477
- const tmpDir = mkdtempSync7(join10(tmpdir7(), "lisa-"));
1478
- const promptFile = join10(tmpDir, "prompt.md");
1479
- writeFileSync10(promptFile, prompt, "utf-8");
1604
+ const tmpDir = mkdtempSync8(join11(tmpdir8(), "lisa-"));
1605
+ const promptFile = join11(tmpDir, "prompt.md");
1606
+ writeFileSync11(promptFile, prompt, "utf-8");
1480
1607
  try {
1481
- const proc = spawn7("sh", ["-c", `opencode run "$(cat '${promptFile}')"`], {
1608
+ const proc = spawn8("sh", ["-c", `opencode run "$(cat '${promptFile}')"`], {
1482
1609
  cwd: opts.cwd,
1483
1610
  stdio: ["ignore", "pipe", "pipe"]
1484
1611
  });
@@ -1493,7 +1620,7 @@ var OpenCodeProvider = class {
1493
1620
  }
1494
1621
  chunks.push(text2);
1495
1622
  try {
1496
- appendFileSync9(opts.logFile, text2);
1623
+ appendFileSync10(opts.logFile, text2);
1497
1624
  } catch {
1498
1625
  }
1499
1626
  });
@@ -1501,7 +1628,7 @@ var OpenCodeProvider = class {
1501
1628
  const text2 = chunk.toString();
1502
1629
  if (getOutputMode() !== "tui") process.stderr.write(text2);
1503
1630
  try {
1504
- appendFileSync9(opts.logFile, text2);
1631
+ appendFileSync10(opts.logFile, text2);
1505
1632
  } catch {
1506
1633
  }
1507
1634
  });
@@ -1527,7 +1654,7 @@ var OpenCodeProvider = class {
1527
1654
  };
1528
1655
  } finally {
1529
1656
  try {
1530
- unlinkSync7(promptFile);
1657
+ unlinkSync8(promptFile);
1531
1658
  } catch {
1532
1659
  }
1533
1660
  }
@@ -1542,7 +1669,8 @@ var providers = {
1542
1669
  copilot: () => new CopilotProvider(),
1543
1670
  cursor: () => new CursorProvider(),
1544
1671
  goose: () => new GooseProvider(),
1545
- aider: () => new AiderProvider()
1672
+ aider: () => new AiderProvider(),
1673
+ codex: () => new CodexProvider()
1546
1674
  };
1547
1675
  async function getAllProvidersWithAvailability() {
1548
1676
  const all = Object.values(providers).map((f) => f());
@@ -1671,7 +1799,7 @@ function formatAttemptsReport(attempts) {
1671
1799
  }
1672
1800
 
1673
1801
  // src/session/lifecycle.ts
1674
- import { spawn as spawn8 } from "child_process";
1802
+ import { spawn as spawn9 } from "child_process";
1675
1803
  import { createConnection } from "net";
1676
1804
  import { resolve as resolve4 } from "path";
1677
1805
  var managedResources = [];
@@ -1709,7 +1837,7 @@ function waitForPort(port, timeoutMs) {
1709
1837
  }
1710
1838
  function spawnResource(config2, baseCwd) {
1711
1839
  const cwd = config2.cwd ? resolve4(baseCwd, config2.cwd) : baseCwd;
1712
- const child = spawn8("sh", ["-c", config2.up], {
1840
+ const child = spawn9("sh", ["-c", config2.up], {
1713
1841
  cwd,
1714
1842
  stdio: "ignore",
1715
1843
  detached: true
@@ -1719,7 +1847,7 @@ function spawnResource(config2, baseCwd) {
1719
1847
  }
1720
1848
  function runSetupCommand(command, cwd) {
1721
1849
  return new Promise((resolve6, reject) => {
1722
- const child = spawn8("sh", ["-c", command], {
1850
+ const child = spawn9("sh", ["-c", command], {
1723
1851
  cwd,
1724
1852
  stdio: "inherit"
1725
1853
  });
@@ -1790,7 +1918,7 @@ async function stopResources() {
1790
1918
  }
1791
1919
  } else {
1792
1920
  await new Promise((resolve6) => {
1793
- const down = spawn8("sh", ["-c", config2.down], {
1921
+ const down = spawn9("sh", ["-c", config2.down], {
1794
1922
  stdio: "ignore"
1795
1923
  });
1796
1924
  down.on("close", () => resolve6());
@@ -3165,7 +3293,16 @@ function resolveModels(config2) {
3165
3293
  if (!config2.models || config2.models.length === 0) {
3166
3294
  return [{ provider: config2.provider }];
3167
3295
  }
3168
- const knownProviders = /* @__PURE__ */ new Set(["claude", "gemini", "opencode", "copilot", "cursor"]);
3296
+ const knownProviders = /* @__PURE__ */ new Set([
3297
+ "claude",
3298
+ "gemini",
3299
+ "opencode",
3300
+ "copilot",
3301
+ "cursor",
3302
+ "goose",
3303
+ "aider",
3304
+ "codex"
3305
+ ]);
3169
3306
  for (const m of config2.models) {
3170
3307
  if (knownProviders.has(m) && m !== config2.provider) {
3171
3308
  warn(
@@ -3189,7 +3326,7 @@ function resolveModels(config2) {
3189
3326
  }
3190
3327
  var PLAN_FILE = ".lisa-plan.json";
3191
3328
  function readLisaPlan(dir) {
3192
- const planPath = join11(dir, PLAN_FILE);
3329
+ const planPath = join12(dir, PLAN_FILE);
3193
3330
  if (!existsSync6(planPath)) return null;
3194
3331
  try {
3195
3332
  return JSON.parse(readFileSync5(planPath, "utf-8").trim());
@@ -3199,13 +3336,13 @@ function readLisaPlan(dir) {
3199
3336
  }
3200
3337
  function cleanupPlan(dir) {
3201
3338
  try {
3202
- unlinkSync8(join11(dir, PLAN_FILE));
3339
+ unlinkSync9(join12(dir, PLAN_FILE));
3203
3340
  } catch {
3204
3341
  }
3205
3342
  }
3206
3343
  var MANIFEST_FILE = ".lisa-manifest.json";
3207
3344
  function readLisaManifest(dir) {
3208
- const manifestPath = join11(dir, MANIFEST_FILE);
3345
+ const manifestPath = join12(dir, MANIFEST_FILE);
3209
3346
  if (!existsSync6(manifestPath)) return null;
3210
3347
  try {
3211
3348
  return JSON.parse(readFileSync5(manifestPath, "utf-8").trim());
@@ -3215,7 +3352,7 @@ function readLisaManifest(dir) {
3215
3352
  }
3216
3353
  function cleanupManifest(dir) {
3217
3354
  try {
3218
- unlinkSync8(join11(dir, MANIFEST_FILE));
3355
+ unlinkSync9(join12(dir, MANIFEST_FILE));
3219
3356
  } catch {
3220
3357
  }
3221
3358
  }
@@ -3608,7 +3745,7 @@ async function runNativeWorktreeSession(config2, issue2, logFile, session, model
3608
3745
  });
3609
3746
  stopSpinner();
3610
3747
  try {
3611
- appendFileSync10(
3748
+ appendFileSync11(
3612
3749
  logFile,
3613
3750
  `
3614
3751
  ${"=".repeat(80)}
@@ -3715,7 +3852,7 @@ async function runManualWorktreeSession(config2, issue2, logFile, session, model
3715
3852
  });
3716
3853
  stopSpinner();
3717
3854
  try {
3718
- appendFileSync10(
3855
+ appendFileSync11(
3719
3856
  logFile,
3720
3857
  `
3721
3858
  ${"=".repeat(80)}
@@ -3774,7 +3911,7 @@ async function runWorktreeMultiRepoSession(config2, issue2, logFile, session, mo
3774
3911
  });
3775
3912
  stopSpinner();
3776
3913
  try {
3777
- appendFileSync10(
3914
+ appendFileSync11(
3778
3915
  logFile,
3779
3916
  `
3780
3917
  ${"=".repeat(80)}
@@ -3910,7 +4047,7 @@ async function runMultiRepoStep(config2, issue2, step, previousResults, logFile,
3910
4047
  stopSpinner();
3911
4048
  if (repoConfig?.lifecycle) await stopResources();
3912
4049
  try {
3913
- appendFileSync10(
4050
+ appendFileSync11(
3914
4051
  logFile,
3915
4052
  `
3916
4053
  ${"=".repeat(80)}
@@ -3989,7 +4126,7 @@ async function runBranchSession(config2, issue2, logFile, session, models) {
3989
4126
  });
3990
4127
  stopSpinner();
3991
4128
  try {
3992
- appendFileSync10(
4129
+ appendFileSync11(
3993
4130
  logFile,
3994
4131
  `
3995
4132
  ${"=".repeat(80)}
@@ -4095,7 +4232,7 @@ Add them to your ${shell} and run: source ${shell}`));
4095
4232
  if (isTUI) {
4096
4233
  const { render } = await import("ink");
4097
4234
  const { createElement } = await import("react");
4098
- const { KanbanApp } = await import("./kanban-5C3WZIKC.js");
4235
+ const { KanbanApp } = await import("./kanban-PD2F4KWT.js");
4099
4236
  render(createElement(KanbanApp, { config: merged }), { exitOnCtrlC: false });
4100
4237
  }
4101
4238
  await runLoop(merged, {
@@ -4195,21 +4332,21 @@ function getVersion() {
4195
4332
  }
4196
4333
  var CURSOR_FREE_PLAN_ERROR = "Free plans can only use Auto";
4197
4334
  async function isCursorFreePlan() {
4198
- const { mkdtempSync: mkdtempSync8, unlinkSync: unlinkSync9, writeFileSync: writeFileSync11 } = await import("fs");
4199
- const tmpDir = mkdtempSync8(join12(tmpdir8(), "lisa-cursor-check-"));
4200
- const promptFile = join12(tmpDir, "prompt.txt");
4201
- writeFileSync11(promptFile, "test", "utf-8");
4335
+ const { mkdtempSync: mkdtempSync9, unlinkSync: unlinkSync10, writeFileSync: writeFileSync12 } = await import("fs");
4336
+ const tmpDir = mkdtempSync9(join13(tmpdir9(), "lisa-cursor-check-"));
4337
+ const promptFile = join13(tmpDir, "prompt.txt");
4338
+ writeFileSync12(promptFile, "test", "utf-8");
4202
4339
  try {
4203
4340
  const bin = ["agent", "cursor-agent"].find((b) => {
4204
4341
  try {
4205
- execSync8(`${b} --version`, { stdio: "ignore" });
4342
+ execSync9(`${b} --version`, { stdio: "ignore" });
4206
4343
  return true;
4207
4344
  } catch {
4208
4345
  return false;
4209
4346
  }
4210
4347
  });
4211
4348
  if (!bin) return false;
4212
- const output = execSync8(`${bin} -p "$(cat '${promptFile}')" --output-format text`, {
4349
+ const output = execSync9(`${bin} -p "$(cat '${promptFile}')" --output-format text`, {
4213
4350
  cwd: process.cwd(),
4214
4351
  encoding: "utf-8",
4215
4352
  timeout: 3e4
@@ -4220,11 +4357,11 @@ async function isCursorFreePlan() {
4220
4357
  return errorOutput.includes(CURSOR_FREE_PLAN_ERROR);
4221
4358
  } finally {
4222
4359
  try {
4223
- unlinkSync9(promptFile);
4360
+ unlinkSync10(promptFile);
4224
4361
  } catch {
4225
4362
  }
4226
4363
  try {
4227
- execSync8(`rm -rf ${tmpDir}`, { stdio: "ignore" });
4364
+ execSync9(`rm -rf ${tmpDir}`, { stdio: "ignore" });
4228
4365
  } catch {
4229
4366
  }
4230
4367
  }
@@ -4311,14 +4448,14 @@ function fetchCursorModels() {
4311
4448
  try {
4312
4449
  const bin = ["agent", "cursor-agent"].find((b) => {
4313
4450
  try {
4314
- execSync8(`${b} --version`, { stdio: "ignore" });
4451
+ execSync9(`${b} --version`, { stdio: "ignore" });
4315
4452
  return true;
4316
4453
  } catch {
4317
4454
  return false;
4318
4455
  }
4319
4456
  });
4320
4457
  if (!bin) return CURSOR_PREFERRED_MODELS;
4321
- const raw = execSync8(`${bin} --list-models`, { encoding: "utf-8", timeout: 1e4 });
4458
+ const raw = execSync9(`${bin} --list-models`, { encoding: "utf-8", timeout: 1e4 });
4322
4459
  const clean = raw.replace(/\x1b\[[0-9;]*[mGKHFA-Z]/g, "");
4323
4460
  const all = clean.split("\n").map((l) => l.trim()).filter((l) => l.includes(" - ")).map((l) => (l.split(" - ")[0] ?? "").trim()).filter(Boolean);
4324
4461
  const filtered = CURSOR_PREFERRED_MODELS.filter((m) => all.includes(m));
@@ -4329,7 +4466,7 @@ function fetchCursorModels() {
4329
4466
  }
4330
4467
  function fetchOpenCodeModels() {
4331
4468
  try {
4332
- const raw = execSync8("opencode models", { encoding: "utf-8", timeout: 1e4 });
4469
+ const raw = execSync9("opencode models", { encoding: "utf-8", timeout: 1e4 });
4333
4470
  const hasAnthropic = Boolean(process.env.ANTHROPIC_API_KEY);
4334
4471
  const hasGoogle = Boolean(
4335
4472
  process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY
@@ -4373,7 +4510,8 @@ async function runConfigWizard(existing) {
4373
4510
  copilot: "GitHub Copilot CLI",
4374
4511
  cursor: "Cursor Agent",
4375
4512
  goose: "Goose",
4376
- aider: "Aider"
4513
+ aider: "Aider",
4514
+ codex: "OpenAI Codex"
4377
4515
  };
4378
4516
  const providerModels = {
4379
4517
  claude: ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5", "claude-sonnet-4-5"],
@@ -4381,7 +4519,8 @@ async function runConfigWizard(existing) {
4381
4519
  // opencode: populated dynamically below (fetchOpenCodeModels)
4382
4520
  copilot: ["claude-opus-4.6", "claude-sonnet-4.6", "claude-haiku-4.5", "gpt-5.2"],
4383
4521
  goose: ["claude-sonnet-4-5", "claude-opus-4-5", "claude-haiku-4-5"],
4384
- aider: ["claude-opus-4-6", "claude-sonnet-4-5", "claude-haiku-4-5"]
4522
+ aider: ["claude-opus-4-6", "claude-sonnet-4-5", "claude-haiku-4-5"],
4523
+ codex: ["gpt-5.1-codex-mini", "gpt-5.1-codex-max", "gpt-5.2-codex", "gpt-5.2", "gpt-5.3-codex"]
4385
4524
  // cursor: populated dynamically below (fetchCursorModels)
4386
4525
  };
4387
4526
  const allProviders = await getAllProvidersWithAvailability();
@@ -4395,6 +4534,7 @@ async function runConfigWizard(existing) {
4395
4534
  ${pc2.bold("Gemini CLI")} ${pc2.dim("npm i -g @google/gemini-cli")}
4396
4535
  ${pc2.bold("OpenCode")} ${pc2.dim("npm i -g opencode")}
4397
4536
  ${pc2.bold("GitHub Copilot CLI")} ${pc2.dim("npm i -g @github/copilot-cli")}
4537
+ ${pc2.bold("OpenAI Codex")} ${pc2.dim("npm i -g @openai/codex")}
4398
4538
  ${pc2.bold("Goose")} ${pc2.dim("https://block.github.io/goose")}
4399
4539
  ${pc2.bold("Aider")} ${pc2.dim("pip install aider-chat")}`
4400
4540
  );
@@ -4651,6 +4791,9 @@ Then reload: ${pc2.cyan(`source ${shell}`)}`
4651
4791
  logs: { dir: ".lisa/logs", format: "text" }
4652
4792
  };
4653
4793
  saveConfig(cfg);
4794
+ if (ensureLogsGitignore(process.cwd())) {
4795
+ clack.log.info("Added .lisa/logs/* to .gitignore");
4796
+ }
4654
4797
  clack.outro(
4655
4798
  `${pc2.green("All set!")} Config saved to ${pc2.cyan(".lisa/config.yaml")}
4656
4799
  Run ${pc2.bold(pc2.cyan("lisa run"))} to start resolving issues.`
@@ -4682,12 +4825,12 @@ async function detectGitHubMethod() {
4682
4825
  }
4683
4826
  async function detectGitRepos() {
4684
4827
  const cwd = process.cwd();
4685
- if (existsSync7(join12(cwd, ".git"))) {
4828
+ if (existsSync7(join13(cwd, ".git"))) {
4686
4829
  clack.log.info("Found a git repository in the current directory.");
4687
4830
  return [];
4688
4831
  }
4689
4832
  const entries = readdirSync(cwd, { withFileTypes: true });
4690
- const gitDirs = entries.filter((e) => e.isDirectory() && existsSync7(join12(cwd, e.name, ".git"))).map((e) => e.name);
4833
+ const gitDirs = entries.filter((e) => e.isDirectory() && existsSync7(join13(cwd, e.name, ".git"))).map((e) => e.name);
4691
4834
  if (gitDirs.length === 0) {
4692
4835
  return [];
4693
4836
  }
@@ -4697,7 +4840,7 @@ async function detectGitRepos() {
4697
4840
  });
4698
4841
  if (clack.isCancel(selected)) return process.exit(0);
4699
4842
  return selected.map((dir) => ({
4700
- name: getGitRepoName(join12(cwd, dir)) ?? dir,
4843
+ name: getGitRepoName(join13(cwd, dir)) ?? dir,
4701
4844
  path: `./${dir}`,
4702
4845
  match: "",
4703
4846
  base_branch: ""
@@ -4705,7 +4848,7 @@ async function detectGitRepos() {
4705
4848
  }
4706
4849
  function detectDefaultBranch(repoPath) {
4707
4850
  try {
4708
- const ref = execSync8("git symbolic-ref refs/remotes/origin/HEAD --short", {
4851
+ const ref = execSync9("git symbolic-ref refs/remotes/origin/HEAD --short", {
4709
4852
  cwd: repoPath,
4710
4853
  encoding: "utf-8"
4711
4854
  }).trim();
@@ -4716,7 +4859,7 @@ function detectDefaultBranch(repoPath) {
4716
4859
  }
4717
4860
  function getGitRepoName(repoPath) {
4718
4861
  try {
4719
- const url = execSync8("git remote get-url origin", { cwd: repoPath, encoding: "utf-8" }).trim();
4862
+ const url = execSync9("git remote get-url origin", { cwd: repoPath, encoding: "utf-8" }).trim();
4720
4863
  const match = url.match(/\/([^/]+?)(?:\.git)?$/) ?? url.match(/:([^/]+?)(?:\.git)?$/);
4721
4864
  return match?.[1] ?? null;
4722
4865
  } catch {
@@ -29,7 +29,32 @@ function formatElapsed(ms) {
29
29
  if (minutes > 0) return `${minutes}m ${remainingSeconds}s`;
30
30
  return `${seconds}s`;
31
31
  }
32
- function Card({ card, isSelected = false }) {
32
+ function wrapTitle(title, maxWidth) {
33
+ if (title.length <= maxWidth) return [title, ""];
34
+ const words = title.split(" ");
35
+ let line1 = "";
36
+ let i = 0;
37
+ for (; i < words.length; i++) {
38
+ const word = words[i] ?? "";
39
+ const candidate = line1 ? `${line1} ${word}` : word;
40
+ if (candidate.length > maxWidth) break;
41
+ line1 = candidate;
42
+ }
43
+ if (!line1) {
44
+ line1 = title.slice(0, maxWidth);
45
+ const rest = title.slice(maxWidth);
46
+ const line22 = rest.length > maxWidth ? `${rest.slice(0, maxWidth - 1)}\u2026` : rest;
47
+ return [line1, line22];
48
+ }
49
+ const remaining = words.slice(i).join(" ");
50
+ const line2 = remaining.length > maxWidth ? `${remaining.slice(0, maxWidth - 1)}\u2026` : remaining;
51
+ return [line1, line2];
52
+ }
53
+ function Card({
54
+ card,
55
+ isSelected = false,
56
+ paused = false
57
+ }) {
33
58
  const [now, setNow] = useState(Date.now());
34
59
  useEffect(() => {
35
60
  if (card.column !== "in_progress") return;
@@ -53,7 +78,9 @@ function Card({ card, isSelected = false }) {
53
78
  }
54
79
  const selectionBar = isSelected ? "\u2590" : " ";
55
80
  const selectionColor = isSelected ? "yellow" : "white";
56
- const truncated = card.title.length > 30 ? `${card.title.slice(0, 27)}\u2026` : card.title;
81
+ const CARD_TITLE_WIDTH = 28;
82
+ const [titleLine1, titleLine2] = wrapTitle(card.title, CARD_TITLE_WIDTH);
83
+ const isPausedInProgress = paused && card.column === "in_progress";
57
84
  return /* @__PURE__ */ jsxs(
58
85
  Box,
59
86
  {
@@ -61,7 +88,7 @@ function Card({ card, isSelected = false }) {
61
88
  paddingX: 0,
62
89
  marginBottom: 0,
63
90
  borderStyle: "single",
64
- borderColor: card.hasError ? "red" : isSelected ? "yellow" : "gray",
91
+ borderColor: card.hasError ? "red" : isPausedInProgress ? "gray" : isSelected ? "yellow" : "gray",
65
92
  children: [
66
93
  /* @__PURE__ */ jsx(Text, { color: selectionColor, children: selectionBar }),
67
94
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: [
@@ -69,10 +96,11 @@ function Card({ card, isSelected = false }) {
69
96
  /* @__PURE__ */ jsx(Text, { color: "yellow", bold: isSelected, children: card.id }),
70
97
  /* @__PURE__ */ jsx(Text, { color: statusColor, children: statusGlyph })
71
98
  ] }),
72
- /* @__PURE__ */ jsx(Text, { bold: isSelected, dimColor: !isSelected, children: truncated }),
99
+ /* @__PURE__ */ jsx(Text, { bold: isSelected, dimColor: !isSelected, children: titleLine1 }),
100
+ /* @__PURE__ */ jsx(Text, { bold: isSelected, dimColor: !isSelected, children: titleLine2 }),
73
101
  card.column === "in_progress" && card.startedAt !== void 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "row", marginTop: 0, children: [
74
- /* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
75
- /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
102
+ isPausedInProgress ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u23F8" }) : /* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
103
+ /* @__PURE__ */ jsxs(Text, { color: isPausedInProgress ? "gray" : "yellow", dimColor: isPausedInProgress, children: [
76
104
  " ",
77
105
  formatElapsed(now - card.startedAt)
78
106
  ] })
@@ -90,9 +118,15 @@ function Card({ card, isSelected = false }) {
90
118
 
91
119
  // src/ui/column.tsx
92
120
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
93
- var CARD_HEIGHT = 5;
121
+ var CARD_HEIGHT = 6;
94
122
  var HEADER_ROWS = 4;
95
- function Column({ label, cards, isFocused = false, activeCardIndex = 0 }) {
123
+ function Column({
124
+ label,
125
+ cards,
126
+ isFocused = false,
127
+ activeCardIndex = 0,
128
+ paused = false
129
+ }) {
96
130
  const terminalRows = process.stdout.rows ?? 24;
97
131
  const visibleCount = Math.max(1, Math.floor((terminalRows - HEADER_ROWS) / CARD_HEIGHT));
98
132
  let scrollOffset = 0;
@@ -134,7 +168,7 @@ function Column({ label, cards, isFocused = false, activeCardIndex = 0 }) {
134
168
  visibleCards.map((card, idx) => {
135
169
  const absoluteIdx = scrollOffset + idx;
136
170
  const isSelected = isFocused && absoluteIdx === activeCardIndex;
137
- return /* @__PURE__ */ jsx2(Card, { card, isSelected }, card.id);
171
+ return /* @__PURE__ */ jsx2(Card, { card, isSelected, paused }, card.id);
138
172
  }),
139
173
  cards.length === 0 && /* @__PURE__ */ jsx2(Box2, { justifyContent: "center", paddingY: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, children: "\u2014 empty \u2014" }) }),
140
174
  hiddenBelow > 0 && /* @__PURE__ */ jsx2(Box2, { justifyContent: "center", children: /* @__PURE__ */ jsx2(Text2, { color: "yellow", dimColor: true, children: `\u2193 ${hiddenBelow} more` }) })
@@ -158,7 +192,8 @@ function Board({
158
192
  isEmpty,
159
193
  workComplete,
160
194
  activeColIndex = 0,
161
- activeCardIndex = 0
195
+ activeCardIndex = 0,
196
+ paused = false
162
197
  }) {
163
198
  const backlog = cards.filter((c) => c.column === "backlog");
164
199
  const inProgress = cards.filter((c) => c.column === "in_progress");
@@ -197,7 +232,8 @@ function Board({
197
232
  label: labels.backlog,
198
233
  cards: backlog,
199
234
  isFocused: activeColIndex === 0,
200
- activeCardIndex: activeColIndex === 0 ? activeCardIndex : 0
235
+ activeCardIndex: activeColIndex === 0 ? activeCardIndex : 0,
236
+ paused
201
237
  }
202
238
  ),
203
239
  /* @__PURE__ */ jsx3(
@@ -206,7 +242,8 @@ function Board({
206
242
  label: labels.inProgress,
207
243
  cards: inProgress,
208
244
  isFocused: activeColIndex === 1,
209
- activeCardIndex: activeColIndex === 1 ? activeCardIndex : 0
245
+ activeCardIndex: activeColIndex === 1 ? activeCardIndex : 0,
246
+ paused
210
247
  }
211
248
  ),
212
249
  /* @__PURE__ */ jsx3(
@@ -215,7 +252,8 @@ function Board({
215
252
  label: labels.done,
216
253
  cards: done,
217
254
  isFocused: activeColIndex === 2,
218
- activeCardIndex: activeColIndex === 2 ? activeCardIndex : 0
255
+ activeCardIndex: activeColIndex === 2 ? activeCardIndex : 0,
256
+ paused
219
257
  }
220
258
  )
221
259
  ] })
@@ -234,6 +272,19 @@ function formatElapsed2(ms) {
234
272
  if (minutes > 0) return `${minutes}m ${remainingSeconds}s`;
235
273
  return `${seconds}s`;
236
274
  }
275
+ function hyperlink(url, text) {
276
+ return `\x1B]8;;${url}\x07${text}\x1B]8;;\x07`;
277
+ }
278
+ function logLineColor(line) {
279
+ if (/\berror\b|✖/i.test(line)) return "red";
280
+ if (/\bwarn(ing)?\b/i.test(line)) return "yellow";
281
+ if (/✔|\bsuccess\b/i.test(line)) return "green";
282
+ return "white";
283
+ }
284
+ function scrollBar(pct, width = 8) {
285
+ const filled = Math.round(pct / 100 * width);
286
+ return "\u2593".repeat(filled) + "\u2591".repeat(width - filled);
287
+ }
237
288
  function statusLabel(column, hasError) {
238
289
  if (hasError) return { text: "FAILED", color: "red" };
239
290
  if (column === "in_progress") return { text: "IN PROGRESS", color: "yellow" };
@@ -292,7 +343,7 @@ function IssueDetail({ card, onBack }) {
292
343
  const separatorInner = Math.max(0, terminalCols - SIDEBAR_TOTAL_WIDTH - 4);
293
344
  const separator = `\u2560${"\u2550".repeat(Math.max(0, separatorInner - 2))}\u2563`;
294
345
  const totalLines = lines.length;
295
- const scrollPct = totalLines <= bodyRows ? "100%" : `${Math.round((startLine + bodyRows) / totalLines * 100)}%`;
346
+ const scrollPctNum = totalLines <= bodyRows ? 100 : Math.round((startLine + bodyRows) / totalLines * 100);
296
347
  return /* @__PURE__ */ jsxs4(
297
348
  Box4,
298
349
  {
@@ -323,21 +374,24 @@ function IssueDetail({ card, onBack }) {
323
374
  /* @__PURE__ */ jsx4(Box4, { marginTop: 0, children: /* @__PURE__ */ jsx4(Text4, { color: "white", bold: true, children: card.title }) }),
324
375
  card.prUrl !== void 0 && card.prUrl.length > 0 && /* @__PURE__ */ jsxs4(Box4, { marginTop: 0, children: [
325
376
  /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: "PR: " }),
326
- /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: card.prUrl })
377
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: hyperlink(card.prUrl, card.prUrl) })
327
378
  ] }),
328
379
  /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: separator }) }),
329
380
  /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", justifyContent: "space-between", children: [
330
381
  /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "PROVIDER OUTPUT" }),
331
- userScrolled && /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: `scroll ${scrollPct}` }),
382
+ userScrolled && /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: scrollBar(scrollPctNum) }),
332
383
  !userScrolled && totalLines > bodyRows && /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "auto-scroll" })
333
384
  ] }),
334
385
  /* @__PURE__ */ jsx4(Box4, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: card.outputLog.length === 0 ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", marginTop: 1, children: [
335
386
  /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
336
387
  /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: " Waiting for provider output..." })
337
- ] }) : visibleLines.map((line, i) => (
338
- // biome-ignore lint/suspicious/noArrayIndexKey: log lines have no stable key
339
- /* @__PURE__ */ jsx4(Text4, { color: "white", dimColor: true, children: line }, i)
340
- )) })
388
+ ] }) : visibleLines.map((line, i) => {
389
+ const color = logLineColor(line);
390
+ return (
391
+ // biome-ignore lint/suspicious/noArrayIndexKey: log lines have no stable key
392
+ /* @__PURE__ */ jsx4(Text4, { color, dimColor: color === "white", children: line }, i)
393
+ );
394
+ }) })
341
395
  ]
342
396
  }
343
397
  );
@@ -402,7 +456,7 @@ function Sidebar({ provider, source, cwd, activeView, paused = false }) {
402
456
  /* @__PURE__ */ jsx5(Box5, { flexGrow: 1 }),
403
457
  /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
404
458
  activeView === "board" ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
405
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[Tab] next column" }),
459
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[\u2190\u2192] columns " }),
406
460
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[\u2191\u2193] navigate " }),
407
461
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[\u21B5] view detail " }),
408
462
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: paused ? "[p] resume " : "[p] pause " }),
@@ -425,6 +479,7 @@ function KanbanApp({ config }) {
425
479
  const [activeColIndex, setActiveColIndex] = useState3(0);
426
480
  const [activeCardIndex, setActiveCardIndex] = useState3(0);
427
481
  const [paused, setPaused] = useState3(false);
482
+ const [selectedCardId, setSelectedCardId] = useState3(null);
428
483
  useEffect3(() => {
429
484
  const onExit = () => exit();
430
485
  kanbanEmitter.on("tui:exit", onExit);
@@ -447,13 +502,34 @@ function KanbanApp({ config }) {
447
502
  }, [inProgress, workComplete]);
448
503
  const done = cards.filter((c) => c.column === "done");
449
504
  const columnCards = [backlog, inProgress, done];
505
+ useEffect3(() => {
506
+ if (!selectedCardId || activeView !== "detail") return;
507
+ const card = cards.find((c) => c.id === selectedCardId);
508
+ if (!card) return;
509
+ const colIndexMap = {
510
+ backlog: 0,
511
+ in_progress: 1,
512
+ done: 2
513
+ };
514
+ const newColIndex = colIndexMap[card.column];
515
+ const colCards = cards.filter((c) => c.column === card.column);
516
+ const newCardIndex = Math.max(
517
+ 0,
518
+ colCards.findIndex((c) => c.id === selectedCardId)
519
+ );
520
+ setActiveColIndex(newColIndex);
521
+ setActiveCardIndex(newCardIndex);
522
+ }, [cards, selectedCardId, activeView]);
450
523
  useInput2((input, key) => {
451
524
  if (input === "q") {
452
525
  process.emit("SIGINT");
453
526
  return;
454
527
  }
455
528
  if (activeView === "detail") {
456
- if (key.escape) setActiveView("board");
529
+ if (key.escape) {
530
+ setActiveView("board");
531
+ setSelectedCardId(null);
532
+ }
457
533
  return;
458
534
  }
459
535
  if (input === "p") {
@@ -462,14 +538,14 @@ function KanbanApp({ config }) {
462
538
  kanbanEmitter.emit(next ? "loop:pause" : "loop:resume");
463
539
  return;
464
540
  }
465
- if (key.tab && !key.shift) {
541
+ if (key.rightArrow) {
466
542
  const nextCol = (activeColIndex + 1) % 3;
467
543
  setActiveColIndex(nextCol);
468
544
  const colLen = columnCards[nextCol]?.length ?? 0;
469
545
  setActiveCardIndex(Math.min(activeCardIndex, Math.max(0, colLen - 1)));
470
546
  return;
471
547
  }
472
- if (key.tab && key.shift) {
548
+ if (key.leftArrow) {
473
549
  const prevCol = (activeColIndex + 2) % 3;
474
550
  setActiveColIndex(prevCol);
475
551
  const colLen = columnCards[prevCol]?.length ?? 0;
@@ -486,8 +562,11 @@ function KanbanApp({ config }) {
486
562
  return;
487
563
  }
488
564
  if (key.return) {
489
- const colLen = columnCards[activeColIndex]?.length ?? 0;
490
- if (colLen > 0) setActiveView("detail");
565
+ const card = columnCards[activeColIndex]?.[activeCardIndex];
566
+ if (card) {
567
+ setSelectedCardId(card.id);
568
+ setActiveView("detail");
569
+ }
491
570
  }
492
571
  });
493
572
  const labels = {
@@ -495,7 +574,7 @@ function KanbanApp({ config }) {
495
574
  inProgress: config.source_config.in_progress,
496
575
  done: config.source_config.done
497
576
  };
498
- const selectedCard = activeView === "detail" ? columnCards[activeColIndex]?.[activeCardIndex] ?? null : null;
577
+ const selectedCard = activeView === "detail" && selectedCardId ? cards.find((c) => c.id === selectedCardId) ?? null : null;
499
578
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "row", height: process.stdout.rows, children: [
500
579
  /* @__PURE__ */ jsx6(
501
580
  Sidebar,
@@ -515,9 +594,19 @@ function KanbanApp({ config }) {
515
594
  isEmpty,
516
595
  workComplete,
517
596
  activeColIndex,
518
- activeCardIndex
597
+ activeCardIndex,
598
+ paused
599
+ }
600
+ ) : /* @__PURE__ */ jsx6(
601
+ IssueDetail,
602
+ {
603
+ card: selectedCard,
604
+ onBack: () => {
605
+ setActiveView("board");
606
+ setSelectedCardId(null);
607
+ }
519
608
  }
520
- ) : /* @__PURE__ */ jsx6(IssueDetail, { card: selectedCard, onBack: () => setActiveView("board") })
609
+ )
521
610
  ] });
522
611
  }
523
612
  export {
package/package.json CHANGED
@@ -1,62 +1,60 @@
1
1
  {
2
- "name": "@tarcisiopgs/lisa",
3
- "version": "1.7.7",
4
- "description": "Autonomous issue resolver",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "lisa": "dist/index.js"
9
- },
10
- "scripts": {
11
- "build": "tsup",
12
- "dev": "tsx src/index.ts",
13
- "check": "biome check src/",
14
- "format": "biome format --write src/",
15
- "lint": "biome lint src/",
16
- "typecheck": "tsc --noEmit",
17
- "test": "vitest run",
18
- "test:watch": "vitest",
19
- "test:coverage": "vitest run --coverage",
20
- "ci": "concurrently -n lint,typecheck,test \"pnpm lint\" \"pnpm typecheck\" \"pnpm test\"",
21
- "prepare": "husky"
22
- },
23
- "dependencies": {
24
- "@clack/prompts": "^1.0.1",
25
- "citty": "^0.2.1",
26
- "execa": "^9.6.1",
27
- "ink": "^6.8.0",
28
- "ink-spinner": "^5.0.0",
29
- "picocolors": "^1.1.1",
30
- "react": "^19.2.4",
31
- "yaml": "^2.8.2"
32
- },
33
- "devDependencies": {
34
- "@biomejs/biome": "^2.4.3",
35
- "@types/node": "^22.13.4",
36
- "@types/react": "^19.2.14",
37
- "@vitest/coverage-v8": "^4.0.18",
38
- "concurrently": "^9.2.1",
39
- "husky": "^9.1.7",
40
- "lint-staged": "^16.2.7",
41
- "tsup": "^8.4.0",
42
- "tsx": "^4.19.3",
43
- "typescript": "^5.9.3",
44
- "vitest": "^4.0.18"
45
- },
46
- "packageManager": "pnpm@10.29.3",
47
- "publishConfig": {
48
- "access": "public"
49
- },
50
- "repository": {
51
- "type": "git",
52
- "url": "https://github.com/tarcisiopgs/lisa.git"
53
- },
54
- "files": [
55
- "dist"
56
- ],
57
- "lint-staged": {
58
- "*.{ts,tsx}": [
59
- "biome check --write"
60
- ]
61
- }
62
- }
2
+ "name": "@tarcisiopgs/lisa",
3
+ "version": "1.8.1",
4
+ "description": "Autonomous issue resolver",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "lisa": "dist/index.js"
9
+ },
10
+ "dependencies": {
11
+ "@clack/prompts": "^1.0.1",
12
+ "citty": "^0.2.1",
13
+ "execa": "^9.6.1",
14
+ "ink": "^6.8.0",
15
+ "ink-spinner": "^5.0.0",
16
+ "picocolors": "^1.1.1",
17
+ "react": "^19.2.4",
18
+ "yaml": "^2.8.2"
19
+ },
20
+ "devDependencies": {
21
+ "@biomejs/biome": "^2.4.3",
22
+ "@types/node": "^22.13.4",
23
+ "@types/react": "^19.2.14",
24
+ "@vitest/coverage-v8": "^4.0.18",
25
+ "concurrently": "^9.2.1",
26
+ "husky": "^9.1.7",
27
+ "lint-staged": "^16.2.7",
28
+ "tsup": "^8.4.0",
29
+ "tsx": "^4.19.3",
30
+ "typescript": "^5.9.3",
31
+ "vitest": "^4.0.18"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/tarcisiopgs/lisa.git"
39
+ },
40
+ "files": [
41
+ "dist"
42
+ ],
43
+ "lint-staged": {
44
+ "*.{ts,tsx}": [
45
+ "biome check --write"
46
+ ]
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "dev": "tsx src/index.ts",
51
+ "check": "biome check src/",
52
+ "format": "biome format --write src/",
53
+ "lint": "biome lint src/",
54
+ "typecheck": "tsc --noEmit",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "test:coverage": "vitest run --coverage",
58
+ "ci": "concurrently -n lint,typecheck,test \"pnpm lint\" \"pnpm typecheck\" \"pnpm test\""
59
+ }
60
+ }