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