@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/README.md +184 -112
- package/dist/chunk-KAME5MG7.js +54 -0
- package/dist/index.js +2033 -1141
- package/dist/kanban-KIPKQ2IL.js +127 -0
- package/package.json +59 -57
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
|
|
7
|
+
import { execSync as execSync8 } from "child_process";
|
|
5
8
|
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6 } from "fs";
|
|
6
|
-
import {
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 (
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
async function
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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/
|
|
222
|
-
import { appendFileSync, existsSync as
|
|
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 (!
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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/
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
357
|
-
|
|
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
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
475
|
-
if (
|
|
476
|
-
|
|
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
|
|
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 =
|
|
521
|
-
if (!
|
|
428
|
+
const packageJsonPath = join2(cwd, "package.json");
|
|
429
|
+
if (!existsSync4(packageJsonPath)) return null;
|
|
522
430
|
try {
|
|
523
|
-
const content = JSON.parse(
|
|
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(
|
|
440
|
+
function buildImplementPrompt(issue2, config2, testRunner, pm) {
|
|
533
441
|
if (config2.workflow === "worktree") {
|
|
534
|
-
return buildWorktreePrompt(
|
|
442
|
+
return buildWorktreePrompt(issue2, testRunner, pm, config2.base_branch);
|
|
535
443
|
}
|
|
536
|
-
return buildBranchPrompt(
|
|
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 \`
|
|
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
|
|
583
|
-
|
|
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
|
|
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:** ${
|
|
607
|
-
- **Title:** ${
|
|
608
|
-
- **URL:** ${
|
|
502
|
+
- **ID:** ${issue2.id}
|
|
503
|
+
- **Title:** ${issue2.title}
|
|
504
|
+
- **URL:** ${issue2.url}
|
|
609
505
|
|
|
610
506
|
### Description
|
|
611
507
|
|
|
612
|
-
${
|
|
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.**
|
|
629
|
-
|
|
630
|
-
\`git branch -m
|
|
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. **
|
|
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>", "
|
|
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(
|
|
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
|
|
661
|
-
const
|
|
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 =
|
|
665
|
-
return `You are an autonomous implementation agent. Your job is to implement
|
|
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:** ${
|
|
671
|
-
- **Title:** ${
|
|
672
|
-
- **URL:** ${
|
|
573
|
+
- **ID:** ${issue2.id}
|
|
574
|
+
- **Title:** ${issue2.title}
|
|
575
|
+
- **URL:** ${issue2.url}
|
|
673
576
|
|
|
674
577
|
### Description
|
|
675
578
|
|
|
676
|
-
${
|
|
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/${
|
|
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/${
|
|
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. **
|
|
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>", "
|
|
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(
|
|
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
|
|
756
|
-
|
|
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:** ${
|
|
766
|
-
- **Title:** ${
|
|
767
|
-
- **URL:** ${
|
|
647
|
+
- **ID:** ${issue2.id}
|
|
648
|
+
- **Title:** ${issue2.title}
|
|
649
|
+
- **URL:** ${issue2.url}
|
|
768
650
|
|
|
769
651
|
### Description
|
|
770
652
|
|
|
771
|
-
${
|
|
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/${
|
|
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. **
|
|
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>", "
|
|
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(
|
|
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 =
|
|
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:** ${
|
|
827
|
-
- **Title:** ${
|
|
828
|
-
- **URL:** ${
|
|
715
|
+
- **ID:** ${issue2.id}
|
|
716
|
+
- **Title:** ${issue2.title}
|
|
717
|
+
- **URL:** ${issue2.url}
|
|
829
718
|
|
|
830
719
|
### Description
|
|
831
720
|
|
|
832
|
-
${
|
|
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(
|
|
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:** ${
|
|
889
|
-
- **Title:** ${
|
|
890
|
-
- **URL:** ${
|
|
780
|
+
- **ID:** ${issue2.id}
|
|
781
|
+
- **Title:** ${issue2.title}
|
|
782
|
+
- **URL:** ${issue2.url}
|
|
891
783
|
|
|
892
784
|
### Description
|
|
893
785
|
|
|
894
|
-
${
|
|
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/${
|
|
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. **
|
|
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>", "
|
|
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
|
|
946
|
-
import { dirname as dirname2, join as
|
|
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
|
|
846
|
+
return join3(dir, GUARDRAILS_FILE);
|
|
952
847
|
}
|
|
953
848
|
function readGuardrails(dir) {
|
|
954
849
|
const path = guardrailsPath(dir);
|
|
955
|
-
if (!
|
|
850
|
+
if (!existsSync5(path)) return "";
|
|
956
851
|
try {
|
|
957
|
-
return
|
|
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 (!
|
|
882
|
+
if (!existsSync5(guardrailsDir)) {
|
|
988
883
|
mkdirSync3(guardrailsDir, { recursive: true });
|
|
989
884
|
}
|
|
990
|
-
const existing =
|
|
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/
|
|
1037
|
-
import { execSync, spawn
|
|
1038
|
-
import { appendFileSync as
|
|
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
|
|
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 =
|
|
1089
|
+
supportsNativeWorktree = false;
|
|
1090
|
+
// --worktree flag requires a TTY and hangs in non-interactive mode
|
|
1116
1091
|
async isAvailable() {
|
|
1117
1092
|
try {
|
|
1118
|
-
|
|
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 =
|
|
1127
|
-
const promptFile =
|
|
1128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1192
|
-
import { appendFileSync as
|
|
1193
|
-
import { tmpdir as
|
|
1194
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
1208
|
-
const promptFile =
|
|
1209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1265
|
-
import { appendFileSync as
|
|
1266
|
-
import { tmpdir as
|
|
1267
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
1294
|
-
const promptFile =
|
|
1295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1355
|
-
import { appendFileSync as
|
|
1356
|
-
import { tmpdir as
|
|
1357
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
1371
|
-
const promptFile =
|
|
1372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1429
|
-
import { appendFileSync as
|
|
1430
|
-
import { tmpdir as
|
|
1431
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
1445
|
-
const promptFile =
|
|
1446
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
|
1710
|
-
const activeBlockers =
|
|
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(
|
|
2424
|
+
unblocked.push(issue3);
|
|
1713
2425
|
} else {
|
|
1714
|
-
blocked.push({ identifier:
|
|
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
|
|
1732
|
-
if (!
|
|
2443
|
+
const issue2 = unblocked[0];
|
|
2444
|
+
if (!issue2) return null;
|
|
1733
2445
|
return {
|
|
1734
|
-
id:
|
|
1735
|
-
title:
|
|
1736
|
-
description:
|
|
1737
|
-
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
|
|
1893
|
-
function
|
|
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:
|
|
1907
|
-
signal: AbortSignal.timeout(
|
|
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
|
|
2002
|
-
const
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
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
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
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
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
3356
|
+
let issue2;
|
|
2445
3357
|
try {
|
|
2446
|
-
|
|
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 (!
|
|
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: ${
|
|
2465
|
-
setTitle(`Lisa \u2014 ${
|
|
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
|
-
|
|
2470
|
-
|
|
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:
|
|
3387
|
+
activeCleanup = { issueId: issue2.id, previousStatus, source };
|
|
2475
3388
|
let sessionResult;
|
|
2476
3389
|
try {
|
|
2477
|
-
sessionResult = config2.workflow === "worktree" ? await runWorktreeSession(config2,
|
|
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 ${
|
|
3394
|
+
`Unhandled error in session for ${issue2.id}: ${err instanceof Error ? err.message : String(err)}`
|
|
2482
3395
|
);
|
|
2483
3396
|
try {
|
|
2484
|
-
await source.updateStatus(
|
|
2485
|
-
ok(`Reverted ${
|
|
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 ${
|
|
3413
|
+
error(`All models failed for ${issue2.id}. Reverting to "${previousStatus}".`);
|
|
2501
3414
|
logAttemptHistory(sessionResult);
|
|
2502
3415
|
try {
|
|
2503
|
-
await source.updateStatus(
|
|
2504
|
-
ok(`Reverted ${
|
|
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 ${
|
|
3444
|
+
`Session succeeded but no PRs created for ${issue2.id}. Reverting to "${previousStatus}".`
|
|
2531
3445
|
);
|
|
2532
3446
|
try {
|
|
2533
|
-
await source.updateStatus(
|
|
2534
|
-
ok(`Reverted ${
|
|
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(
|
|
2554
|
-
ok(`Attached PR to ${
|
|
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(
|
|
2563
|
-
ok(`Updated ${
|
|
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 ${
|
|
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 ${
|
|
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,
|
|
3515
|
+
function findRepoConfig(config2, issue2) {
|
|
2598
3516
|
if (config2.repos.length === 0) return void 0;
|
|
2599
|
-
if (
|
|
2600
|
-
const match = config2.repos.find((r) => r.name ===
|
|
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 &&
|
|
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,
|
|
3544
|
+
async function runWorktreeSession(config2, issue2, logFile, session, models) {
|
|
2641
3545
|
if (config2.repos.length > 1) {
|
|
2642
|
-
return runWorktreeMultiRepoSession(config2,
|
|
3546
|
+
return runWorktreeMultiRepoSession(config2, issue2, logFile, session, models);
|
|
2643
3547
|
}
|
|
2644
3548
|
const workspace = resolve5(config2.workspace);
|
|
2645
|
-
const repoPath = determineRepoPath(config2.repos,
|
|
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
|
-
|
|
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,
|
|
3564
|
+
return runManualWorktreeSession(config2, issue2, logFile, session, models, repoPath, defaultBranch);
|
|
2661
3565
|
}
|
|
2662
|
-
async function runNativeWorktreeSession(config2,
|
|
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,
|
|
3573
|
+
const repo = findRepoConfig(config2, issue2);
|
|
2670
3574
|
if (repo?.lifecycle) {
|
|
2671
|
-
startSpinner(`${
|
|
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 ${
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
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
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
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
|
-
|
|
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 ${
|
|
2778
|
-
return {
|
|
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,
|
|
2781
|
-
const branchName = generateBranchName(
|
|
2782
|
-
startSpinner(`${
|
|
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,
|
|
3667
|
+
const repo = findRepoConfig(config2, issue2);
|
|
2806
3668
|
if (repo?.lifecycle) {
|
|
2807
|
-
startSpinner(`${
|
|
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 ${
|
|
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
|
|
2832
|
-
|
|
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:
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
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
|
-
|
|
2922
|
-
prUrls
|
|
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
|
-
|
|
3738
|
+
ok(`PR created by provider: ${manifest.prUrl}`);
|
|
2927
3739
|
await cleanupWorktree(repoPath, worktreePath);
|
|
2928
|
-
ok(`Session ${session} complete for ${
|
|
2929
|
-
return {
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
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 ${
|
|
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,
|
|
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(
|
|
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(`${
|
|
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
|
|
3048
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
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:
|
|
3140
|
-
prUrl,
|
|
3931
|
+
branch: manifest.branch ?? branchName,
|
|
3932
|
+
prUrl: manifest.prUrl,
|
|
3141
3933
|
fallback: result
|
|
3142
3934
|
};
|
|
3143
3935
|
}
|
|
3144
|
-
async function runBranchSession(config2,
|
|
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
|
|
3152
|
-
const
|
|
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(`${
|
|
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 ${
|
|
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:
|
|
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
|
-
|
|
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 ${
|
|
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 (
|
|
3225
|
-
error(`
|
|
3226
|
-
|
|
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
|
-
|
|
3257
|
-
ok(`Session ${session} complete for ${
|
|
3258
|
-
return {
|
|
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": {
|
|
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-
|
|
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
|
-
|
|
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?
|
|
3466
|
-
options: availableModels.map((m
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 {
|