@tarcisiopgs/lisa 1.26.2 → 1.28.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.
@@ -0,0 +1,3924 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ GitHubIssuesSource,
4
+ GitLabIssuesSource,
5
+ REQUEST_TIMEOUT_MS,
6
+ createApiClient,
7
+ error,
8
+ getOutputMode,
9
+ kanbanEmitter,
10
+ log,
11
+ normalizeLabels,
12
+ ok,
13
+ warn
14
+ } from "./chunk-5N4BWHIT.js";
15
+ import {
16
+ appendEntry,
17
+ buildGuardrailsSection,
18
+ extractContext,
19
+ extractErrorType
20
+ } from "./chunk-3EOEDL3T.js";
21
+ import {
22
+ getManifestPath,
23
+ getPlanPath
24
+ } from "./chunk-7OCDGYDM.js";
25
+ import {
26
+ appendPrAttribution,
27
+ appendPrBody,
28
+ formatError,
29
+ stripProviderAttribution
30
+ } from "./chunk-2TW2MJXF.js";
31
+
32
+ // src/providers/aider.ts
33
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
34
+ import { tmpdir as tmpdir2 } from "os";
35
+ import { join as join2 } from "path";
36
+
37
+ // src/providers/output-buffer.ts
38
+ import { appendFileSync } from "fs";
39
+ var logWriteWarned = false;
40
+ function safeAppendLog(logFile, text) {
41
+ try {
42
+ appendFileSync(logFile, text);
43
+ } catch (err) {
44
+ if (!logWriteWarned) {
45
+ logWriteWarned = true;
46
+ warn(`Failed to write to log file ${logFile}: ${formatError(err)}`);
47
+ }
48
+ }
49
+ }
50
+ function escapeShellPath(filePath) {
51
+ if (/[\x00-\x1f\x7f]/.test(filePath)) {
52
+ throw new Error(`Path contains control characters: ${filePath}`);
53
+ }
54
+ return filePath.replace(/'/g, "'\\''");
55
+ }
56
+ var OutputBuffer = class {
57
+ chunks = [];
58
+ totalLength = 0;
59
+ maxBytes;
60
+ constructor(maxBytes = 10 * 1024 * 1024) {
61
+ this.maxBytes = maxBytes;
62
+ }
63
+ push(text) {
64
+ this.chunks.push(text);
65
+ this.totalLength += text.length;
66
+ this.evict();
67
+ }
68
+ toString() {
69
+ if (this.chunks.length === 0) return "";
70
+ if (this.chunks.length === 1) return this.chunks[0];
71
+ const result = this.chunks.join("");
72
+ this.chunks = [result];
73
+ return result;
74
+ }
75
+ evict() {
76
+ if (this.totalLength <= this.maxBytes) return;
77
+ while (this.chunks.length > 1 && this.totalLength > this.maxBytes) {
78
+ const removed = this.chunks.shift();
79
+ if (!removed) break;
80
+ this.totalLength -= removed.length;
81
+ }
82
+ if (this.totalLength > this.maxBytes && this.chunks.length === 1) {
83
+ const chunk = this.chunks[0];
84
+ const trimmed = chunk.slice(chunk.length - this.maxBytes);
85
+ this.chunks[0] = trimmed;
86
+ this.totalLength = trimmed.length;
87
+ }
88
+ }
89
+ };
90
+
91
+ // src/providers/run-provider.ts
92
+ import { execFile as execFile2, spawn as spawn2 } from "child_process";
93
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
94
+ import { tmpdir } from "os";
95
+ import { join } from "path";
96
+ import { promisify as promisify2 } from "util";
97
+
98
+ // src/session/overseer.ts
99
+ import { execFile } from "child_process";
100
+ import { promisify } from "util";
101
+
102
+ // src/providers/timeout.ts
103
+ var TIMEOUT_MESSAGE = "\n[lisa-timeout] Provider killed: exceeded session_timeout. Eligible for fallback.\n";
104
+ var SIGKILL_GRACE_MS = 1e4;
105
+ function killWithEscalation(proc) {
106
+ proc.kill("SIGTERM");
107
+ const escalation = setTimeout(() => {
108
+ try {
109
+ proc.kill("SIGKILL");
110
+ } catch {
111
+ }
112
+ }, SIGKILL_GRACE_MS);
113
+ if (escalation && typeof escalation === "object" && "unref" in escalation) {
114
+ escalation.unref();
115
+ }
116
+ }
117
+ function createSessionTimeout(proc, timeoutSeconds) {
118
+ if (!timeoutSeconds || timeoutSeconds <= 0) {
119
+ return { stop() {
120
+ }, wasTimedOut: () => false };
121
+ }
122
+ let timedOut = false;
123
+ const timer = setTimeout(() => {
124
+ timedOut = true;
125
+ killWithEscalation(proc);
126
+ }, timeoutSeconds * 1e3);
127
+ return {
128
+ stop() {
129
+ clearTimeout(timer);
130
+ },
131
+ wasTimedOut() {
132
+ return timedOut;
133
+ }
134
+ };
135
+ }
136
+
137
+ // src/session/overseer.ts
138
+ var execFileAsync = promisify(execFile);
139
+ var STUCK_MESSAGE = "\n[lisa-overseer] Provider killed: no git changes detected within the stuck threshold. Eligible for fallback.\n";
140
+ var STALL_MESSAGE = "\n[lisa-stall] Provider killed: no output received within the stall timeout. Eligible for fallback.\n";
141
+ var DEFAULT_OUTPUT_STALL_TIMEOUT = 300;
142
+ function createOutputStallDetector(proc, timeoutSeconds) {
143
+ const timeout = timeoutSeconds ?? DEFAULT_OUTPUT_STALL_TIMEOUT;
144
+ if (timeout <= 0) {
145
+ return { reset() {
146
+ }, wasKilled: () => false, stop() {
147
+ } };
148
+ }
149
+ let killed = false;
150
+ let timer = setTimeout(() => {
151
+ killed = true;
152
+ killWithEscalation(proc);
153
+ }, timeout * 1e3);
154
+ return {
155
+ reset() {
156
+ if (killed || !timer) return;
157
+ clearTimeout(timer);
158
+ timer = setTimeout(() => {
159
+ killed = true;
160
+ killWithEscalation(proc);
161
+ }, timeout * 1e3);
162
+ },
163
+ wasKilled() {
164
+ return killed;
165
+ },
166
+ stop() {
167
+ if (timer) {
168
+ clearTimeout(timer);
169
+ timer = null;
170
+ }
171
+ }
172
+ };
173
+ }
174
+ function createErrorLoopDetector(proc, pattern, threshold = 25) {
175
+ let consecutive = 0;
176
+ let killed = false;
177
+ return {
178
+ check(text) {
179
+ if (killed) return;
180
+ for (const line of text.split("\n")) {
181
+ const trimmed = line.trim();
182
+ if (!trimmed) continue;
183
+ if (pattern.test(trimmed)) {
184
+ if (++consecutive >= threshold) {
185
+ killed = true;
186
+ killWithEscalation(proc);
187
+ return;
188
+ }
189
+ } else {
190
+ consecutive = 0;
191
+ }
192
+ }
193
+ },
194
+ wasKilled() {
195
+ return killed;
196
+ }
197
+ };
198
+ }
199
+ async function getGitSnapshot(cwd) {
200
+ try {
201
+ const { stdout } = await execFileAsync("git", ["status", "--porcelain"], {
202
+ cwd,
203
+ timeout: 1e4
204
+ });
205
+ return stdout;
206
+ } catch {
207
+ return "";
208
+ }
209
+ }
210
+ function startOverseer(proc, cwd, config, getSnapshot = getGitSnapshot) {
211
+ if (!config.enabled) {
212
+ return {
213
+ stop() {
214
+ },
215
+ wasKilled() {
216
+ return false;
217
+ }
218
+ };
219
+ }
220
+ let killed = false;
221
+ let paused = false;
222
+ let lastSnapshot;
223
+ let lastChangeTime = Date.now();
224
+ let timer = null;
225
+ const onPauseProvider = () => {
226
+ paused = true;
227
+ };
228
+ const onResumeProvider = () => {
229
+ paused = false;
230
+ lastChangeTime = Date.now();
231
+ };
232
+ kanbanEmitter.on("loop:pause-provider", onPauseProvider);
233
+ kanbanEmitter.on("loop:resume-provider", onResumeProvider);
234
+ const check = async () => {
235
+ if (killed || paused) return;
236
+ try {
237
+ const snapshot = await getSnapshot(cwd);
238
+ if (lastSnapshot === void 0) {
239
+ lastSnapshot = snapshot;
240
+ lastChangeTime = Date.now();
241
+ return;
242
+ }
243
+ if (snapshot !== lastSnapshot) {
244
+ lastSnapshot = snapshot;
245
+ lastChangeTime = Date.now();
246
+ return;
247
+ }
248
+ const idleMs = Date.now() - lastChangeTime;
249
+ if (idleMs >= config.stuck_threshold * 1e3) {
250
+ killed = true;
251
+ if (timer) {
252
+ clearInterval(timer);
253
+ timer = null;
254
+ }
255
+ killWithEscalation(proc);
256
+ }
257
+ } catch {
258
+ }
259
+ };
260
+ timer = setInterval(check, config.check_interval * 1e3);
261
+ if (timer && typeof timer === "object" && "unref" in timer) {
262
+ timer.unref();
263
+ }
264
+ return {
265
+ stop() {
266
+ if (timer) {
267
+ clearInterval(timer);
268
+ timer = null;
269
+ }
270
+ kanbanEmitter.off("loop:pause-provider", onPauseProvider);
271
+ kanbanEmitter.off("loop:resume-provider", onResumeProvider);
272
+ },
273
+ wasKilled() {
274
+ return killed;
275
+ }
276
+ };
277
+ }
278
+
279
+ // src/providers/heap.ts
280
+ function buildNodeOptions(heapMb = 8192) {
281
+ const existing = process.env.NODE_OPTIONS ?? "";
282
+ if (existing.includes("max-old-space-size")) {
283
+ return existing;
284
+ }
285
+ return `${existing} --max-old-space-size=${heapMb}`.trim();
286
+ }
287
+
288
+ // src/providers/pty.ts
289
+ import { spawn } from "child_process";
290
+ import { platform } from "os";
291
+ var ANSI_REGEX = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07]*\x07|\([A-Z0-9]|[A-Z])/g;
292
+ var CTRL_D_REGEX = /\x04/g;
293
+ var CARET_D_BACKSPACE_REGEX = /\^D\x08\x08/g;
294
+ function stripAnsi(text) {
295
+ return text.replace(ANSI_REGEX, "").replace(CTRL_D_REGEX, "").replace(CARET_D_BACKSPACE_REGEX, "").replace(/\r\n/g, "\n").replace(/\r/g, "");
296
+ }
297
+ function buildPtyArgs(command, os) {
298
+ const currentOs = os ?? platform();
299
+ const wrappedCommand = `stty eof undef 2>/dev/null; ${command} < /dev/null`;
300
+ if (currentOs === "darwin") {
301
+ return { file: "script", args: ["-qF", "/dev/null", "sh", "-c", wrappedCommand] };
302
+ }
303
+ if (currentOs === "linux") {
304
+ return { file: "script", args: ["-qef", "-c", wrappedCommand, "/dev/null"] };
305
+ }
306
+ return null;
307
+ }
308
+ function spawnWithPty(command, options = {}) {
309
+ const ptyArgs = buildPtyArgs(command);
310
+ if (ptyArgs) {
311
+ const proc2 = spawn(ptyArgs.file, ptyArgs.args, {
312
+ ...options,
313
+ stdio: ["ignore", "pipe", "pipe"]
314
+ });
315
+ return { proc: proc2, isPty: true };
316
+ }
317
+ const proc = spawn("sh", ["-c", command], {
318
+ ...options,
319
+ stdio: ["ignore", "pipe", "pipe"]
320
+ });
321
+ return { proc, isPty: false };
322
+ }
323
+
324
+ // src/providers/run-provider.ts
325
+ var SAFE_SHELL_ARG = /^[a-zA-Z0-9._:/@-]+$/;
326
+ function validateShellArg(value, label) {
327
+ if (!SAFE_SHELL_ARG.test(value)) {
328
+ throw new Error(
329
+ `Invalid ${label}: "${value}" contains unsafe characters. Only alphanumeric, dots, colons, slashes, hyphens, and underscores are allowed.`
330
+ );
331
+ }
332
+ }
333
+ var execFileAsync2 = promisify2(execFile2);
334
+ var availabilityCache = /* @__PURE__ */ new Map();
335
+ async function isCommandAvailable(command, args = ["--version"]) {
336
+ const key = `${command}:${args.join(",")}`;
337
+ const cached = availabilityCache.get(key);
338
+ if (cached !== void 0) return cached;
339
+ try {
340
+ await execFileAsync2(command, args, { stdio: "ignore" });
341
+ availabilityCache.set(key, true);
342
+ return true;
343
+ } catch {
344
+ availabilityCache.set(key, false);
345
+ return false;
346
+ }
347
+ }
348
+ async function runProviderProcess(config, prompt, opts) {
349
+ const start = Date.now();
350
+ const tmpDir = mkdtempSync(join(tmpdir(), "lisa-"));
351
+ const promptFile = join(tmpDir, "prompt.md");
352
+ writeFileSync(promptFile, prompt, { encoding: "utf-8", mode: 384 });
353
+ try {
354
+ const promptCatExpr = `"$(cat '${escapeShellPath(promptFile)}')"`;
355
+ const command = config.buildCommand(promptCatExpr);
356
+ log(`[${config.name}] Running: ${config.logLine}`.trim());
357
+ if (opts.issueId) {
358
+ kanbanEmitter.emit("issue:output", opts.issueId, config.kanbanLine);
359
+ }
360
+ const spawnEnv = {
361
+ ...process.env,
362
+ ...opts.env,
363
+ NODE_OPTIONS: buildNodeOptions(),
364
+ ...config.extraEnv
365
+ };
366
+ let proc;
367
+ let isPty;
368
+ if (config.forceRawSpawn) {
369
+ proc = spawn2("sh", ["-c", command], {
370
+ cwd: opts.cwd,
371
+ env: spawnEnv,
372
+ stdio: ["ignore", "pipe", "pipe"]
373
+ });
374
+ isPty = false;
375
+ } else {
376
+ ({ proc, isPty } = spawnWithPty(command, { cwd: opts.cwd, env: spawnEnv }));
377
+ }
378
+ if (proc.pid) opts.onProcess?.(proc.pid);
379
+ const overseer = opts.overseer?.enabled ? startOverseer(proc, opts.cwd, opts.overseer) : null;
380
+ const sessionTimeout = createSessionTimeout(proc, opts.sessionTimeout);
381
+ const errorLoopDetector = createErrorLoopDetector(proc, config.errorPattern);
382
+ const outputStall = createOutputStallDetector(proc, opts.outputStallTimeout);
383
+ const chunks = new OutputBuffer();
384
+ const stderrChunks = new OutputBuffer();
385
+ proc.stdout?.on("data", (chunk) => {
386
+ const raw = chunk.toString();
387
+ const cleaned = isPty ? stripAnsi(raw) : raw;
388
+ const text = config.outputTransform ? config.outputTransform(cleaned) : cleaned;
389
+ if (!text) return;
390
+ errorLoopDetector.check(text);
391
+ outputStall.reset();
392
+ if (getOutputMode() !== "tui") process.stdout.write(text);
393
+ if (opts.issueId) {
394
+ kanbanEmitter.emit("issue:output", opts.issueId, text);
395
+ }
396
+ chunks.push(text);
397
+ safeAppendLog(opts.logFile, text);
398
+ });
399
+ proc.stderr?.on("data", (chunk) => {
400
+ const raw = chunk.toString();
401
+ const text = isPty ? stripAnsi(raw) : raw;
402
+ if (getOutputMode() !== "tui") process.stderr.write(raw);
403
+ stderrChunks.push(text);
404
+ safeAppendLog(opts.logFile, text);
405
+ });
406
+ const exitCode = await new Promise((resolve2) => {
407
+ proc.on("close", (code) => {
408
+ overseer?.stop();
409
+ sessionTimeout.stop();
410
+ outputStall.stop();
411
+ resolve2(code ?? 1);
412
+ });
413
+ });
414
+ if (sessionTimeout.wasTimedOut()) {
415
+ chunks.push(TIMEOUT_MESSAGE);
416
+ } else if (outputStall.wasKilled()) {
417
+ chunks.push(STALL_MESSAGE);
418
+ } else if (overseer?.wasKilled() || errorLoopDetector.wasKilled()) {
419
+ chunks.push(STUCK_MESSAGE);
420
+ }
421
+ const success = exitCode === 0 && !overseer?.wasKilled() && !errorLoopDetector.wasKilled() && !outputStall.wasKilled() && !sessionTimeout.wasTimedOut();
422
+ const stderrOutput = stderrChunks.toString();
423
+ if (!success && stderrOutput) {
424
+ chunks.push(`
425
+ [stderr]
426
+ ${stderrOutput}`);
427
+ }
428
+ return {
429
+ success,
430
+ output: chunks.toString(),
431
+ duration: Date.now() - start,
432
+ exitCode
433
+ };
434
+ } catch (err) {
435
+ return {
436
+ success: false,
437
+ output: formatError(err),
438
+ duration: Date.now() - start
439
+ };
440
+ } finally {
441
+ try {
442
+ rmSync(tmpDir, { recursive: true, force: true });
443
+ } catch {
444
+ }
445
+ }
446
+ }
447
+
448
+ // src/providers/aider.ts
449
+ var AIDER_API_KEY_ENV_VARS = [
450
+ "OPENAI_API_KEY",
451
+ "ANTHROPIC_API_KEY",
452
+ "GEMINI_API_KEY",
453
+ "GROQ_API_KEY",
454
+ "OPENROUTER_API_KEY",
455
+ "COHERE_API_KEY",
456
+ "MISTRAL_API_KEY",
457
+ "DEEPSEEK_API_KEY",
458
+ "AZURE_API_KEY",
459
+ "XAI_API_KEY"
460
+ ];
461
+ var AiderProvider = class {
462
+ name = "aider";
463
+ async isAvailable() {
464
+ return isCommandAvailable("aider");
465
+ }
466
+ async run(prompt, opts) {
467
+ const hasApiKey = AIDER_API_KEY_ENV_VARS.some((v) => process.env[v]);
468
+ if (!hasApiKey) {
469
+ return {
470
+ success: false,
471
+ output: `Aider requires a direct LLM API key. Set one of: ${AIDER_API_KEY_ENV_VARS.join(", ")}`,
472
+ duration: 0
473
+ };
474
+ }
475
+ try {
476
+ if (opts.model) validateShellArg(opts.model, "model");
477
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
478
+ const aiderTmpDir = mkdtempSync2(join2(tmpdir2(), "lisa-aider-"));
479
+ const aiderPromptFile = join2(aiderTmpDir, "prompt.md");
480
+ writeFileSync2(aiderPromptFile, prompt, { encoding: "utf-8", mode: 384 });
481
+ const config = {
482
+ name: "aider",
483
+ buildCommand: () => `aider --message-file '${escapeShellPath(aiderPromptFile)}' --yes-always ${modelFlag}`,
484
+ logLine: `aider --message-file --yes-always ${modelFlag || "(default model)"}`,
485
+ kanbanLine: `$ aider --message-file --yes-always ${modelFlag || "(default model)"} <prompt: ${prompt.length} chars>
486
+ `,
487
+ errorPattern: /^Error /
488
+ };
489
+ try {
490
+ return await runProviderProcess(config, prompt, opts);
491
+ } finally {
492
+ try {
493
+ rmSync2(aiderTmpDir, { recursive: true, force: true });
494
+ } catch {
495
+ }
496
+ }
497
+ } catch (err) {
498
+ return { success: false, output: formatError(err), duration: 0 };
499
+ }
500
+ }
501
+ };
502
+
503
+ // src/providers/claude.ts
504
+ var ClaudeProvider = class {
505
+ name = "claude";
506
+ supportsNativeWorktree = false;
507
+ // --worktree flag requires a TTY and hangs in non-interactive mode
508
+ async isAvailable() {
509
+ return isCommandAvailable("claude");
510
+ }
511
+ async run(prompt, opts) {
512
+ try {
513
+ const flags = ["-p", "--dangerously-skip-permissions"];
514
+ if (opts.model) {
515
+ validateShellArg(opts.model, "model");
516
+ flags.push("--model", opts.model);
517
+ }
518
+ if (opts.providerOptions?.effort) {
519
+ validateShellArg(opts.providerOptions.effort, "effort");
520
+ flags.push("--effort", opts.providerOptions.effort);
521
+ }
522
+ const flagStr = flags.join(" ");
523
+ const config = {
524
+ name: "claude",
525
+ buildCommand: (promptCatExpr) => `claude ${flagStr} ${promptCatExpr}`,
526
+ logLine: `claude ${flagStr}`,
527
+ kanbanLine: `$ claude ${flagStr} <prompt: ${prompt.length} chars>
528
+ `,
529
+ errorPattern: /^Error /,
530
+ extraEnv: { CLAUDECODE: void 0 },
531
+ // When Lisa itself runs inside an active Claude Code session (CLAUDECODE is set in the
532
+ // parent environment), the script PTY wrapper reads EOF from its closed stdin and
533
+ // forwards ^D to Claude, causing it to exit. Use a plain spawn without PTY instead.
534
+ forceRawSpawn: Boolean(process.env.CLAUDECODE)
535
+ };
536
+ return await runProviderProcess(config, prompt, opts);
537
+ } catch (err) {
538
+ return { success: false, output: formatError(err), duration: 0 };
539
+ }
540
+ }
541
+ };
542
+
543
+ // src/providers/codex.ts
544
+ var CodexProvider = class {
545
+ name = "codex";
546
+ async isAvailable() {
547
+ return isCommandAvailable("codex");
548
+ }
549
+ async run(prompt, opts) {
550
+ try {
551
+ if (opts.model) validateShellArg(opts.model, "model");
552
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
553
+ const config = {
554
+ name: "codex",
555
+ buildCommand: (promptCatExpr) => `codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral ${modelFlag} ${promptCatExpr}`,
556
+ logLine: `codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral ${modelFlag || "(default model)"}`,
557
+ kanbanLine: `$ codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral ${modelFlag || "(default model)"} <prompt: ${prompt.length} chars>
558
+ `,
559
+ errorPattern: /^Error /,
560
+ extraEnv: { CODEX_QUIET_MODE: "1" }
561
+ };
562
+ return await runProviderProcess(config, prompt, opts);
563
+ } catch (err) {
564
+ return { success: false, output: formatError(err), duration: 0 };
565
+ }
566
+ }
567
+ };
568
+
569
+ // src/providers/copilot.ts
570
+ var CopilotProvider = class {
571
+ name = "copilot";
572
+ async isAvailable() {
573
+ return isCommandAvailable("copilot", ["version"]);
574
+ }
575
+ async run(prompt, opts) {
576
+ try {
577
+ if (opts.model) validateShellArg(opts.model, "model");
578
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
579
+ const config = {
580
+ name: "copilot",
581
+ buildCommand: (promptCatExpr) => `copilot --allow-all --no-alt-screen ${modelFlag} -p ${promptCatExpr}`,
582
+ logLine: `copilot --allow-all --no-alt-screen ${modelFlag || "(default model)"} -p`,
583
+ kanbanLine: `$ copilot --allow-all --no-alt-screen ${modelFlag || "(default model)"} -p <prompt: ${prompt.length} chars>
584
+ `,
585
+ errorPattern: /^Error /
586
+ };
587
+ return await runProviderProcess(config, prompt, opts);
588
+ } catch (err) {
589
+ return { success: false, output: formatError(err), duration: 0 };
590
+ }
591
+ }
592
+ };
593
+
594
+ // src/providers/cursor.ts
595
+ import { execSync } from "child_process";
596
+ function findCursorBinary() {
597
+ for (const bin of ["agent", "cursor-agent"]) {
598
+ try {
599
+ execSync(`which ${bin}`, { stdio: "ignore" });
600
+ return bin;
601
+ } catch {
602
+ }
603
+ }
604
+ return null;
605
+ }
606
+ var TOOL_LABELS = {
607
+ readToolCall: "Read",
608
+ editToolCall: "Edit",
609
+ writeToolCall: "Write",
610
+ runCommandToolCall: "Run",
611
+ listDirectoryToolCall: "List",
612
+ codebaseSearchToolCall: "Search",
613
+ grepToolCall: "Grep",
614
+ fileSearchToolCall: "Find",
615
+ deleteToolCall: "Delete"
616
+ };
617
+ function formatStreamEvent(event) {
618
+ const type = event.type;
619
+ if (type === "assistant") {
620
+ const message = event.message;
621
+ const text = message?.content?.[0]?.text;
622
+ return text ? `${text}
623
+ ` : null;
624
+ }
625
+ if (type === "tool_call") {
626
+ const subtype = event.subtype;
627
+ const toolCall = event.tool_call;
628
+ if (!toolCall) return null;
629
+ const toolKey = Object.keys(toolCall)[0];
630
+ if (!toolKey) return null;
631
+ const label = TOOL_LABELS[toolKey] ?? toolKey.replace(/ToolCall$/, "");
632
+ const toolData = toolCall[toolKey];
633
+ if (subtype === "started") {
634
+ const path = toolData?.args?.path;
635
+ const command = toolData?.args?.command;
636
+ const query = toolData?.args?.query;
637
+ const target = path ?? command ?? query ?? "";
638
+ return `\u25CF ${label} ${target}
639
+ `;
640
+ }
641
+ if (subtype === "completed") {
642
+ const result = toolData?.result;
643
+ if (result?.error) {
644
+ const errMsg = typeof result.error === "string" ? result.error : result.error?.message ?? "error";
645
+ return `\u2717 ${label} \u2014 ${errMsg}
646
+ `;
647
+ }
648
+ return null;
649
+ }
650
+ }
651
+ return null;
652
+ }
653
+ function createStreamJsonTransform() {
654
+ let buffer = "";
655
+ return (raw) => {
656
+ buffer += raw;
657
+ const lines = buffer.split("\n");
658
+ buffer = lines.pop() ?? "";
659
+ let output = "";
660
+ for (const line of lines) {
661
+ const trimmed = line.trim();
662
+ if (!trimmed) continue;
663
+ try {
664
+ const event = JSON.parse(trimmed);
665
+ const formatted = formatStreamEvent(event);
666
+ if (formatted) output += formatted;
667
+ } catch {
668
+ output += `${trimmed}
669
+ `;
670
+ }
671
+ }
672
+ return output;
673
+ };
674
+ }
675
+ var CursorProvider = class {
676
+ name = "cursor";
677
+ _bin = void 0;
678
+ resolveBin() {
679
+ if (this._bin === void 0) this._bin = findCursorBinary();
680
+ return this._bin;
681
+ }
682
+ async isAvailable() {
683
+ return this.resolveBin() !== null;
684
+ }
685
+ async run(prompt, opts) {
686
+ const bin = this.resolveBin();
687
+ if (!bin) {
688
+ return {
689
+ success: false,
690
+ output: "cursor agent (agent / cursor-agent) is not installed or not in PATH",
691
+ duration: 0
692
+ };
693
+ }
694
+ try {
695
+ if (opts.model) validateShellArg(opts.model, "model");
696
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
697
+ const config = {
698
+ name: "cursor",
699
+ buildCommand: (promptCatExpr) => `${bin} -p ${promptCatExpr} --output-format stream-json --force ${modelFlag}`,
700
+ logLine: `${bin} -p --output-format stream-json --force ${modelFlag || "(default model)"}`,
701
+ kanbanLine: `$ ${bin} -p --output-format stream-json --force ${modelFlag || "(default model)"} <prompt: ${prompt.length} chars>
702
+ `,
703
+ errorPattern: /^Error /,
704
+ outputTransform: createStreamJsonTransform()
705
+ };
706
+ return await runProviderProcess(config, prompt, opts);
707
+ } catch (err) {
708
+ return { success: false, output: formatError(err), duration: 0 };
709
+ }
710
+ }
711
+ };
712
+
713
+ // src/providers/gemini.ts
714
+ var GEMINI_ERROR_PATTERN = /^Error (executing tool|generating content)/;
715
+ var GeminiProvider = class {
716
+ name = "gemini";
717
+ async isAvailable() {
718
+ return isCommandAvailable("gemini");
719
+ }
720
+ async run(prompt, opts) {
721
+ try {
722
+ if (opts.model) validateShellArg(opts.model, "model");
723
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
724
+ const config = {
725
+ name: "gemini",
726
+ buildCommand: (promptCatExpr) => `gemini --yolo ${modelFlag} -p ${promptCatExpr}`,
727
+ logLine: `gemini --yolo ${modelFlag || "(default model)"} -p`,
728
+ kanbanLine: `$ gemini --yolo ${modelFlag || "(default model)"} -p <prompt: ${prompt.length} chars>
729
+ `,
730
+ errorPattern: GEMINI_ERROR_PATTERN
731
+ };
732
+ return await runProviderProcess(config, prompt, opts);
733
+ } catch (err) {
734
+ return { success: false, output: formatError(err), duration: 0 };
735
+ }
736
+ }
737
+ };
738
+
739
+ // src/providers/goose.ts
740
+ var GooseProvider = class {
741
+ name = "goose";
742
+ async isAvailable() {
743
+ return isCommandAvailable("goose");
744
+ }
745
+ async run(prompt, opts) {
746
+ try {
747
+ let providerFlag = "";
748
+ if (process.env.GOOSE_PROVIDER) {
749
+ validateShellArg(process.env.GOOSE_PROVIDER, "GOOSE_PROVIDER");
750
+ providerFlag = `--provider ${process.env.GOOSE_PROVIDER}`;
751
+ }
752
+ if (opts.model) validateShellArg(opts.model, "model");
753
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
754
+ const config = {
755
+ name: "goose",
756
+ buildCommand: (promptCatExpr) => `goose run ${providerFlag} ${modelFlag} --text ${promptCatExpr}`,
757
+ logLine: `goose run ${providerFlag} ${modelFlag || "(default model)"} --text`,
758
+ kanbanLine: `$ goose run ${providerFlag} ${modelFlag || "(default model)"} --text <prompt: ${prompt.length} chars>
759
+ `,
760
+ errorPattern: /^Error /
761
+ };
762
+ return await runProviderProcess(config, prompt, opts);
763
+ } catch (err) {
764
+ return { success: false, output: formatError(err), duration: 0 };
765
+ }
766
+ }
767
+ };
768
+
769
+ // src/providers/opencode.ts
770
+ var OpenCodeProvider = class {
771
+ name = "opencode";
772
+ async isAvailable() {
773
+ return isCommandAvailable("opencode");
774
+ }
775
+ async run(prompt, opts) {
776
+ try {
777
+ if (opts.model) validateShellArg(opts.model, "model");
778
+ const modelFlag = opts.model ? `--model ${opts.model}` : "";
779
+ const config = {
780
+ name: "opencode",
781
+ buildCommand: (promptCatExpr) => `opencode run ${modelFlag} ${promptCatExpr}`,
782
+ logLine: `opencode run ${modelFlag || "(default model)"}`,
783
+ kanbanLine: `$ opencode run ${modelFlag || "(default model)"} <prompt: ${prompt.length} chars>
784
+ `,
785
+ errorPattern: /^Error /
786
+ };
787
+ return await runProviderProcess(config, prompt, opts);
788
+ } catch (err) {
789
+ return { success: false, output: formatError(err), duration: 0 };
790
+ }
791
+ }
792
+ };
793
+
794
+ // src/providers/index.ts
795
+ var providers = {
796
+ claude: () => new ClaudeProvider(),
797
+ gemini: () => new GeminiProvider(),
798
+ opencode: () => new OpenCodeProvider(),
799
+ copilot: () => new CopilotProvider(),
800
+ cursor: () => new CursorProvider(),
801
+ goose: () => new GooseProvider(),
802
+ aider: () => new AiderProvider(),
803
+ codex: () => new CodexProvider()
804
+ };
805
+ async function getAllProvidersWithAvailability() {
806
+ const all = Object.values(providers).map((f) => f());
807
+ return Promise.all(all.map(async (p) => ({ provider: p, available: await p.isAvailable() })));
808
+ }
809
+ function createProvider(name) {
810
+ const factory = providers[name];
811
+ if (!factory) {
812
+ throw new Error(`Unknown provider: ${name}. Available: ${Object.keys(providers).join(", ")}`);
813
+ }
814
+ return factory();
815
+ }
816
+ var ELIGIBLE_ERROR_PATTERNS = [
817
+ /429/i,
818
+ /quota/i,
819
+ /rate.?limit/i,
820
+ /too many requests/i,
821
+ /resource.?exhausted/i,
822
+ /overloaded/i,
823
+ /unavailable/i,
824
+ /not\s+available/i,
825
+ /interactive mode/i,
826
+ /not.?found.*model/i,
827
+ /model.*not.?found/i,
828
+ /does not exist/i,
829
+ /ETIMEDOUT/,
830
+ /ECONNREFUSED/,
831
+ /ECONNRESET/,
832
+ /ENOTFOUND/,
833
+ /fetch failed/i,
834
+ /\btimeout\b/i,
835
+ /\btimed?\s*out\b/i,
836
+ /network.?error/i,
837
+ /not installed/i,
838
+ /not in PATH/i,
839
+ /command not found/i,
840
+ /lisa-overseer/i,
841
+ /lisa-timeout/i,
842
+ /lisa-stall/i,
843
+ /lisa-reconciliation/i,
844
+ /named models unavailable/i,
845
+ /free plans can only use/i,
846
+ /empty commit/i,
847
+ // Process crash patterns (OOM, fatal errors, signals)
848
+ /heap.*out of memory/i,
849
+ /out of memory/i,
850
+ /FATAL ERROR/,
851
+ /allocation failed/i,
852
+ /segmentation fault/i,
853
+ /\bSIGKILL\b/,
854
+ /\bSIGABRT\b/,
855
+ /\bSIGSEGV\b/
856
+ ];
857
+ function isEligibleForFallback(output, exitCode) {
858
+ if (ELIGIBLE_ERROR_PATTERNS.some((pattern) => pattern.test(output))) return true;
859
+ if (exitCode !== void 0 && exitCode > 128) return true;
860
+ return false;
861
+ }
862
+ function isCompleteProviderExhaustion(attempts) {
863
+ if (attempts.length === 0) return false;
864
+ return attempts.every((a) => !a.success && a.error !== "Non-eligible error");
865
+ }
866
+ async function runWithFallback(models, prompt, opts) {
867
+ const attempts = [];
868
+ for (const spec of models) {
869
+ if (opts.shouldAbort?.()) {
870
+ break;
871
+ }
872
+ kanbanEmitter.emit("provider:model-changed", spec.model ?? spec.provider);
873
+ const provider = createProvider(spec.provider);
874
+ const available = await provider.isAvailable();
875
+ if (!available) {
876
+ attempts.push({
877
+ provider: spec.provider,
878
+ model: spec.model,
879
+ success: false,
880
+ error: `Provider "${spec.provider}" is not installed or not in PATH`,
881
+ duration: 0
882
+ });
883
+ continue;
884
+ }
885
+ const guardrailsSection = opts.guardrailsDir ? buildGuardrailsSection(opts.guardrailsDir) : "";
886
+ const fullPrompt = guardrailsSection ? `${prompt}${guardrailsSection}` : prompt;
887
+ const result = await provider.run(fullPrompt, { ...opts, model: spec.model });
888
+ if (result.success || opts.earlySuccess?.()) {
889
+ attempts.push({
890
+ provider: spec.provider,
891
+ model: spec.model,
892
+ success: true,
893
+ duration: result.duration
894
+ });
895
+ return {
896
+ success: true,
897
+ output: result.output,
898
+ duration: result.duration,
899
+ providerUsed: spec.model ? `${spec.provider}/${spec.model}` : spec.provider,
900
+ provider,
901
+ attempts
902
+ };
903
+ }
904
+ if (opts.guardrailsDir && opts.issueId) {
905
+ appendEntry(opts.guardrailsDir, {
906
+ issueId: opts.issueId,
907
+ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
908
+ provider: spec.provider,
909
+ errorType: extractErrorType(result.output),
910
+ context: extractContext(result.output)
911
+ });
912
+ }
913
+ const eligible = isEligibleForFallback(result.output, result.exitCode);
914
+ attempts.push({
915
+ provider: spec.provider,
916
+ model: spec.model,
917
+ success: false,
918
+ error: eligible ? "Eligible error (quota/unavailable/timeout)" : "Non-eligible error",
919
+ duration: result.duration
920
+ });
921
+ if (!eligible) {
922
+ return {
923
+ success: false,
924
+ output: result.output,
925
+ duration: result.duration,
926
+ providerUsed: spec.model ? `${spec.provider}/${spec.model}` : spec.provider,
927
+ provider,
928
+ attempts
929
+ };
930
+ }
931
+ }
932
+ const totalDuration = attempts.reduce((sum, a) => sum + a.duration, 0);
933
+ return {
934
+ success: false,
935
+ output: formatAttemptsReport(attempts),
936
+ duration: totalDuration,
937
+ providerUsed: attempts[attempts.length - 1]?.provider ?? models[0]?.provider ?? "claude",
938
+ attempts
939
+ };
940
+ }
941
+ function formatAttemptsReport(attempts) {
942
+ const lines = ["All models exhausted. Attempt history:"];
943
+ for (const [i, a] of attempts.entries()) {
944
+ const status = a.success ? "OK" : "FAILED";
945
+ const error2 = a.error ? ` \u2014 ${a.error}` : "";
946
+ const duration = a.duration > 0 ? ` (${Math.round(a.duration / 1e3)}s)` : "";
947
+ const label = a.model ? `${a.provider}/${a.model}` : a.provider;
948
+ lines.push(` ${i + 1}. ${label}: ${status}${error2}${duration}`);
949
+ }
950
+ return lines.join("\n");
951
+ }
952
+
953
+ // src/sources/jira.ts
954
+ var PRIORITY_RANK = {
955
+ highest: 1,
956
+ high: 2,
957
+ medium: 3,
958
+ low: 4,
959
+ lowest: 5
960
+ };
961
+ function getBaseUrl() {
962
+ const url = process.env.JIRA_BASE_URL;
963
+ if (!url) throw new Error("JIRA_BASE_URL is not set");
964
+ return url.replace(/\/$/, "");
965
+ }
966
+ function getAuthHeaders() {
967
+ const email = process.env.JIRA_EMAIL;
968
+ const token = process.env.JIRA_API_TOKEN;
969
+ if (!email || !token) throw new Error("JIRA_EMAIL and JIRA_API_TOKEN must be set");
970
+ const credentials = Buffer.from(`${email}:${token}`).toString("base64");
971
+ return {
972
+ Authorization: `Basic ${credentials}`,
973
+ Accept: "application/json"
974
+ };
975
+ }
976
+ var _api = null;
977
+ function api() {
978
+ if (!_api) _api = createApiClient(`${getBaseUrl()}/rest/api/3`, getAuthHeaders, "Jira");
979
+ return _api;
980
+ }
981
+ function jiraGet(path) {
982
+ return api().get(path);
983
+ }
984
+ function jiraPost(path, body) {
985
+ return api().post(path, body);
986
+ }
987
+ function jiraPut(path, body) {
988
+ return api().put(path, body);
989
+ }
990
+ async function jiraSearchJql(jql, fields, maxResults) {
991
+ return jiraPost("/search/jql", { jql, fields: fields.split(","), maxResults });
992
+ }
993
+ function priorityRank(issue) {
994
+ const name = issue.fields.priority?.name?.toLowerCase() ?? "";
995
+ return PRIORITY_RANK[name] ?? Number.MAX_SAFE_INTEGER;
996
+ }
997
+ function extractDescription(description) {
998
+ if (!description) return "";
999
+ if (typeof description === "string") return description;
1000
+ if (typeof description === "object") {
1001
+ return extractAdfText(description);
1002
+ }
1003
+ return "";
1004
+ }
1005
+ function extractAdfText(node) {
1006
+ if (node.type === "text" && typeof node.text === "string") return node.text;
1007
+ const parts = [];
1008
+ if (Array.isArray(node.content)) {
1009
+ for (const child of node.content) {
1010
+ const text = extractAdfText(child);
1011
+ if (text) parts.push(text);
1012
+ }
1013
+ }
1014
+ return parts.join("\n");
1015
+ }
1016
+ function escapeJql(value) {
1017
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1018
+ }
1019
+ async function resolveStatusId(scope, statusName) {
1020
+ try {
1021
+ const data = await jiraGet(
1022
+ `/project/${encodeURIComponent(scope)}/statuses`
1023
+ );
1024
+ for (const issueType of data) {
1025
+ const match = issueType.statuses.find(
1026
+ (s) => s.name.toLowerCase() === statusName.toLowerCase()
1027
+ );
1028
+ if (match) return match.id;
1029
+ }
1030
+ } catch {
1031
+ }
1032
+ return null;
1033
+ }
1034
+ function issueUrl(baseUrl, key) {
1035
+ return `${baseUrl}/browse/${key}`;
1036
+ }
1037
+ var JiraSource = class {
1038
+ name = "jira";
1039
+ async fetchNextIssue(config) {
1040
+ const labels = normalizeLabels(config);
1041
+ const labelClause = labels.map((l) => `labels = "${escapeJql(l)}"`).join(" AND ");
1042
+ const statusId = await resolveStatusId(config.scope, config.pick_from);
1043
+ const statusClause = statusId ? `status = ${statusId}` : `status = "${escapeJql(config.pick_from)}"`;
1044
+ const jql = `project = "${escapeJql(config.scope)}" AND ${labelClause} AND ${statusClause} ORDER BY priority ASC, created ASC`;
1045
+ const fields = "summary,description,priority,status,labels,issuelinks";
1046
+ const data = await jiraSearchJql(jql, fields, 50);
1047
+ const issues = data.issues ?? [];
1048
+ if (issues.length === 0) return null;
1049
+ const unblocked = [];
1050
+ const blocked = [];
1051
+ for (const issue2 of issues) {
1052
+ const activeBlockers = (issue2.fields.issuelinks ?? []).filter((link) => {
1053
+ if (link.inwardIssue && link.type.inward.toLowerCase().includes("is blocked by")) {
1054
+ return link.inwardIssue.fields.status.statusCategory.key !== "done";
1055
+ }
1056
+ return false;
1057
+ }).map((link) => link.inwardIssue?.key ?? "");
1058
+ if (activeBlockers.length === 0) {
1059
+ unblocked.push(issue2);
1060
+ } else {
1061
+ blocked.push({ key: issue2.key, blockers: activeBlockers });
1062
+ }
1063
+ }
1064
+ if (unblocked.length === 0) {
1065
+ if (blocked.length > 0) {
1066
+ warn("No unblocked issues found. Blocked issues:");
1067
+ for (const entry of blocked) {
1068
+ warn(` ${entry.key} \u2014 blocked by: ${entry.blockers.join(", ")}`);
1069
+ }
1070
+ }
1071
+ return null;
1072
+ }
1073
+ const sorted = [...unblocked].sort((a, b) => priorityRank(a) - priorityRank(b));
1074
+ const issue = sorted[0];
1075
+ if (!issue) return null;
1076
+ const baseUrl = getBaseUrl();
1077
+ return {
1078
+ id: issue.key,
1079
+ title: issue.fields.summary,
1080
+ description: extractDescription(issue.fields.description),
1081
+ url: issueUrl(baseUrl, issue.key)
1082
+ };
1083
+ }
1084
+ async fetchIssueById(id) {
1085
+ const key = parseJiraIdentifier(id);
1086
+ try {
1087
+ const issue = await jiraGet(
1088
+ `/issue/${key}?fields=summary,description,priority,status,labels`
1089
+ );
1090
+ const baseUrl = getBaseUrl();
1091
+ return {
1092
+ id: issue.key,
1093
+ title: issue.fields.summary,
1094
+ description: extractDescription(issue.fields.description),
1095
+ url: issueUrl(baseUrl, issue.key),
1096
+ status: issue.fields.status.name
1097
+ };
1098
+ } catch {
1099
+ return null;
1100
+ }
1101
+ }
1102
+ async updateStatus(issueId, statusName) {
1103
+ const key = parseJiraIdentifier(issueId);
1104
+ const data = await jiraGet(`/issue/${key}/transitions`);
1105
+ const lowerName = statusName.toLowerCase();
1106
+ const transition = data.transitions.find((t) => t.to.name.toLowerCase() === lowerName) ?? data.transitions.find((t) => t.name.toLowerCase() === lowerName);
1107
+ if (!transition) {
1108
+ const available = data.transitions.map((t) => `${t.name} \u2192 ${t.to.name}`).join(", ");
1109
+ throw new Error(`Jira transition "${statusName}" not found. Available: ${available}`);
1110
+ }
1111
+ await jiraPost(`/issue/${key}/transitions`, { transition: { id: transition.id } });
1112
+ }
1113
+ async attachPullRequest(issueId, prUrl) {
1114
+ const key = parseJiraIdentifier(issueId);
1115
+ await jiraPost(`/issue/${key}/remotelink`, {
1116
+ object: {
1117
+ url: prUrl,
1118
+ title: "Pull Request",
1119
+ icon: {
1120
+ url16x16: "https://github.com/favicon.ico",
1121
+ title: "GitHub"
1122
+ }
1123
+ }
1124
+ });
1125
+ }
1126
+ async completeIssue(issueId, statusName, labelToRemove) {
1127
+ await this.updateStatus(issueId, statusName);
1128
+ if (labelToRemove) {
1129
+ await this.removeLabel(issueId, labelToRemove);
1130
+ }
1131
+ }
1132
+ async listIssues(config) {
1133
+ const labels = normalizeLabels(config);
1134
+ const labelClause = labels.map((l) => `labels = "${escapeJql(l)}"`).join(" AND ");
1135
+ const statusId = await resolveStatusId(config.scope, config.pick_from);
1136
+ const statusClause = statusId ? `status = ${statusId}` : `status = "${escapeJql(config.pick_from)}"`;
1137
+ const jql = `project = "${escapeJql(config.scope)}" AND ${labelClause} AND ${statusClause} ORDER BY priority ASC, created ASC`;
1138
+ const fields = "summary,description,priority,status,labels";
1139
+ const data = await jiraSearchJql(jql, fields, 100);
1140
+ const baseUrl = getBaseUrl();
1141
+ return (data.issues ?? []).map((issue) => ({
1142
+ id: issue.key,
1143
+ title: issue.fields.summary,
1144
+ description: extractDescription(issue.fields.description),
1145
+ url: issueUrl(baseUrl, issue.key)
1146
+ }));
1147
+ }
1148
+ async listScopes() {
1149
+ const data = await jiraGet(
1150
+ "/project/search?maxResults=50"
1151
+ );
1152
+ return (data.values ?? []).map((p) => ({
1153
+ value: p.key,
1154
+ label: `${p.key} \u2014 ${p.name}`
1155
+ }));
1156
+ }
1157
+ async listLabels() {
1158
+ const data = await jiraGet("/label?maxResults=100");
1159
+ return (data.values ?? []).map((l) => ({
1160
+ value: l,
1161
+ label: l
1162
+ }));
1163
+ }
1164
+ async listStatuses(scope) {
1165
+ const data = await jiraGet(
1166
+ `/project/${encodeURIComponent(scope)}/statuses`
1167
+ );
1168
+ const seen = /* @__PURE__ */ new Set();
1169
+ const results = [];
1170
+ for (const issueType of data) {
1171
+ for (const status of issueType.statuses) {
1172
+ if (!seen.has(status.name)) {
1173
+ seen.add(status.name);
1174
+ results.push({ value: status.name, label: status.name });
1175
+ }
1176
+ }
1177
+ }
1178
+ return results;
1179
+ }
1180
+ async removeLabel(issueId, labelName) {
1181
+ const key = parseJiraIdentifier(issueId);
1182
+ const issue = await jiraGet(`/issue/${key}?fields=labels`);
1183
+ const currentLabels = issue.fields.labels ?? [];
1184
+ const filtered = currentLabels.filter((l) => l.toLowerCase() !== labelName.toLowerCase());
1185
+ if (filtered.length === currentLabels.length) return;
1186
+ await jiraPut(`/issue/${key}`, { fields: { labels: filtered } });
1187
+ }
1188
+ async createIssue(opts, config) {
1189
+ const labels = Array.isArray(opts.label) ? opts.label : [opts.label];
1190
+ const fields = {
1191
+ project: { key: config.scope },
1192
+ summary: opts.title,
1193
+ description: {
1194
+ type: "doc",
1195
+ version: 1,
1196
+ content: [
1197
+ {
1198
+ type: "paragraph",
1199
+ content: [{ type: "text", text: opts.description }]
1200
+ }
1201
+ ]
1202
+ },
1203
+ issuetype: { name: "Task" },
1204
+ labels
1205
+ };
1206
+ if (opts.parentId) fields.parent = { key: opts.parentId };
1207
+ const result = await jiraPost("/issue", { fields });
1208
+ if (opts.status) {
1209
+ try {
1210
+ await this.updateStatus(result.key, opts.status);
1211
+ } catch {
1212
+ }
1213
+ }
1214
+ return result.key;
1215
+ }
1216
+ async linkDependency(issueId, dependsOnId) {
1217
+ await jiraPost("/issueLink", {
1218
+ type: { name: "Blocks" },
1219
+ inwardIssue: { key: dependsOnId },
1220
+ outwardIssue: { key: issueId }
1221
+ });
1222
+ }
1223
+ };
1224
+ function parseJiraIdentifier(input) {
1225
+ const urlMatch = input.match(/\/browse\/([A-Z][A-Z0-9_]+-\d+)/);
1226
+ if (urlMatch?.[1]) return urlMatch[1];
1227
+ return input;
1228
+ }
1229
+
1230
+ // src/sources/linear.ts
1231
+ var API_URL = "https://api.linear.app/graphql";
1232
+ function getApiKey() {
1233
+ const key = process.env.LINEAR_API_KEY;
1234
+ if (!key) throw new Error("LINEAR_API_KEY is not set");
1235
+ return key;
1236
+ }
1237
+ async function gql(query, variables) {
1238
+ const res = await fetch(API_URL, {
1239
+ method: "POST",
1240
+ headers: {
1241
+ "Content-Type": "application/json",
1242
+ Authorization: getApiKey()
1243
+ },
1244
+ body: JSON.stringify({ query, variables }),
1245
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
1246
+ });
1247
+ if (!res.ok) {
1248
+ const text = await res.text();
1249
+ throw new Error(`Linear API error (${res.status}): ${text}`);
1250
+ }
1251
+ const json = await res.json();
1252
+ if (json.errors?.length) {
1253
+ throw new Error(`Linear GraphQL error: ${json.errors.map((e) => e.message).join(", ")}`);
1254
+ }
1255
+ return json.data;
1256
+ }
1257
+ var LinearSource = class {
1258
+ name = "linear";
1259
+ async fetchNextIssue(config) {
1260
+ const labels = normalizeLabels(config);
1261
+ const primaryLabel = labels[0] ?? "";
1262
+ const data = await gql(
1263
+ `query($teamName: String!, $projectName: String!, $labelName: String!, $statusName: String!) {
1264
+ issues(
1265
+ filter: {
1266
+ team: { name: { eq: $teamName } }
1267
+ project: { name: { eq: $projectName } }
1268
+ labels: { name: { eq: $labelName } }
1269
+ state: { name: { eq: $statusName } }
1270
+ }
1271
+ first: 50
1272
+ ) {
1273
+ nodes {
1274
+ id
1275
+ identifier
1276
+ title
1277
+ description
1278
+ url
1279
+ priority
1280
+ labels { nodes { name } }
1281
+ inverseRelations(first: 50) {
1282
+ nodes {
1283
+ type
1284
+ issue {
1285
+ identifier
1286
+ state { type }
1287
+ }
1288
+ }
1289
+ }
1290
+ }
1291
+ }
1292
+ }`,
1293
+ {
1294
+ teamName: config.scope,
1295
+ projectName: config.project,
1296
+ labelName: primaryLabel,
1297
+ statusName: config.pick_from
1298
+ }
1299
+ );
1300
+ const issues = labels.length > 1 ? data.issues.nodes.filter((issue2) => {
1301
+ const issueLabels = new Set(issue2.labels.nodes.map((l) => l.name.toLowerCase()));
1302
+ return labels.every((l) => issueLabels.has(l.toLowerCase()));
1303
+ }) : data.issues.nodes;
1304
+ if (issues.length === 0) return null;
1305
+ const unblocked = [];
1306
+ const blocked = [];
1307
+ const completedBlockerMap = /* @__PURE__ */ new Map();
1308
+ for (const issue2 of issues) {
1309
+ const blockerRelations = issue2.inverseRelations.nodes.filter((r) => r.type === "blocks");
1310
+ const activeBlockers = blockerRelations.filter((r) => r.issue.state.type !== "completed" && r.issue.state.type !== "canceled").map((r) => r.issue.identifier);
1311
+ const completedBlockers = blockerRelations.filter((r) => r.issue.state.type === "completed").map((r) => r.issue.identifier);
1312
+ if (completedBlockers.length > 0) {
1313
+ completedBlockerMap.set(issue2.identifier, completedBlockers);
1314
+ }
1315
+ if (activeBlockers.length === 0) {
1316
+ unblocked.push(issue2);
1317
+ } else {
1318
+ blocked.push({ identifier: issue2.identifier, blockers: activeBlockers });
1319
+ }
1320
+ }
1321
+ if (unblocked.length === 0) {
1322
+ if (blocked.length > 0) {
1323
+ warn("No unblocked issues found. Blocked issues:");
1324
+ for (const entry of blocked) {
1325
+ warn(` ${entry.identifier} \u2014 blocked by: ${entry.blockers.join(", ")}`);
1326
+ }
1327
+ }
1328
+ return null;
1329
+ }
1330
+ unblocked.sort((a, b) => {
1331
+ const pa = a.priority === 0 ? 5 : a.priority;
1332
+ const pb = b.priority === 0 ? 5 : b.priority;
1333
+ return pa - pb;
1334
+ });
1335
+ const issue = unblocked[0];
1336
+ if (!issue) return null;
1337
+ const completedBlockerIds = completedBlockerMap.get(issue.identifier);
1338
+ return {
1339
+ id: issue.identifier,
1340
+ title: issue.title,
1341
+ description: issue.description || "",
1342
+ url: issue.url,
1343
+ ...completedBlockerIds && { completedBlockerIds }
1344
+ };
1345
+ }
1346
+ async fetchIssueById(id) {
1347
+ const identifier = parseLinearIdentifier(id);
1348
+ const data = await gql(
1349
+ `query($identifier: String!) {
1350
+ issue(id: $identifier) {
1351
+ id
1352
+ identifier
1353
+ title
1354
+ description
1355
+ url
1356
+ state { name }
1357
+ }
1358
+ }`,
1359
+ { identifier }
1360
+ );
1361
+ if (!data.issue) return null;
1362
+ return {
1363
+ id: data.issue.identifier,
1364
+ title: data.issue.title,
1365
+ description: data.issue.description || "",
1366
+ url: data.issue.url,
1367
+ status: data.issue.state?.name
1368
+ };
1369
+ }
1370
+ async updateStatus(issueId, statusName) {
1371
+ const issueData = await gql(
1372
+ `query($identifier: String!) {
1373
+ issue(id: $identifier) {
1374
+ id
1375
+ team { id }
1376
+ }
1377
+ }`,
1378
+ { identifier: issueId }
1379
+ );
1380
+ const statesData = await gql(
1381
+ `query($teamId: ID!) {
1382
+ workflowStates(filter: { team: { id: { eq: $teamId } } }) {
1383
+ nodes { id name }
1384
+ }
1385
+ }`,
1386
+ { teamId: issueData.issue.team.id }
1387
+ );
1388
+ const state = statesData.workflowStates.nodes.find(
1389
+ (s) => s.name.toLowerCase() === statusName.toLowerCase()
1390
+ );
1391
+ if (!state) {
1392
+ const available = statesData.workflowStates.nodes.map((s) => s.name).join(", ");
1393
+ throw new Error(`Status "${statusName}" not found. Available: ${available}`);
1394
+ }
1395
+ const mutationResult = await gql(
1396
+ `mutation($issueId: String!, $stateId: String!) {
1397
+ issueUpdate(id: $issueId, input: { stateId: $stateId }) {
1398
+ success
1399
+ }
1400
+ }`,
1401
+ { issueId: issueData.issue.id, stateId: state.id }
1402
+ );
1403
+ if (!mutationResult.issueUpdate.success) {
1404
+ throw new Error(
1405
+ `issueUpdate returned success=false for ${issueId} (stateId: ${state.id}, stateName: ${state.name})`
1406
+ );
1407
+ }
1408
+ }
1409
+ async attachPullRequest(_issueId, _prUrl) {
1410
+ }
1411
+ async completeIssue(issueId, statusName, labelToRemove) {
1412
+ const issueData = await gql(
1413
+ `query($identifier: String!) {
1414
+ issue(id: $identifier) {
1415
+ id
1416
+ team { id }
1417
+ labels { nodes { id name } }
1418
+ }
1419
+ }`,
1420
+ { identifier: issueId }
1421
+ );
1422
+ const statesData = await gql(
1423
+ `query($teamId: ID!) {
1424
+ workflowStates(filter: { team: { id: { eq: $teamId } } }) {
1425
+ nodes { id name }
1426
+ }
1427
+ }`,
1428
+ { teamId: issueData.issue.team.id }
1429
+ );
1430
+ const state = statesData.workflowStates.nodes.find(
1431
+ (s) => s.name.toLowerCase() === statusName.toLowerCase()
1432
+ );
1433
+ if (!state) {
1434
+ const available = statesData.workflowStates.nodes.map((s) => s.name).join(", ");
1435
+ throw new Error(`Status "${statusName}" not found. Available: ${available}`);
1436
+ }
1437
+ const input = { stateId: state.id };
1438
+ if (labelToRemove) {
1439
+ const currentLabels = issueData.issue.labels.nodes;
1440
+ const filtered = currentLabels.filter(
1441
+ (l) => l.name.toLowerCase() !== labelToRemove.toLowerCase()
1442
+ );
1443
+ if (filtered.length !== currentLabels.length) {
1444
+ input.labelIds = filtered.map((l) => l.id);
1445
+ }
1446
+ }
1447
+ const mutationResult = await gql(
1448
+ `mutation($issueId: String!, $input: IssueUpdateInput!) {
1449
+ issueUpdate(id: $issueId, input: $input) {
1450
+ success
1451
+ }
1452
+ }`,
1453
+ { issueId: issueData.issue.id, input }
1454
+ );
1455
+ if (!mutationResult.issueUpdate.success) {
1456
+ throw new Error(
1457
+ `issueUpdate returned success=false for ${issueId} (stateId: ${state.id}, stateName: ${state.name})`
1458
+ );
1459
+ }
1460
+ }
1461
+ async listIssues(config) {
1462
+ const labels = normalizeLabels(config);
1463
+ const primaryLabel = labels[0] ?? "";
1464
+ const data = await gql(
1465
+ `query($teamName: String!, $projectName: String!, $labelName: String!, $statusName: String!) {
1466
+ issues(
1467
+ filter: {
1468
+ team: { name: { eq: $teamName } }
1469
+ project: { name: { eq: $projectName } }
1470
+ labels: { name: { eq: $labelName } }
1471
+ state: { name: { eq: $statusName } }
1472
+ }
1473
+ first: 100
1474
+ ) {
1475
+ nodes {
1476
+ identifier
1477
+ title
1478
+ description
1479
+ url
1480
+ labels { nodes { name } }
1481
+ }
1482
+ }
1483
+ }`,
1484
+ {
1485
+ teamName: config.scope,
1486
+ projectName: config.project,
1487
+ labelName: primaryLabel,
1488
+ statusName: config.pick_from
1489
+ }
1490
+ );
1491
+ const filtered = labels.length > 1 ? data.issues.nodes.filter((issue) => {
1492
+ const issueLabels = new Set(issue.labels.nodes.map((l) => l.name.toLowerCase()));
1493
+ return labels.every((l) => issueLabels.has(l.toLowerCase()));
1494
+ }) : data.issues.nodes;
1495
+ return filtered.map((issue) => ({
1496
+ id: issue.identifier,
1497
+ title: issue.title,
1498
+ description: issue.description || "",
1499
+ url: issue.url
1500
+ }));
1501
+ }
1502
+ async addLabel(issueId, labelName) {
1503
+ const issueData = await gql(
1504
+ `query($identifier: String!) {
1505
+ issue(id: $identifier) {
1506
+ id
1507
+ team {
1508
+ id
1509
+ labels { nodes { id name } }
1510
+ }
1511
+ labels { nodes { id name } }
1512
+ }
1513
+ }`,
1514
+ { identifier: issueId }
1515
+ );
1516
+ let teamLabel = issueData.issue.team.labels.nodes.find(
1517
+ (l) => l.name.toLowerCase() === labelName.toLowerCase()
1518
+ );
1519
+ if (!teamLabel) {
1520
+ const created = await gql(
1521
+ `mutation($teamId: String!, $name: String!) {
1522
+ issueLabelCreate(input: { teamId: $teamId, name: $name }) {
1523
+ success
1524
+ issueLabel { id name }
1525
+ }
1526
+ }`,
1527
+ { teamId: issueData.issue.team.id, name: labelName }
1528
+ );
1529
+ if (created.issueLabelCreate.success && created.issueLabelCreate.issueLabel) {
1530
+ log(`Label "${labelName}" created automatically in team ${issueData.issue.team.id}`);
1531
+ teamLabel = created.issueLabelCreate.issueLabel;
1532
+ } else {
1533
+ const refetch = await gql(
1534
+ `query($identifier: String!) {
1535
+ issue(id: $identifier) {
1536
+ team { labels { nodes { id name } } }
1537
+ }
1538
+ }`,
1539
+ { identifier: issueId }
1540
+ );
1541
+ teamLabel = refetch.issue.team.labels.nodes.find(
1542
+ (l) => l.name.toLowerCase() === labelName.toLowerCase()
1543
+ );
1544
+ if (!teamLabel) {
1545
+ throw new Error(`Failed to create or find label "${labelName}" in team`);
1546
+ }
1547
+ }
1548
+ }
1549
+ const alreadyHasLabel = issueData.issue.labels.nodes.some(
1550
+ (l) => l.name.toLowerCase() === labelName.toLowerCase()
1551
+ );
1552
+ if (alreadyHasLabel) return;
1553
+ const newLabelIds = [...issueData.issue.labels.nodes.map((l) => l.id), teamLabel.id];
1554
+ const mutationResult = await gql(
1555
+ `mutation($issueId: String!, $labelIds: [String!]!) {
1556
+ issueUpdate(id: $issueId, input: { labelIds: $labelIds }) {
1557
+ success
1558
+ }
1559
+ }`,
1560
+ {
1561
+ issueId: issueData.issue.id,
1562
+ labelIds: newLabelIds
1563
+ }
1564
+ );
1565
+ if (!mutationResult.issueUpdate.success) {
1566
+ throw new Error(
1567
+ `issueUpdate returned success=false when adding label "${labelName}" to ${issueId}`
1568
+ );
1569
+ }
1570
+ }
1571
+ async listProjects(scope) {
1572
+ const data = await gql(
1573
+ `query($teamName: String!) {
1574
+ teams(filter: { name: { eq: $teamName } }) {
1575
+ nodes { id }
1576
+ }
1577
+ }`,
1578
+ { teamName: scope }
1579
+ );
1580
+ const team = data.teams.nodes[0];
1581
+ if (!team) throw new Error(`Team "${scope}" not found`);
1582
+ const projectData = await gql(
1583
+ `query($teamId: ID!) {
1584
+ projects(filter: { accessibleTeams: { id: { eq: $teamId } } }) {
1585
+ nodes { name }
1586
+ }
1587
+ }`,
1588
+ { teamId: team.id }
1589
+ );
1590
+ return projectData.projects.nodes.map((p) => ({ value: p.name, label: p.name }));
1591
+ }
1592
+ async listStatuses(scope) {
1593
+ const data = await gql(
1594
+ `query($teamName: String!) {
1595
+ teams(filter: { name: { eq: $teamName } }) {
1596
+ nodes { id }
1597
+ }
1598
+ }`,
1599
+ { teamName: scope }
1600
+ );
1601
+ const team = data.teams.nodes[0];
1602
+ if (!team) throw new Error(`Team "${scope}" not found`);
1603
+ const statesData = await gql(
1604
+ `query($teamId: ID!) {
1605
+ workflowStates(filter: { team: { id: { eq: $teamId } } }) {
1606
+ nodes { name type }
1607
+ }
1608
+ }`,
1609
+ { teamId: team.id }
1610
+ );
1611
+ return statesData.workflowStates.nodes.map((s) => ({
1612
+ value: s.name,
1613
+ label: `${s.name} (${s.type})`
1614
+ }));
1615
+ }
1616
+ async removeLabel(issueId, labelName) {
1617
+ const issueData = await gql(
1618
+ `query($identifier: String!) {
1619
+ issue(id: $identifier) {
1620
+ id
1621
+ labels { nodes { id name } }
1622
+ }
1623
+ }`,
1624
+ { identifier: issueId }
1625
+ );
1626
+ const currentLabels = issueData.issue.labels.nodes;
1627
+ const filtered = currentLabels.filter((l) => l.name.toLowerCase() !== labelName.toLowerCase());
1628
+ if (filtered.length === currentLabels.length) return;
1629
+ const mutationResult = await gql(
1630
+ `mutation($issueId: String!, $labelIds: [String!]!) {
1631
+ issueUpdate(id: $issueId, input: { labelIds: $labelIds }) {
1632
+ success
1633
+ }
1634
+ }`,
1635
+ {
1636
+ issueId: issueData.issue.id,
1637
+ labelIds: filtered.map((l) => l.id)
1638
+ }
1639
+ );
1640
+ if (!mutationResult.issueUpdate.success) {
1641
+ throw new Error(
1642
+ `issueUpdate returned success=false when removing label "${labelName}" from ${issueId}`
1643
+ );
1644
+ }
1645
+ }
1646
+ async createIssue(opts, config) {
1647
+ const teamData = await gql(
1648
+ `query($teamName: String!) {
1649
+ teams(filter: { name: { eq: $teamName } }) {
1650
+ nodes { id }
1651
+ }
1652
+ }`,
1653
+ { teamName: config.scope }
1654
+ );
1655
+ const team = teamData.teams.nodes[0];
1656
+ if (!team) throw new Error(`Team "${config.scope}" not found`);
1657
+ const statesData = await gql(
1658
+ `query($teamId: ID!) {
1659
+ workflowStates(filter: { team: { id: { eq: $teamId } } }) {
1660
+ nodes { id name }
1661
+ }
1662
+ }`,
1663
+ { teamId: team.id }
1664
+ );
1665
+ const state = statesData.workflowStates.nodes.find(
1666
+ (s) => s.name.toLowerCase() === opts.status.toLowerCase()
1667
+ );
1668
+ if (!state) {
1669
+ const available = statesData.workflowStates.nodes.map((s) => s.name).join(", ");
1670
+ throw new Error(`Status "${opts.status}" not found. Available: ${available}`);
1671
+ }
1672
+ const labelNames = Array.isArray(opts.label) ? opts.label : [opts.label];
1673
+ const teamLabelsData = await gql(
1674
+ `query($teamName: String!) {
1675
+ teams(filter: { name: { eq: $teamName } }) {
1676
+ nodes { labels { nodes { id name } } }
1677
+ }
1678
+ }`,
1679
+ { teamName: config.scope }
1680
+ );
1681
+ const teamLabels = teamLabelsData.teams.nodes[0]?.labels.nodes ?? [];
1682
+ const labelIds = [];
1683
+ for (const name of labelNames) {
1684
+ const label = teamLabels.find((l) => l.name.toLowerCase() === name.toLowerCase());
1685
+ if (!label) throw new Error(`Label "${name}" not found in team "${config.scope}"`);
1686
+ labelIds.push(label.id);
1687
+ }
1688
+ let projectId;
1689
+ if (config.project) {
1690
+ const projectData = await gql(
1691
+ `query($teamId: ID!) {
1692
+ projects(filter: { accessibleTeams: { id: { eq: $teamId } } }) {
1693
+ nodes { id name }
1694
+ }
1695
+ }`,
1696
+ { teamId: team.id }
1697
+ );
1698
+ const project = projectData.projects.nodes.find(
1699
+ (p) => p.name.toLowerCase() === config.project.toLowerCase()
1700
+ );
1701
+ if (project) projectId = project.id;
1702
+ }
1703
+ const input = {
1704
+ teamId: team.id,
1705
+ title: opts.title,
1706
+ description: opts.description,
1707
+ stateId: state.id,
1708
+ labelIds
1709
+ };
1710
+ if (projectId) input.projectId = projectId;
1711
+ if (opts.order !== void 0) input.priority = opts.order;
1712
+ if (opts.parentId) input.parentId = opts.parentId;
1713
+ const result = await gql(
1714
+ `mutation($input: IssueCreateInput!) {
1715
+ issueCreate(input: $input) {
1716
+ success
1717
+ issue { identifier }
1718
+ }
1719
+ }`,
1720
+ { input }
1721
+ );
1722
+ if (!result.issueCreate.success || !result.issueCreate.issue) {
1723
+ throw new Error("issueCreate returned success=false");
1724
+ }
1725
+ return result.issueCreate.issue.identifier;
1726
+ }
1727
+ async linkDependency(issueId, dependsOnId) {
1728
+ const issueData = await gql(
1729
+ `query($identifier: String!) { issue(id: $identifier) { id } }`,
1730
+ { identifier: dependsOnId }
1731
+ );
1732
+ const dependentData = await gql(
1733
+ `query($identifier: String!) { issue(id: $identifier) { id } }`,
1734
+ { identifier: issueId }
1735
+ );
1736
+ await gql(
1737
+ `mutation($issueId: String!, $relatedIssueId: String!) {
1738
+ issueRelationCreate(input: { issueId: $issueId, relatedIssueId: $relatedIssueId, type: blocks }) {
1739
+ success
1740
+ }
1741
+ }`,
1742
+ {
1743
+ issueId: dependentData.issue.id,
1744
+ relatedIssueId: issueData.issue.id
1745
+ }
1746
+ );
1747
+ }
1748
+ };
1749
+ function parseLinearIdentifier(input) {
1750
+ const urlMatch = input.match(/\/issue\/([A-Z]+-\d+)/);
1751
+ if (urlMatch?.[1]) return urlMatch[1];
1752
+ return input;
1753
+ }
1754
+
1755
+ // src/sources/plane.ts
1756
+ var DEFAULT_BASE_URL = "https://api.plane.so";
1757
+ function getBaseUrl2() {
1758
+ return (process.env.PLANE_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
1759
+ }
1760
+ function getAppUrl() {
1761
+ const base = process.env.PLANE_BASE_URL ?? DEFAULT_BASE_URL;
1762
+ if (base === DEFAULT_BASE_URL || base.replace(/\/$/, "") === DEFAULT_BASE_URL) {
1763
+ return "https://app.plane.so";
1764
+ }
1765
+ return base.replace(/\/$/, "");
1766
+ }
1767
+ function getAuthHeaders2() {
1768
+ const token = process.env.PLANE_API_TOKEN;
1769
+ if (!token) throw new Error("PLANE_API_TOKEN must be set");
1770
+ return { "X-Api-Key": token };
1771
+ }
1772
+ function escapeHtml(str) {
1773
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1774
+ }
1775
+ var _api2 = null;
1776
+ function api2() {
1777
+ if (!_api2) _api2 = createApiClient(`${getBaseUrl2()}/api/v1`, getAuthHeaders2, "Plane");
1778
+ return _api2;
1779
+ }
1780
+ function planeGet(path) {
1781
+ return api2().get(path);
1782
+ }
1783
+ function planePost(path, body) {
1784
+ return api2().post(path, body);
1785
+ }
1786
+ function planePatch(path, body) {
1787
+ return api2().patch(path, body);
1788
+ }
1789
+ async function fetchAll(path) {
1790
+ const data = await planeGet(path);
1791
+ if (Array.isArray(data)) return data;
1792
+ return data.results ?? [];
1793
+ }
1794
+ var PRIORITY_ORDER = {
1795
+ urgent: 1,
1796
+ high: 2,
1797
+ medium: 3,
1798
+ low: 4,
1799
+ none: 5
1800
+ };
1801
+ function priorityRank2(priority) {
1802
+ return PRIORITY_ORDER[priority.toLowerCase()] ?? 5;
1803
+ }
1804
+ async function resolveProjectId(workspaceSlug, projectIdentifier) {
1805
+ const projects = await fetchAll(`/workspaces/${workspaceSlug}/projects/`);
1806
+ const project = projects.find(
1807
+ (p) => p.identifier.toLowerCase() === projectIdentifier.toLowerCase() || p.name.toLowerCase() === projectIdentifier.toLowerCase() || p.id === projectIdentifier
1808
+ );
1809
+ if (!project) {
1810
+ const available = projects.map((p) => `${p.name} (${p.identifier})`).join(", ");
1811
+ throw new Error(`Plane project "${projectIdentifier}" not found. Available: ${available}`);
1812
+ }
1813
+ return project.id;
1814
+ }
1815
+ async function resolveStateId(workspaceSlug, projectId, stateName) {
1816
+ const states = await fetchAll(
1817
+ `/workspaces/${workspaceSlug}/projects/${projectId}/states/`
1818
+ );
1819
+ const state = states.find((s) => s.name.toLowerCase() === stateName.toLowerCase());
1820
+ if (!state) {
1821
+ const available = states.map((s) => s.name).join(", ");
1822
+ throw new Error(`Plane state "${stateName}" not found. Available: ${available}`);
1823
+ }
1824
+ return state.id;
1825
+ }
1826
+ async function resolveLabelId(workspaceSlug, projectId, labelName) {
1827
+ const labels = await fetchAll(
1828
+ `/workspaces/${workspaceSlug}/projects/${projectId}/labels/`
1829
+ );
1830
+ const label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
1831
+ if (!label) {
1832
+ const available = labels.map((l) => l.name).join(", ");
1833
+ throw new Error(`Plane label "${labelName}" not found. Available: ${available}`);
1834
+ }
1835
+ return label.id;
1836
+ }
1837
+ async function fetchLabels(workspaceSlug, projectId) {
1838
+ return fetchAll(`/workspaces/${workspaceSlug}/projects/${projectId}/labels/`);
1839
+ }
1840
+ function makeIssueId(workspaceSlug, projectId, issueId) {
1841
+ return `${workspaceSlug}::${projectId}::${issueId}`;
1842
+ }
1843
+ function parseIssueId(id) {
1844
+ const urlMatch = id.match(/\/([^/]+)\/projects\/([^/]+)\/issues\/([^/?#]+)/);
1845
+ if (urlMatch?.[1] && urlMatch?.[2] && urlMatch?.[3]) {
1846
+ return { workspaceSlug: urlMatch[1], projectId: urlMatch[2], issueId: urlMatch[3] };
1847
+ }
1848
+ const parts = id.split("::");
1849
+ if (parts.length === 3 && parts[0] && parts[1] && parts[2]) {
1850
+ return { workspaceSlug: parts[0], projectId: parts[1], issueId: parts[2] };
1851
+ }
1852
+ throw new Error(
1853
+ `Cannot parse Plane issue ID: "${id}". Expected URL or "workspace::projectId::issueId" format.`
1854
+ );
1855
+ }
1856
+ var PlaneSource = class {
1857
+ name = "plane";
1858
+ async fetchNextIssue(config) {
1859
+ const workspaceSlug = config.scope;
1860
+ const projectId = await resolveProjectId(workspaceSlug, config.project);
1861
+ const stateId = await resolveStateId(workspaceSlug, projectId, config.pick_from);
1862
+ const labelNames = normalizeLabels(config);
1863
+ const labelIds = await Promise.all(
1864
+ labelNames.map((name) => resolveLabelId(workspaceSlug, projectId, name))
1865
+ );
1866
+ const data = await planeGet(
1867
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/?state=${stateId}&per_page=100`
1868
+ );
1869
+ const matching = data.results.filter((i) => i.state === stateId).filter((i) => labelIds.every((lid) => i.labels.includes(lid)));
1870
+ if (matching.length === 0) return null;
1871
+ const allStates = await fetchAll(
1872
+ `/workspaces/${workspaceSlug}/projects/${projectId}/states/`
1873
+ );
1874
+ const doneGroups = /* @__PURE__ */ new Set(["completed", "cancelled"]);
1875
+ const doneStateIds = new Set(allStates.filter((s) => doneGroups.has(s.group)).map((s) => s.id));
1876
+ const unblocked = [];
1877
+ const blocked = [];
1878
+ for (const issue2 of matching) {
1879
+ const relations = await fetchAll(
1880
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${issue2.id}/relations/`
1881
+ );
1882
+ const blockerIds = relations.filter((r) => r.relation_type === "blocked_by").map((r) => r.related_issue);
1883
+ const activeBlockers = [];
1884
+ for (const blockerId of blockerIds) {
1885
+ try {
1886
+ const blocker = await planeGet(
1887
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${blockerId}/`
1888
+ );
1889
+ if (!doneStateIds.has(blocker.state)) {
1890
+ activeBlockers.push(blockerId);
1891
+ }
1892
+ } catch (err) {
1893
+ warn(`Could not fetch blocker ${blockerId}: ${formatError(err)}`);
1894
+ activeBlockers.push(blockerId);
1895
+ }
1896
+ }
1897
+ if (activeBlockers.length === 0) {
1898
+ unblocked.push(issue2);
1899
+ } else {
1900
+ blocked.push({ id: issue2.id, name: issue2.name, blockers: activeBlockers });
1901
+ }
1902
+ }
1903
+ if (unblocked.length === 0) {
1904
+ if (blocked.length > 0) {
1905
+ warn("No unblocked issues found. Blocked issues:");
1906
+ for (const entry of blocked) {
1907
+ warn(` ${entry.name} \u2014 blocked by: ${entry.blockers.join(", ")}`);
1908
+ }
1909
+ }
1910
+ return null;
1911
+ }
1912
+ const sorted = [...unblocked].sort(
1913
+ (a, b) => priorityRank2(a.priority) - priorityRank2(b.priority)
1914
+ );
1915
+ const issue = sorted[0];
1916
+ if (!issue) return null;
1917
+ const webUrl = `${getAppUrl()}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`;
1918
+ return {
1919
+ id: makeIssueId(workspaceSlug, projectId, issue.id),
1920
+ title: issue.name,
1921
+ description: issue.description_stripped ?? "",
1922
+ url: webUrl
1923
+ };
1924
+ }
1925
+ async fetchIssueById(id) {
1926
+ try {
1927
+ const { workspaceSlug, projectId, issueId } = parseIssueId(id);
1928
+ const issue = await planeGet(
1929
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${issueId}/`
1930
+ );
1931
+ const webUrl = `${getAppUrl()}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`;
1932
+ let stateName;
1933
+ try {
1934
+ const states = await fetchAll(
1935
+ `/workspaces/${workspaceSlug}/projects/${projectId}/states/`
1936
+ );
1937
+ stateName = states.find((s) => s.id === issue.state)?.name;
1938
+ } catch {
1939
+ }
1940
+ return {
1941
+ id: makeIssueId(workspaceSlug, projectId, issue.id),
1942
+ title: issue.name,
1943
+ description: issue.description_stripped ?? "",
1944
+ url: webUrl,
1945
+ status: stateName
1946
+ };
1947
+ } catch {
1948
+ return null;
1949
+ }
1950
+ }
1951
+ async updateStatus(issueId, stateName) {
1952
+ const { workspaceSlug, projectId, issueId: planeIssueId } = parseIssueId(issueId);
1953
+ const stateId = await resolveStateId(workspaceSlug, projectId, stateName);
1954
+ await planePatch(
1955
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${planeIssueId}/`,
1956
+ { state: stateId }
1957
+ );
1958
+ }
1959
+ async attachPullRequest(issueId, prUrl) {
1960
+ const { workspaceSlug, projectId, issueId: planeIssueId } = parseIssueId(issueId);
1961
+ await planePost(
1962
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${planeIssueId}/comments/`,
1963
+ {
1964
+ comment_html: `<p>Pull request: <a href="${escapeHtml(prUrl)}">${escapeHtml(prUrl)}</a></p>`
1965
+ }
1966
+ );
1967
+ }
1968
+ async completeIssue(issueId, stateName, labelToRemove) {
1969
+ await this.updateStatus(issueId, stateName);
1970
+ if (labelToRemove) {
1971
+ await this.removeLabel(issueId, labelToRemove);
1972
+ }
1973
+ }
1974
+ async listIssues(config) {
1975
+ const workspaceSlug = config.scope;
1976
+ const projectId = await resolveProjectId(workspaceSlug, config.project);
1977
+ const stateId = await resolveStateId(workspaceSlug, projectId, config.pick_from);
1978
+ const labelNames = normalizeLabels(config);
1979
+ const labelIds = await Promise.all(
1980
+ labelNames.map((name) => resolveLabelId(workspaceSlug, projectId, name))
1981
+ );
1982
+ const data = await planeGet(
1983
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/?state=${stateId}&per_page=100`
1984
+ );
1985
+ return data.results.filter((i) => i.state === stateId).filter((i) => labelIds.every((lid) => i.labels.includes(lid))).map((i) => {
1986
+ const webUrl = `${getAppUrl()}/${workspaceSlug}/projects/${projectId}/issues/${i.id}`;
1987
+ return {
1988
+ id: makeIssueId(workspaceSlug, projectId, i.id),
1989
+ title: i.name,
1990
+ description: i.description_stripped ?? "",
1991
+ url: webUrl
1992
+ };
1993
+ });
1994
+ }
1995
+ async listProjects(scope) {
1996
+ const projects = await fetchAll(`/workspaces/${scope}/projects/`);
1997
+ return projects.map((p) => ({ value: p.identifier, label: p.name }));
1998
+ }
1999
+ async listLabels(scope, project) {
2000
+ if (!project) return [];
2001
+ const projectId = await resolveProjectId(scope, project);
2002
+ const labels = await fetchAll(`/workspaces/${scope}/projects/${projectId}/labels/`);
2003
+ return labels.map((l) => ({ value: l.name, label: l.name }));
2004
+ }
2005
+ async listStatuses(scope, project) {
2006
+ if (!project) return [];
2007
+ const projectId = await resolveProjectId(scope, project);
2008
+ const states = await fetchAll(`/workspaces/${scope}/projects/${projectId}/states/`);
2009
+ return states.map((s) => ({ value: s.name, label: `${s.name} (${s.group})` }));
2010
+ }
2011
+ async removeLabel(issueId, labelName) {
2012
+ const { workspaceSlug, projectId, issueId: planeIssueId } = parseIssueId(issueId);
2013
+ const issue = await planeGet(
2014
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${planeIssueId}/`
2015
+ );
2016
+ const labels = await fetchLabels(workspaceSlug, projectId);
2017
+ const labelObj = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
2018
+ if (!labelObj || !issue.labels.includes(labelObj.id)) return;
2019
+ const updatedLabels = issue.labels.filter((lid) => lid !== labelObj.id);
2020
+ await planePatch(
2021
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${planeIssueId}/`,
2022
+ { labels: updatedLabels }
2023
+ );
2024
+ }
2025
+ async createIssue(opts, config) {
2026
+ const workspaceSlug = config.scope;
2027
+ const projectId = await resolveProjectId(workspaceSlug, config.project);
2028
+ const stateId = await resolveStateId(workspaceSlug, projectId, opts.status);
2029
+ const labelNames = Array.isArray(opts.label) ? opts.label : [opts.label];
2030
+ const labelIds = await Promise.all(
2031
+ labelNames.map((name) => resolveLabelId(workspaceSlug, projectId, name))
2032
+ );
2033
+ const body = {
2034
+ name: opts.title,
2035
+ description_html: `<p>${escapeHtml(opts.description)}</p>`,
2036
+ state: stateId,
2037
+ label_ids: labelIds
2038
+ };
2039
+ if (opts.order !== void 0) body.priority = opts.order;
2040
+ if (opts.parentId) {
2041
+ const parent = parseIssueId(opts.parentId);
2042
+ body.parent = parent.issueId;
2043
+ }
2044
+ const issue = await planePost(
2045
+ `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/`,
2046
+ body
2047
+ );
2048
+ return makeIssueId(workspaceSlug, projectId, issue.id);
2049
+ }
2050
+ };
2051
+
2052
+ // src/sources/shortcut.ts
2053
+ function getAuthHeaders3() {
2054
+ const token = process.env.SHORTCUT_API_TOKEN;
2055
+ if (!token) throw new Error("SHORTCUT_API_TOKEN must be set");
2056
+ return { "Shortcut-Token": token };
2057
+ }
2058
+ var _api3 = null;
2059
+ function api3() {
2060
+ if (!_api3) _api3 = createApiClient("https://api.app.shortcut.com", getAuthHeaders3, "Shortcut");
2061
+ return _api3;
2062
+ }
2063
+ function shortcutGet(path) {
2064
+ return api3().get(path);
2065
+ }
2066
+ function shortcutPost(path, body) {
2067
+ return api3().post(path, body);
2068
+ }
2069
+ function shortcutPut(path, body) {
2070
+ return api3().put(path, body);
2071
+ }
2072
+ function extractStories(result) {
2073
+ if (Array.isArray(result)) return result;
2074
+ return result.data ?? [];
2075
+ }
2076
+ function extractNext(result) {
2077
+ if (Array.isArray(result)) return null;
2078
+ return result.next ?? null;
2079
+ }
2080
+ async function searchStoriesAll(body) {
2081
+ const all = [];
2082
+ let next = null;
2083
+ do {
2084
+ const req = next ? { ...body, next } : body;
2085
+ const result = await shortcutPost(
2086
+ "/api/v3/stories/search",
2087
+ req
2088
+ );
2089
+ all.push(...extractStories(result));
2090
+ next = extractNext(result);
2091
+ } while (next);
2092
+ return all;
2093
+ }
2094
+ async function resolveWorkflowStateId(stateName) {
2095
+ const workflows = await shortcutGet("/api/v3/workflows");
2096
+ for (const workflow of workflows) {
2097
+ const state = workflow.states.find((s) => s.name.toLowerCase() === stateName.toLowerCase());
2098
+ if (state) return state.id;
2099
+ }
2100
+ const allStates = workflows.flatMap((w) => w.states.map((s) => s.name));
2101
+ throw new Error(
2102
+ `Shortcut workflow state "${stateName}" not found. Available: ${allStates.join(", ")}`
2103
+ );
2104
+ }
2105
+ async function resolveAllWorkflowStateIds(stateName) {
2106
+ const workflows = await shortcutGet("/api/v3/workflows");
2107
+ const ids = [];
2108
+ for (const workflow of workflows) {
2109
+ for (const state of workflow.states) {
2110
+ if (state.name.toLowerCase() === stateName.toLowerCase()) {
2111
+ ids.push(state.id);
2112
+ }
2113
+ }
2114
+ }
2115
+ if (ids.length === 0) {
2116
+ const allStates = workflows.flatMap((w) => w.states.map((s) => s.name));
2117
+ throw new Error(
2118
+ `Shortcut workflow state "${stateName}" not found. Available: ${allStates.join(", ")}`
2119
+ );
2120
+ }
2121
+ return ids;
2122
+ }
2123
+ async function resolveLabelId2(labelName) {
2124
+ const labels = await shortcutGet("/api/v3/labels");
2125
+ const label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase() && !l.archived);
2126
+ if (!label) {
2127
+ const available = labels.filter((l) => !l.archived).map((l) => l.name).join(", ");
2128
+ throw new Error(`Shortcut label "${labelName}" not found. Available: ${available}`);
2129
+ }
2130
+ return label.id;
2131
+ }
2132
+ function priorityRank3(priority) {
2133
+ if (priority === null) return Number.MAX_SAFE_INTEGER;
2134
+ return priority;
2135
+ }
2136
+ var ShortcutSource = class {
2137
+ name = "shortcut";
2138
+ async fetchNextIssue(config) {
2139
+ const stateIds = await resolveAllWorkflowStateIds(config.pick_from);
2140
+ const labelNames = normalizeLabels(config);
2141
+ const primaryLabel = labelNames[0] ?? "";
2142
+ const labelIds = await Promise.all(labelNames.map((name) => resolveLabelId2(name)));
2143
+ const workflows = await shortcutGet("/api/v3/workflows");
2144
+ const doneStateIds = /* @__PURE__ */ new Set();
2145
+ for (const workflow of workflows) {
2146
+ for (const state of workflow.states) {
2147
+ if (state.type === "done") {
2148
+ doneStateIds.add(state.id);
2149
+ }
2150
+ }
2151
+ }
2152
+ const seen = /* @__PURE__ */ new Set();
2153
+ const allStories = [];
2154
+ for (const stateId of stateIds) {
2155
+ for (const story2 of await searchStoriesAll({
2156
+ workflow_state_id: stateId,
2157
+ label_name: primaryLabel,
2158
+ archived: false
2159
+ })) {
2160
+ if (!seen.has(story2.id)) {
2161
+ seen.add(story2.id);
2162
+ allStories.push(story2);
2163
+ }
2164
+ }
2165
+ }
2166
+ const stories = labelIds.length > 1 ? allStories.filter((s) => labelIds.every((lid) => s.label_ids.includes(lid))) : allStories;
2167
+ if (stories.length === 0) return null;
2168
+ const unblocked = [];
2169
+ const blocked = [];
2170
+ for (const story2 of stories) {
2171
+ const storyLinks = story2.story_links ?? [];
2172
+ const blockerIds = storyLinks.filter((link) => link.verb === "blocks" && link.object_id === story2.id).map((link) => link.subject_id);
2173
+ const activeBlockers = [];
2174
+ for (const blockerId of blockerIds) {
2175
+ try {
2176
+ const blocker = await shortcutGet(`/api/v3/stories/${blockerId}`);
2177
+ if (!doneStateIds.has(blocker.workflow_state_id)) {
2178
+ activeBlockers.push(blockerId);
2179
+ }
2180
+ } catch (err) {
2181
+ warn(`Could not fetch blocker ${blockerId}: ${formatError(err)}`);
2182
+ activeBlockers.push(blockerId);
2183
+ }
2184
+ }
2185
+ if (activeBlockers.length === 0) {
2186
+ unblocked.push(story2);
2187
+ } else {
2188
+ blocked.push({ id: story2.id, name: story2.name, blockers: activeBlockers });
2189
+ }
2190
+ }
2191
+ if (unblocked.length === 0) {
2192
+ if (blocked.length > 0) {
2193
+ warn("No unblocked issues found. Blocked issues:");
2194
+ for (const entry of blocked) {
2195
+ warn(
2196
+ ` #${entry.id} (${entry.name}) \u2014 blocked by: ${entry.blockers.map((b) => `#${b}`).join(", ")}`
2197
+ );
2198
+ }
2199
+ }
2200
+ return null;
2201
+ }
2202
+ const sorted = [...unblocked].sort((a, b) => {
2203
+ const pa = priorityRank3(a.priority);
2204
+ const pb = priorityRank3(b.priority);
2205
+ if (pa !== pb) return pa - pb;
2206
+ return a.position - b.position;
2207
+ });
2208
+ const story = sorted[0];
2209
+ if (!story) return null;
2210
+ return {
2211
+ id: String(story.id),
2212
+ title: story.name,
2213
+ description: story.description ?? "",
2214
+ url: story.app_url
2215
+ };
2216
+ }
2217
+ async fetchIssueById(id) {
2218
+ const storyId = parseShortcutIdentifier(id);
2219
+ try {
2220
+ const story = await shortcutGet(`/api/v3/stories/${storyId}`);
2221
+ let stateName;
2222
+ try {
2223
+ const workflows = await shortcutGet("/api/v3/workflows");
2224
+ for (const workflow of workflows) {
2225
+ const state = workflow.states.find((s) => s.id === story.workflow_state_id);
2226
+ if (state) {
2227
+ stateName = state.name;
2228
+ break;
2229
+ }
2230
+ }
2231
+ } catch {
2232
+ }
2233
+ return {
2234
+ id: String(story.id),
2235
+ title: story.name,
2236
+ description: story.description ?? "",
2237
+ url: story.app_url,
2238
+ status: stateName
2239
+ };
2240
+ } catch {
2241
+ return null;
2242
+ }
2243
+ }
2244
+ async updateStatus(storyId, stateName) {
2245
+ const stateId = await resolveWorkflowStateId(stateName);
2246
+ await shortcutPut(`/api/v3/stories/${storyId}`, {
2247
+ workflow_state_id: stateId
2248
+ });
2249
+ }
2250
+ async attachPullRequest(storyId, prUrl) {
2251
+ await shortcutPost(`/api/v3/stories/${storyId}/comments`, {
2252
+ text: `Pull request: ${prUrl}`
2253
+ });
2254
+ }
2255
+ async completeIssue(storyId, stateName, labelToRemove) {
2256
+ await this.updateStatus(storyId, stateName);
2257
+ if (labelToRemove) {
2258
+ await this.removeLabel(storyId, labelToRemove);
2259
+ }
2260
+ }
2261
+ async listIssues(config) {
2262
+ const stateIds = await resolveAllWorkflowStateIds(config.pick_from);
2263
+ const labelNames = normalizeLabels(config);
2264
+ const primaryLabel = labelNames[0] ?? "";
2265
+ const labelIds = await Promise.all(labelNames.map((name) => resolveLabelId2(name)));
2266
+ const seen = /* @__PURE__ */ new Set();
2267
+ const allStories = [];
2268
+ for (const stateId of stateIds) {
2269
+ for (const story of await searchStoriesAll({
2270
+ workflow_state_id: stateId,
2271
+ label_name: primaryLabel,
2272
+ archived: false
2273
+ })) {
2274
+ if (!seen.has(story.id)) {
2275
+ seen.add(story.id);
2276
+ allStories.push(story);
2277
+ }
2278
+ }
2279
+ }
2280
+ const stories = labelIds.length > 1 ? allStories.filter((s) => labelIds.every((lid) => s.label_ids.includes(lid))) : allStories;
2281
+ return stories.map((story) => ({
2282
+ id: String(story.id),
2283
+ title: story.name,
2284
+ description: story.description ?? "",
2285
+ url: story.app_url
2286
+ }));
2287
+ }
2288
+ async listStatuses() {
2289
+ const workflows = await shortcutGet("/api/v3/workflows");
2290
+ const seen = /* @__PURE__ */ new Set();
2291
+ const result = [];
2292
+ for (const workflow of workflows) {
2293
+ for (const state of workflow.states) {
2294
+ if (!seen.has(state.name)) {
2295
+ seen.add(state.name);
2296
+ result.push({ value: state.name, label: `${state.name} (${state.type})` });
2297
+ }
2298
+ }
2299
+ }
2300
+ return result;
2301
+ }
2302
+ async removeLabel(storyId, labelName) {
2303
+ const story = await shortcutGet(`/api/v3/stories/${storyId}`);
2304
+ const allLabels = await shortcutGet("/api/v3/labels");
2305
+ const labelToRemove = allLabels.find(
2306
+ (l) => l.name.toLowerCase() === labelName.toLowerCase() && !l.archived
2307
+ );
2308
+ if (!labelToRemove || !story.label_ids.includes(labelToRemove.id)) return;
2309
+ const remainingIds = story.label_ids.filter((lid) => lid !== labelToRemove.id);
2310
+ const labelNames = remainingIds.map((lid) => allLabels.find((l) => l.id === lid)).filter((l) => l !== void 0).map((l) => ({ name: l.name }));
2311
+ await shortcutPut(`/api/v3/stories/${storyId}`, {
2312
+ labels: labelNames
2313
+ });
2314
+ }
2315
+ async createIssue(opts, _config) {
2316
+ const stateId = await resolveWorkflowStateId(opts.status);
2317
+ const labelNames = Array.isArray(opts.label) ? opts.label : [opts.label];
2318
+ const body = {
2319
+ name: opts.title,
2320
+ description: opts.description,
2321
+ workflow_state_id: stateId,
2322
+ labels: labelNames.map((name) => ({ name }))
2323
+ };
2324
+ if (opts.parentId) body.epic_id = Number(opts.parentId);
2325
+ const story = await shortcutPost("/api/v3/stories", body);
2326
+ return String(story.id);
2327
+ }
2328
+ async linkDependency(issueId, dependsOnId) {
2329
+ await shortcutPost("/api/v3/story-links", {
2330
+ subject_id: Number(dependsOnId),
2331
+ object_id: Number(issueId),
2332
+ verb: "blocks"
2333
+ });
2334
+ }
2335
+ };
2336
+ function parseShortcutIdentifier(input) {
2337
+ const urlMatch = input.match(/\/story\/(\d+)/);
2338
+ if (urlMatch?.[1]) return urlMatch[1];
2339
+ return input;
2340
+ }
2341
+
2342
+ // src/sources/trello.ts
2343
+ var API_URL2 = "https://api.trello.com/1";
2344
+ function getAuthHeaders4() {
2345
+ const key = process.env.TRELLO_API_KEY;
2346
+ const token = process.env.TRELLO_TOKEN;
2347
+ if (!key || !token) throw new Error("TRELLO_API_KEY and TRELLO_TOKEN must be set");
2348
+ return {
2349
+ Authorization: `OAuth oauth_consumer_key="${key}", oauth_token="${token}"`
2350
+ };
2351
+ }
2352
+ async function trelloFetch(method, path, params = "") {
2353
+ const sep = params ? "?" : "";
2354
+ const url = `${API_URL2}${path}${sep}${params}`;
2355
+ const res = await fetch(url, {
2356
+ method,
2357
+ headers: getAuthHeaders4(),
2358
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
2359
+ });
2360
+ if (!res.ok) {
2361
+ const text = await res.text();
2362
+ throw new Error(`Trello API error (${res.status}): ${text}`);
2363
+ }
2364
+ if (method === "DELETE") return void 0;
2365
+ return await res.json();
2366
+ }
2367
+ async function trelloGet(path, params = "") {
2368
+ return trelloFetch("GET", path, params);
2369
+ }
2370
+ async function trelloPut(path, params = "") {
2371
+ return trelloFetch("PUT", path, params);
2372
+ }
2373
+ async function trelloPost(path, params = "") {
2374
+ return trelloFetch("POST", path, params);
2375
+ }
2376
+ async function trelloDelete(path) {
2377
+ await trelloFetch("DELETE", path);
2378
+ }
2379
+ async function findBoardByName(name) {
2380
+ const boards = await trelloGet("/members/me/boards", "fields=name");
2381
+ const board = boards.find((b) => b.name.toLowerCase() === name.toLowerCase());
2382
+ if (!board) throw new Error(`Trello board "${name}" not found`);
2383
+ return board;
2384
+ }
2385
+ async function findListByName(boardId, name) {
2386
+ const lists = await trelloGet(`/boards/${boardId}/lists`, "fields=name");
2387
+ const list = lists.find((l) => l.name.toLowerCase() === name.toLowerCase());
2388
+ if (!list) {
2389
+ const available = lists.map((l) => l.name).join(", ");
2390
+ throw new Error(`Trello list "${name}" not found. Available: ${available}`);
2391
+ }
2392
+ return list;
2393
+ }
2394
+ async function findLabelByName(boardId, name) {
2395
+ const labels = await trelloGet(`/boards/${boardId}/labels`, "fields=name");
2396
+ const label = labels.find((l) => l.name.toLowerCase() === name.toLowerCase());
2397
+ if (!label) throw new Error(`Trello label "${name}" not found`);
2398
+ return label;
2399
+ }
2400
+ var TrelloSource = class {
2401
+ name = "trello";
2402
+ async fetchNextIssue(config) {
2403
+ const board = await findBoardByName(config.scope);
2404
+ const list = await findListByName(board.id, config.pick_from);
2405
+ const labelIds = await Promise.all(
2406
+ normalizeLabels(config).map((name) => findLabelByName(board.id, name).then((l) => l.id))
2407
+ );
2408
+ const cards = await trelloGet(
2409
+ `/lists/${list.id}/cards`,
2410
+ "fields=name,desc,url,idLabels,idList"
2411
+ );
2412
+ const matching = cards.filter((c) => labelIds.every((lid) => c.idLabels.includes(lid)));
2413
+ if (matching.length === 0) return null;
2414
+ const card = matching[0];
2415
+ if (!card) return null;
2416
+ return {
2417
+ id: card.id,
2418
+ title: card.name,
2419
+ description: card.desc || "",
2420
+ url: card.url
2421
+ };
2422
+ }
2423
+ async fetchIssueById(id) {
2424
+ const shortLink = parseTrelloIdentifier(id);
2425
+ try {
2426
+ const card = await trelloGet(
2427
+ `/cards/${shortLink}`,
2428
+ "fields=name,desc,url,idLabels,idList&list=true&list_fields=name"
2429
+ );
2430
+ return {
2431
+ id: card.id,
2432
+ title: card.name,
2433
+ description: card.desc || "",
2434
+ url: card.url,
2435
+ status: card.list?.name
2436
+ };
2437
+ } catch {
2438
+ return null;
2439
+ }
2440
+ }
2441
+ async updateStatus(cardId, listName) {
2442
+ const card = await trelloGet(`/cards/${cardId}`, "fields=idBoard");
2443
+ const list = await findListByName(card.idBoard, listName);
2444
+ await trelloPut(`/cards/${cardId}`, `idList=${list.id}`);
2445
+ }
2446
+ async attachPullRequest(cardId, prUrl) {
2447
+ await trelloPost(`/cards/${cardId}/attachments`, `url=${encodeURIComponent(prUrl)}`);
2448
+ }
2449
+ async completeIssue(cardId, listName, labelToRemove) {
2450
+ await this.updateStatus(cardId, listName);
2451
+ if (labelToRemove) {
2452
+ await this.removeLabel(cardId, labelToRemove);
2453
+ }
2454
+ }
2455
+ async listIssues(config) {
2456
+ const board = await findBoardByName(config.scope);
2457
+ const list = await findListByName(board.id, config.pick_from);
2458
+ const labelIds = await Promise.all(
2459
+ normalizeLabels(config).map((name) => findLabelByName(board.id, name).then((l) => l.id))
2460
+ );
2461
+ const cards = await trelloGet(
2462
+ `/lists/${list.id}/cards`,
2463
+ "fields=name,desc,url,idLabels,idList"
2464
+ );
2465
+ return cards.filter((c) => labelIds.every((lid) => c.idLabels.includes(lid))).map((c) => ({
2466
+ id: c.id,
2467
+ title: c.name,
2468
+ description: c.desc || "",
2469
+ url: c.url
2470
+ }));
2471
+ }
2472
+ async listScopes() {
2473
+ const boards = await trelloGet("/members/me/boards", "fields=name");
2474
+ return boards.map((b) => ({ value: b.name, label: b.name }));
2475
+ }
2476
+ async listLabels(scope) {
2477
+ const board = await findBoardByName(scope);
2478
+ const labels = await trelloGet(`/boards/${board.id}/labels`, "fields=name");
2479
+ return labels.filter((l) => l.name.length > 0).map((l) => ({ value: l.name, label: l.name }));
2480
+ }
2481
+ async listStatuses(scope) {
2482
+ const board = await findBoardByName(scope);
2483
+ const lists = await trelloGet(`/boards/${board.id}/lists`, "fields=name");
2484
+ return lists.map((l) => ({ value: l.name, label: l.name }));
2485
+ }
2486
+ async removeLabel(cardId, labelName) {
2487
+ const card = await trelloGet(
2488
+ `/cards/${cardId}`,
2489
+ "fields=idBoard,idLabels"
2490
+ );
2491
+ const label = await findLabelByName(card.idBoard, labelName);
2492
+ if (!card.idLabels.includes(label.id)) return;
2493
+ await trelloDelete(`/cards/${cardId}/idLabels/${label.id}`);
2494
+ }
2495
+ async createIssue(opts, config) {
2496
+ const board = await findBoardByName(config.scope);
2497
+ const list = await findListByName(board.id, opts.status);
2498
+ const labelNames = Array.isArray(opts.label) ? opts.label : [opts.label];
2499
+ const labelIds = await Promise.all(
2500
+ labelNames.map((name) => findLabelByName(board.id, name).then((l) => l.id))
2501
+ );
2502
+ const params = [
2503
+ `idList=${list.id}`,
2504
+ `name=${encodeURIComponent(opts.title)}`,
2505
+ `desc=${encodeURIComponent(opts.description)}`,
2506
+ `pos=${opts.order ?? "bottom"}`,
2507
+ ...labelIds.map((id) => `idLabels=${id}`)
2508
+ ].join("&");
2509
+ const card = await trelloPost("/cards", params);
2510
+ return card.id;
2511
+ }
2512
+ };
2513
+ function parseTrelloIdentifier(input) {
2514
+ const urlMatch = input.match(/\/c\/([a-zA-Z0-9]+)/);
2515
+ if (urlMatch?.[1]) return urlMatch[1];
2516
+ return input;
2517
+ }
2518
+
2519
+ // src/sources/index.ts
2520
+ var sources = {
2521
+ linear: () => new LinearSource(),
2522
+ trello: () => new TrelloSource(),
2523
+ plane: () => new PlaneSource(),
2524
+ shortcut: () => new ShortcutSource(),
2525
+ "gitlab-issues": () => new GitLabIssuesSource(),
2526
+ "github-issues": () => new GitHubIssuesSource(),
2527
+ jira: () => new JiraSource()
2528
+ };
2529
+ function createSource(name) {
2530
+ const factory = sources[name];
2531
+ if (!factory) {
2532
+ throw new Error(`Unknown source: ${name}. Available: ${Object.keys(sources).join(", ")}`);
2533
+ }
2534
+ return factory();
2535
+ }
2536
+
2537
+ // src/loop/models.ts
2538
+ var WATCH_POLL_INTERVAL_MS = 6e4;
2539
+ function resolveModels(config) {
2540
+ const providerModels = config.provider_options?.[config.provider]?.models;
2541
+ if (!providerModels || providerModels.length === 0) {
2542
+ return [{ provider: config.provider }];
2543
+ }
2544
+ const knownProviders = /* @__PURE__ */ new Set([
2545
+ "claude",
2546
+ "gemini",
2547
+ "opencode",
2548
+ "copilot",
2549
+ "cursor",
2550
+ "goose",
2551
+ "aider",
2552
+ "codex"
2553
+ ]);
2554
+ for (const m of providerModels) {
2555
+ if (knownProviders.has(m) && m !== config.provider) {
2556
+ warn(
2557
+ `Model "${m}" looks like a provider name but provider is "${config.provider}". Since v1.4.0, "models" lists model names within the configured provider, not provider names. Update your .lisa/config.yaml.`
2558
+ );
2559
+ }
2560
+ }
2561
+ for (const m of providerModels) {
2562
+ if (m.includes("/") && m.startsWith(`${config.provider}/`)) {
2563
+ warn(
2564
+ `Model "${m}" starts with the provider name "${config.provider}/". Most provider CLIs expect just the model name (e.g. "${m.slice(config.provider.length + 1)}"). If the provider fails silently, try removing the "${config.provider}/" prefix.`
2565
+ );
2566
+ }
2567
+ }
2568
+ if (config.provider === "cursor") {
2569
+ const hasAuto = providerModels.some((m) => m.toLowerCase() === "auto");
2570
+ if (!hasAuto) {
2571
+ warn(
2572
+ "Cursor Free plan detected (or model not set to 'auto'). Forcing 'auto' model. Set model to 'auto' explicitly in .lisa/config.yaml to silence this warning."
2573
+ );
2574
+ return [{ provider: config.provider, model: "auto" }];
2575
+ }
2576
+ }
2577
+ return providerModels.map((m) => ({
2578
+ provider: config.provider,
2579
+ model: m === config.provider ? void 0 : m
2580
+ }));
2581
+ }
2582
+
2583
+ // src/session/context-manager.ts
2584
+ import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync3 } from "fs";
2585
+ import { join as join3 } from "path";
2586
+ function getContextPath(dir) {
2587
+ return join3(dir, ".lisa", "context.md");
2588
+ }
2589
+ function contextExists(dir) {
2590
+ return existsSync(getContextPath(dir));
2591
+ }
2592
+ function readContext(dir) {
2593
+ const path = getContextPath(dir);
2594
+ if (!existsSync(path)) return null;
2595
+ try {
2596
+ return readFileSync(path, "utf-8");
2597
+ } catch {
2598
+ return null;
2599
+ }
2600
+ }
2601
+
2602
+ // src/session/proof-of-work.ts
2603
+ import { spawn as spawn3 } from "child_process";
2604
+ var DEFAULT_TIMEOUT = 12e4;
2605
+ function runValidationCommand(cmd, cwd, timeoutMs) {
2606
+ const start = Date.now();
2607
+ return new Promise((resolve2) => {
2608
+ const proc = spawn3("sh", ["-c", cmd.run], {
2609
+ cwd,
2610
+ stdio: ["ignore", "pipe", "pipe"],
2611
+ env: process.env
2612
+ });
2613
+ let output = "";
2614
+ let killed = false;
2615
+ const timer = setTimeout(() => {
2616
+ killed = true;
2617
+ proc.kill("SIGTERM");
2618
+ setTimeout(() => {
2619
+ try {
2620
+ proc.kill("SIGKILL");
2621
+ } catch {
2622
+ }
2623
+ }, 1e3);
2624
+ }, timeoutMs);
2625
+ proc.stdout?.on("data", (data) => {
2626
+ output += data.toString();
2627
+ });
2628
+ proc.stderr?.on("data", (data) => {
2629
+ output += data.toString();
2630
+ });
2631
+ proc.on("close", (code) => {
2632
+ clearTimeout(timer);
2633
+ const duration = Date.now() - start;
2634
+ if (killed) {
2635
+ resolve2({
2636
+ name: cmd.name,
2637
+ success: false,
2638
+ output: `${output}
2639
+ [lisa-validation] Command timed out after ${timeoutMs}ms`,
2640
+ duration
2641
+ });
2642
+ } else {
2643
+ resolve2({ name: cmd.name, success: code === 0, output, duration });
2644
+ }
2645
+ });
2646
+ proc.on("error", (err) => {
2647
+ clearTimeout(timer);
2648
+ resolve2({
2649
+ name: cmd.name,
2650
+ success: false,
2651
+ output: `Spawn error: ${err.message}`,
2652
+ duration: Date.now() - start
2653
+ });
2654
+ });
2655
+ });
2656
+ }
2657
+ async function runValidationCommands(commands, cwd, timeoutMs) {
2658
+ const timeout = timeoutMs ?? DEFAULT_TIMEOUT;
2659
+ const results = [];
2660
+ for (const cmd of commands) {
2661
+ log(`Validating: ${cmd.name} \u2014 ${cmd.run}`);
2662
+ const result = await runValidationCommand(cmd, cwd, timeout);
2663
+ results.push(result);
2664
+ if (result.success) {
2665
+ ok(`${cmd.name}: passed (${Math.round(result.duration / 1e3)}s)`);
2666
+ } else {
2667
+ error(`${cmd.name}: failed (${Math.round(result.duration / 1e3)}s)`);
2668
+ }
2669
+ }
2670
+ return results;
2671
+ }
2672
+ function formatProofOfWork(results) {
2673
+ const lines = ["", "---", "## Proof of Work", ""];
2674
+ lines.push("| Check | Status | Duration |");
2675
+ lines.push("|-------|--------|----------|");
2676
+ for (const r of results) {
2677
+ const status = r.success ? "Pass" : "Fail";
2678
+ const duration = `${Math.round(r.duration / 1e3)}s`;
2679
+ lines.push(`| ${r.name} | ${status} | ${duration} |`);
2680
+ }
2681
+ const failures = results.filter((r) => !r.success);
2682
+ if (failures.length > 0) {
2683
+ lines.push("");
2684
+ for (const f of failures) {
2685
+ const trimmed = f.output.trim().slice(-2e3);
2686
+ lines.push(`<details><summary>${f.name} output</summary>`);
2687
+ lines.push("");
2688
+ lines.push("```");
2689
+ lines.push(trimmed);
2690
+ lines.push("```");
2691
+ lines.push("");
2692
+ lines.push("</details>");
2693
+ }
2694
+ }
2695
+ return lines.join("\n");
2696
+ }
2697
+ function buildValidationRecoveryPrompt(issue, failures) {
2698
+ const failureSections = failures.map((f) => {
2699
+ const trimmed = f.output.trim().slice(-3e3);
2700
+ return `### ${f.name}
2701
+ Command: \`${f.name}\`
2702
+ Output:
2703
+ \`\`\`
2704
+ ${trimmed}
2705
+ \`\`\``;
2706
+ }).join("\n\n");
2707
+ return `You are continuing work on issue ${issue.id}: "${issue.title}".
2708
+
2709
+ Your implementation has code changes but the following validation checks failed. Fix the issues, commit your changes, and push.
2710
+
2711
+ ${failureSections}
2712
+
2713
+ IMPORTANT:
2714
+ - Do NOT create a new branch \u2014 you are already on the correct branch.
2715
+ - Fix ONLY the validation failures above.
2716
+ - Commit and push your fixes.
2717
+ - Do NOT create a PR \u2014 that will be handled separately.`;
2718
+ }
2719
+ function isProofOfWorkEnabled(config) {
2720
+ return !!(config?.enabled && config.commands.length > 0);
2721
+ }
2722
+
2723
+ // src/prompt.ts
2724
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2725
+ import { join as join5, resolve } from "path";
2726
+
2727
+ // src/context.ts
2728
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
2729
+ import { join as join4, relative } from "path";
2730
+ var QUALITY_SCRIPT_NAMES = /* @__PURE__ */ new Set([
2731
+ "lint",
2732
+ "typecheck",
2733
+ "check",
2734
+ "format",
2735
+ "test",
2736
+ "build",
2737
+ "ci"
2738
+ ]);
2739
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2740
+ "node_modules",
2741
+ "dist",
2742
+ ".git",
2743
+ ".worktrees",
2744
+ "coverage",
2745
+ ".next",
2746
+ ".nuxt",
2747
+ ".output",
2748
+ "build",
2749
+ ".cache",
2750
+ ".turbo",
2751
+ ".lisa"
2752
+ ]);
2753
+ var CONFIG_FILE_PATTERNS = [
2754
+ // ORM / migrations
2755
+ "prisma/schema.prisma",
2756
+ "drizzle.config.ts",
2757
+ "drizzle.config.js",
2758
+ "drizzle.config.mjs",
2759
+ "data-source.ts",
2760
+ "ormconfig.ts",
2761
+ "ormconfig.js",
2762
+ ".sequelizerc",
2763
+ "alembic.ini",
2764
+ "flyway.conf",
2765
+ "liquibase.properties",
2766
+ // API codegen
2767
+ "orval.config.ts",
2768
+ "orval.config.js",
2769
+ "orval.config.mjs",
2770
+ "kubb.config.ts",
2771
+ "kubb.config.js",
2772
+ "kubb.config.mjs",
2773
+ "codegen.ts",
2774
+ "codegen.js",
2775
+ "codegen.yml",
2776
+ "codegen.yaml",
2777
+ "openapi-ts.config.ts",
2778
+ "openapi-ts.config.js",
2779
+ "openapi-ts.config.mjs",
2780
+ "openapitools.json",
2781
+ "swagger-codegen-config.json",
2782
+ "buf.yaml",
2783
+ "buf.gen.yaml",
2784
+ // Linters / formatters
2785
+ "biome.json",
2786
+ "biome.jsonc",
2787
+ ".eslintrc",
2788
+ ".eslintrc.js",
2789
+ ".eslintrc.cjs",
2790
+ ".eslintrc.json",
2791
+ ".eslintrc.yml",
2792
+ "eslint.config.js",
2793
+ "eslint.config.mjs",
2794
+ "eslint.config.ts",
2795
+ ".prettierrc",
2796
+ ".prettierrc.json",
2797
+ ".prettierrc.js",
2798
+ "prettier.config.js",
2799
+ // Language / project markers
2800
+ "go.mod",
2801
+ "Cargo.toml",
2802
+ "Gemfile",
2803
+ "pubspec.yaml",
2804
+ "pyproject.toml",
2805
+ "requirements.txt",
2806
+ "composer.json",
2807
+ "Makefile",
2808
+ "justfile"
2809
+ ];
2810
+ function detectConfigFiles(cwd) {
2811
+ return CONFIG_FILE_PATTERNS.filter((pattern) => {
2812
+ const parts = pattern.split("/");
2813
+ if (parts.length === 2) {
2814
+ return existsSync2(join4(cwd, parts[0], parts[1]));
2815
+ }
2816
+ return existsSync2(join4(cwd, pattern));
2817
+ });
2818
+ }
2819
+ function analyzeProject(cwd) {
2820
+ return {
2821
+ qualityScripts: detectQualityScripts(cwd),
2822
+ testPattern: detectTestPattern(cwd),
2823
+ projectTree: generateProjectTree(cwd),
2824
+ environment: detectEnvironment(cwd),
2825
+ configFiles: detectConfigFiles(cwd)
2826
+ };
2827
+ }
2828
+ var CLI_DEPS = [
2829
+ "ink",
2830
+ "citty",
2831
+ "commander",
2832
+ "yargs",
2833
+ "oclif",
2834
+ "meow",
2835
+ "cleye",
2836
+ "cac",
2837
+ "minimist",
2838
+ "caporal"
2839
+ ];
2840
+ var MOBILE_DEPS = ["react-native", "expo", "@react-native", "@expo"];
2841
+ var WEB_DEPS = [
2842
+ "react-dom",
2843
+ "next",
2844
+ "vue",
2845
+ "nuxt",
2846
+ "@angular/core",
2847
+ "svelte",
2848
+ "gatsby",
2849
+ "remix",
2850
+ "astro",
2851
+ "@remix-run/react"
2852
+ ];
2853
+ var SERVER_DEPS = ["express", "fastify", "koa", "@hapi/hapi", "@nestjs/core", "hono", "elysia"];
2854
+ function detectEnvironment(cwd) {
2855
+ if (existsSync2(join4(cwd, "pubspec.yaml"))) return "mobile";
2856
+ if (existsSync2(join4(cwd, "Cargo.toml"))) return "cli";
2857
+ if (existsSync2(join4(cwd, "go.mod"))) return "server";
2858
+ const packageJsonPath = join4(cwd, "package.json");
2859
+ if (!existsSync2(packageJsonPath)) return "unknown";
2860
+ try {
2861
+ const pkg = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
2862
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2863
+ const depNames = Object.keys(deps);
2864
+ if (pkg.bin || CLI_DEPS.some((d) => depNames.includes(d))) return "cli";
2865
+ if (MOBILE_DEPS.some((d) => depNames.some((dep) => dep === d || dep.startsWith(`${d}/`))) || existsSync2(join4(cwd, "android")) || existsSync2(join4(cwd, "ios")))
2866
+ return "mobile";
2867
+ if (WEB_DEPS.some((d) => depNames.includes(d))) return "web";
2868
+ if (SERVER_DEPS.some((d) => depNames.includes(d))) return "server";
2869
+ return "library";
2870
+ } catch {
2871
+ return "unknown";
2872
+ }
2873
+ }
2874
+ function detectQualityScripts(cwd) {
2875
+ const packageJsonPath = join4(cwd, "package.json");
2876
+ if (!existsSync2(packageJsonPath)) return [];
2877
+ try {
2878
+ const content = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
2879
+ if (!content.scripts) return [];
2880
+ const scripts = [];
2881
+ for (const [name, command] of Object.entries(content.scripts)) {
2882
+ if (QUALITY_SCRIPT_NAMES.has(name)) {
2883
+ scripts.push({ name, command });
2884
+ }
2885
+ }
2886
+ return scripts;
2887
+ } catch {
2888
+ return [];
2889
+ }
2890
+ }
2891
+ function detectTestPattern(cwd) {
2892
+ const testFiles = findTestFiles(cwd, 3);
2893
+ if (testFiles.length === 0) return null;
2894
+ const location = inferTestLocation(cwd, testFiles);
2895
+ let style = "unknown";
2896
+ const mocking = /* @__PURE__ */ new Set();
2897
+ let example;
2898
+ for (const file of testFiles) {
2899
+ try {
2900
+ const content = readFileSync2(file, "utf-8");
2901
+ const fileStyle = inferTestStyle(content);
2902
+ if (style === "unknown") {
2903
+ style = fileStyle;
2904
+ } else if (style !== fileStyle && fileStyle !== "unknown") {
2905
+ style = "mixed";
2906
+ }
2907
+ for (const mock of inferMocking(content)) {
2908
+ mocking.add(mock);
2909
+ }
2910
+ if (!example) {
2911
+ example = extractTestExample(content, file, cwd);
2912
+ }
2913
+ } catch {
2914
+ }
2915
+ }
2916
+ return {
2917
+ location,
2918
+ style,
2919
+ mocking: [...mocking],
2920
+ example
2921
+ };
2922
+ }
2923
+ function generateProjectTree(cwd) {
2924
+ const lines = [];
2925
+ try {
2926
+ const entries = readdirSync(cwd);
2927
+ const filtered = entries.filter((e) => !IGNORED_DIRS.has(e) && !e.startsWith(".")).sort((a, b) => {
2928
+ const aIsDir = isDirectory(join4(cwd, a));
2929
+ const bIsDir = isDirectory(join4(cwd, b));
2930
+ if (aIsDir && !bIsDir) return -1;
2931
+ if (!aIsDir && bIsDir) return 1;
2932
+ return a.localeCompare(b);
2933
+ });
2934
+ for (const entry of filtered) {
2935
+ const fullPath = join4(cwd, entry);
2936
+ if (isDirectory(fullPath)) {
2937
+ lines.push(`${entry}/`);
2938
+ try {
2939
+ const children = readdirSync(fullPath);
2940
+ const filteredChildren = children.filter((c) => !IGNORED_DIRS.has(c) && !c.startsWith(".")).sort().slice(0, 15);
2941
+ for (const child of filteredChildren) {
2942
+ const childPath = join4(fullPath, child);
2943
+ const suffix = isDirectory(childPath) ? "/" : "";
2944
+ lines.push(` ${child}${suffix}`);
2945
+ }
2946
+ if (children.filter((c) => !IGNORED_DIRS.has(c) && !c.startsWith(".")).length > 15) {
2947
+ lines.push(" ...");
2948
+ }
2949
+ } catch {
2950
+ }
2951
+ } else {
2952
+ lines.push(entry);
2953
+ }
2954
+ }
2955
+ } catch {
2956
+ return "";
2957
+ }
2958
+ return lines.join("\n");
2959
+ }
2960
+ function formatProjectContext(ctx) {
2961
+ const sections = [];
2962
+ if (ctx.environment !== "unknown" && ctx.environment !== "library") {
2963
+ const labels = {
2964
+ cli: "CLI (Node.js)",
2965
+ mobile: "Mobile (React Native / Flutter / native)",
2966
+ web: "Web (browser)",
2967
+ server: "Server-side (Node.js)",
2968
+ library: "Library",
2969
+ unknown: ""
2970
+ };
2971
+ sections.push(`### Project Environment
2972
+
2973
+ **${labels[ctx.environment]}**`);
2974
+ }
2975
+ if (ctx.qualityScripts.length > 0) {
2976
+ const scriptLines = ctx.qualityScripts.map((s) => `- \`${s.name}\`: \`${s.command}\``).join("\n");
2977
+ sections.push(`### Quality Scripts
2978
+
2979
+ ${scriptLines}`);
2980
+ }
2981
+ if (ctx.configFiles.length > 0) {
2982
+ const fileLines = ctx.configFiles.map((f) => `- \`${f}\``).join("\n");
2983
+ sections.push(`### Config Files Detected
2984
+
2985
+ ${fileLines}`);
2986
+ }
2987
+ if (ctx.testPattern) {
2988
+ const tp = ctx.testPattern;
2989
+ const details = [
2990
+ `- Location: ${tp.location === "colocated" ? "tests are colocated next to source files" : tp.location === "separate" ? "tests are in a separate directory" : "unknown"}`,
2991
+ `- Style: ${tp.style === "describe-it" ? "describe/it blocks" : tp.style === "test" ? "top-level test() calls" : tp.style === "mixed" ? "mixed (describe/it and test())" : "unknown"}`
2992
+ ];
2993
+ if (tp.mocking.length > 0) {
2994
+ details.push(`- Mocking: ${tp.mocking.join(", ")}`);
2995
+ }
2996
+ let block = `### Test Patterns
2997
+
2998
+ ${details.join("\n")}`;
2999
+ if (tp.example) {
3000
+ block += `
3001
+
3002
+ **Reference test file:**
3003
+ \`\`\`typescript
3004
+ ${tp.example}
3005
+ \`\`\``;
3006
+ }
3007
+ sections.push(block);
3008
+ }
3009
+ if (ctx.projectTree) {
3010
+ sections.push(`### Project Structure
3011
+
3012
+ \`\`\`
3013
+ ${ctx.projectTree}
3014
+ \`\`\``);
3015
+ }
3016
+ if (sections.length === 0) return "";
3017
+ return `## Project Context
3018
+
3019
+ ${sections.join("\n\n")}`;
3020
+ }
3021
+ function findTestFiles(cwd, maxFiles) {
3022
+ const results = [];
3023
+ walkForTests(cwd, cwd, results, maxFiles, 0);
3024
+ return results;
3025
+ }
3026
+ function walkForTests(root, dir, results, maxFiles, depth) {
3027
+ if (results.length >= maxFiles || depth > 5) return;
3028
+ try {
3029
+ const entries = readdirSync(dir);
3030
+ for (const entry of entries) {
3031
+ if (results.length >= maxFiles) return;
3032
+ if (IGNORED_DIRS.has(entry)) continue;
3033
+ const fullPath = join4(dir, entry);
3034
+ if (isDirectory(fullPath)) {
3035
+ walkForTests(root, fullPath, results, maxFiles, depth + 1);
3036
+ } 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")) {
3037
+ results.push(fullPath);
3038
+ }
3039
+ }
3040
+ } catch {
3041
+ }
3042
+ }
3043
+ function inferTestLocation(cwd, testFiles) {
3044
+ let colocated = 0;
3045
+ let separate = 0;
3046
+ for (const file of testFiles) {
3047
+ const rel = relative(cwd, file);
3048
+ const parts = rel.split("/");
3049
+ if (parts.some(
3050
+ (p) => p === "__tests__" || p === "tests" || p === "test" || p === "spec" || p === "__specs__"
3051
+ )) {
3052
+ separate++;
3053
+ } else {
3054
+ colocated++;
3055
+ }
3056
+ }
3057
+ if (colocated > 0 && separate === 0) return "colocated";
3058
+ if (separate > 0 && colocated === 0) return "separate";
3059
+ if (colocated > 0 && separate > 0) return "colocated";
3060
+ return "unknown";
3061
+ }
3062
+ function inferTestStyle(content) {
3063
+ const hasDescribe = /\bdescribe\s*\(/.test(content);
3064
+ const hasIt = /\bit\s*\(/.test(content);
3065
+ const hasTopLevelTest = /\btest\s*\(/.test(content);
3066
+ if (hasDescribe && hasIt) return "describe-it";
3067
+ if (hasTopLevelTest && !hasDescribe) return "test";
3068
+ if (hasDescribe || hasIt) return "describe-it";
3069
+ return "unknown";
3070
+ }
3071
+ function inferMocking(content) {
3072
+ const mocks = [];
3073
+ if (/\bvi\.(mock|fn|spyOn)\b/.test(content)) mocks.push("vi.mock/vi.fn");
3074
+ if (/\bjest\.(mock|fn|spyOn)\b/.test(content)) mocks.push("jest.mock/jest.fn");
3075
+ if (/\bfixture/.test(content)) mocks.push("fixtures");
3076
+ return mocks;
3077
+ }
3078
+ function extractTestExample(content, filePath, cwd) {
3079
+ const lines = content.split("\n");
3080
+ const snippet = lines.slice(0, 30).join("\n").trim();
3081
+ if (snippet.length < 10) return void 0;
3082
+ const relPath = relative(cwd, filePath);
3083
+ return `// ${relPath}
3084
+ ${snippet}`;
3085
+ }
3086
+ function isDirectory(path) {
3087
+ try {
3088
+ return statSync(path).isDirectory();
3089
+ } catch {
3090
+ return false;
3091
+ }
3092
+ }
3093
+
3094
+ // src/git/bitbucket.ts
3095
+ import { execa } from "execa";
3096
+ var API_URL3 = "https://api.bitbucket.org/2.0";
3097
+ var REQUEST_TIMEOUT_MS2 = 3e4;
3098
+ var PROVIDER_DISPLAY_NAMES = {
3099
+ claude: "Claude Code",
3100
+ gemini: "Gemini CLI",
3101
+ opencode: "OpenCode",
3102
+ copilot: "GitHub Copilot CLI",
3103
+ cursor: "Cursor Agent",
3104
+ goose: "Goose",
3105
+ aider: "Aider",
3106
+ codex: "OpenAI Codex"
3107
+ };
3108
+ function formatProviderName(providerUsed) {
3109
+ const providerKey = providerUsed.split("/")[0] ?? providerUsed;
3110
+ return PROVIDER_DISPLAY_NAMES[providerKey] ?? providerKey;
3111
+ }
3112
+ function getAuthHeader() {
3113
+ const token = process.env.BITBUCKET_TOKEN;
3114
+ if (!token) throw new Error("BITBUCKET_TOKEN is not set");
3115
+ return `Bearer ${token}`;
3116
+ }
3117
+ async function appendPrAttribution2(prUrl, providerUsed) {
3118
+ try {
3119
+ const match = prUrl.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)/);
3120
+ if (!match) return;
3121
+ const [, workspace, repoSlug, prId] = match;
3122
+ if (!process.env.BITBUCKET_TOKEN) return;
3123
+ const authHeader = getAuthHeader();
3124
+ const getRes = await fetch(
3125
+ `${API_URL3}/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`,
3126
+ {
3127
+ headers: { Authorization: authHeader },
3128
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
3129
+ }
3130
+ );
3131
+ if (!getRes.ok) return;
3132
+ const prData = await getRes.json();
3133
+ const currentDescription = prData.description ?? "";
3134
+ const currentTitle = prData.title ?? "";
3135
+ const providerName = formatProviderName(providerUsed);
3136
+ const attribution = `
3137
+
3138
+ ---
3139
+ \u{1F916} Resolved by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerName}**`;
3140
+ const newDescription = stripProviderAttribution(currentDescription) + attribution;
3141
+ await fetch(`${API_URL3}/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, {
3142
+ method: "PUT",
3143
+ headers: {
3144
+ Authorization: authHeader,
3145
+ "Content-Type": "application/json"
3146
+ },
3147
+ body: JSON.stringify({ description: newDescription, title: currentTitle }),
3148
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
3149
+ });
3150
+ } catch {
3151
+ }
3152
+ }
3153
+ async function appendPrBody2(prUrl, content) {
3154
+ try {
3155
+ const match = prUrl.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)/);
3156
+ if (!match) return;
3157
+ const [, workspace, repoSlug, prId] = match;
3158
+ if (!process.env.BITBUCKET_TOKEN) return;
3159
+ const authHeader = getAuthHeader();
3160
+ const getRes = await fetch(
3161
+ `${API_URL3}/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`,
3162
+ {
3163
+ headers: { Authorization: authHeader },
3164
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
3165
+ }
3166
+ );
3167
+ if (!getRes.ok) return;
3168
+ const prData = await getRes.json();
3169
+ const newDescription = (prData.description ?? "") + content;
3170
+ await fetch(`${API_URL3}/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, {
3171
+ method: "PUT",
3172
+ headers: {
3173
+ Authorization: authHeader,
3174
+ "Content-Type": "application/json"
3175
+ },
3176
+ body: JSON.stringify({ description: newDescription, title: prData.title ?? "" }),
3177
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
3178
+ });
3179
+ } catch {
3180
+ }
3181
+ }
3182
+
3183
+ // src/git/gitlab.ts
3184
+ import { execa as execa2 } from "execa";
3185
+ var REQUEST_TIMEOUT_MS3 = 3e4;
3186
+ var PROVIDER_DISPLAY_NAMES2 = {
3187
+ claude: "Claude Code",
3188
+ gemini: "Gemini CLI",
3189
+ opencode: "OpenCode",
3190
+ copilot: "GitHub Copilot CLI",
3191
+ cursor: "Cursor Agent",
3192
+ goose: "Goose",
3193
+ aider: "Aider",
3194
+ codex: "OpenAI Codex"
3195
+ };
3196
+ function formatProviderName2(providerUsed) {
3197
+ const providerKey = providerUsed.split("/")[0] ?? providerUsed;
3198
+ return PROVIDER_DISPLAY_NAMES2[providerKey] ?? providerKey;
3199
+ }
3200
+ function buildApiBase(host) {
3201
+ return `https://${host}/api/v4`;
3202
+ }
3203
+ async function appendMrAttribution(mrUrl, providerUsed) {
3204
+ try {
3205
+ const match = mrUrl.match(/https?:\/\/([^/]+)\/(.+?)\/-\/merge_requests\/(\d+)/);
3206
+ if (!match) return;
3207
+ const [, host, projectPath, iidStr] = match;
3208
+ const token = process.env.GITLAB_TOKEN;
3209
+ if (!token) return;
3210
+ const apiBase = buildApiBase(host ?? "gitlab.com");
3211
+ const encodedPath = encodeURIComponent(projectPath ?? "");
3212
+ const iid = iidStr;
3213
+ const getRes = await fetch(`${apiBase}/projects/${encodedPath}/merge_requests/${iid}`, {
3214
+ headers: { "PRIVATE-TOKEN": token },
3215
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
3216
+ });
3217
+ if (!getRes.ok) return;
3218
+ const mrData = await getRes.json();
3219
+ const currentDescription = mrData.description ?? "";
3220
+ const providerName = formatProviderName2(providerUsed);
3221
+ const attribution = `
3222
+
3223
+ ---
3224
+ \u{1F916} Resolved by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerName}**`;
3225
+ const newDescription = stripProviderAttribution(currentDescription) + attribution;
3226
+ await fetch(`${apiBase}/projects/${encodedPath}/merge_requests/${iid}`, {
3227
+ method: "PUT",
3228
+ headers: {
3229
+ "PRIVATE-TOKEN": token,
3230
+ "Content-Type": "application/json"
3231
+ },
3232
+ body: JSON.stringify({ description: newDescription }),
3233
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
3234
+ });
3235
+ } catch {
3236
+ }
3237
+ }
3238
+ async function appendMrBody(mrUrl, content) {
3239
+ try {
3240
+ const match = mrUrl.match(/https?:\/\/([^/]+)\/(.+?)\/-\/merge_requests\/(\d+)/);
3241
+ if (!match) return;
3242
+ const [, host, projectPath, iid] = match;
3243
+ const token = process.env.GITLAB_TOKEN;
3244
+ if (!token) return;
3245
+ const apiBase = buildApiBase(host ?? "gitlab.com");
3246
+ const encodedPath = encodeURIComponent(projectPath ?? "");
3247
+ const getRes = await fetch(`${apiBase}/projects/${encodedPath}/merge_requests/${iid}`, {
3248
+ headers: { "PRIVATE-TOKEN": token },
3249
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
3250
+ });
3251
+ if (!getRes.ok) return;
3252
+ const mrData = await getRes.json();
3253
+ const newDescription = (mrData.description ?? "") + content;
3254
+ await fetch(`${apiBase}/projects/${encodedPath}/merge_requests/${iid}`, {
3255
+ method: "PUT",
3256
+ headers: {
3257
+ "PRIVATE-TOKEN": token,
3258
+ "Content-Type": "application/json"
3259
+ },
3260
+ body: JSON.stringify({ description: newDescription }),
3261
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
3262
+ });
3263
+ } catch {
3264
+ }
3265
+ }
3266
+
3267
+ // src/git/platform.ts
3268
+ async function appendPlatformAttribution(prUrl, providerUsed, platform2) {
3269
+ if (platform2 === "gitlab") {
3270
+ await appendMrAttribution(prUrl, providerUsed);
3271
+ } else if (platform2 === "bitbucket") {
3272
+ await appendPrAttribution2(prUrl, providerUsed);
3273
+ } else {
3274
+ await appendPrAttribution(prUrl, providerUsed);
3275
+ }
3276
+ }
3277
+ async function appendPlatformProofOfWork(prUrl, results, platform2) {
3278
+ const section = formatProofOfWork(results);
3279
+ try {
3280
+ if (platform2 === "gitlab") {
3281
+ await appendMrBody(prUrl, section);
3282
+ } else if (platform2 === "bitbucket") {
3283
+ await appendPrBody2(prUrl, section);
3284
+ } else {
3285
+ await appendPrBody(prUrl, section);
3286
+ }
3287
+ } catch {
3288
+ }
3289
+ }
3290
+ function buildPrCreateInstruction(platform2, targetBranch) {
3291
+ const base = targetBranch ? ` --base ${targetBranch}` : "";
3292
+ if (platform2 === "gitlab") {
3293
+ const branchArg = targetBranch ? ` --target-branch ${targetBranch}` : "";
3294
+ return `**Create MR**: Create a merge request on GitLab:
3295
+ **Option A \u2014 glab CLI (if available):**
3296
+ \`glab mr create --fill${branchArg} --yes\`
3297
+
3298
+ **Option B \u2014 curl with GITLAB_TOKEN:**
3299
+ \`\`\`bash
3300
+ GITLAB_PROJECT=$(git remote get-url origin | sed 's/.*gitlab\\.com[:/]//;s/\\.git$//' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=''))")
3301
+ BRANCH=$(git branch --show-current)
3302
+ curl --request POST \\
3303
+ --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
3304
+ --header "Content-Type: application/json" \\
3305
+ --data "{\\"source_branch\\":\\"$BRANCH\\",\\"target_branch\\":\\"${targetBranch ?? "main"}\\",\\"title\\":\\"<conventional-commit-title>\\",\\"description\\":\\"<markdown-summary>\\"}" \\
3306
+ "https://gitlab.com/api/v4/projects/$GITLAB_PROJECT/merge_requests"
3307
+ \`\`\`
3308
+ Capture the MR URL (\`web_url\`) from the output.`;
3309
+ }
3310
+ if (platform2 === "bitbucket") {
3311
+ const dest = targetBranch ?? "main";
3312
+ return `**Create PR**: Create a pull request on Bitbucket:
3313
+ \`\`\`bash
3314
+ WORKSPACE=$(git remote get-url origin | sed 's/.*bitbucket\\.org[:/]//;s/\\/.*$//')
3315
+ REPO=$(git remote get-url origin | sed 's/.*bitbucket\\.org[:/][^/]*\\///;s/\\.git$//')
3316
+ BRANCH=$(git branch --show-current)
3317
+ curl -X POST \\
3318
+ -H "Authorization: Basic $(printf '%s:%s' "$BITBUCKET_USERNAME" "$BITBUCKET_TOKEN" | base64)" \\
3319
+ -H "Content-Type: application/json" \\
3320
+ "https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO/pullrequests" \\
3321
+ --data "{\\"title\\":\\"<conventional-commit-title>\\",\\"description\\":\\"<markdown-summary>\\",\\"source\\":{\\"branch\\":{\\"name\\":\\"$BRANCH\\"}},\\"destination\\":{\\"branch\\":{\\"name\\":\\"${dest}\\"}}}"
3322
+ \`\`\`
3323
+ Capture the PR URL (\`links.html.href\`) from the output.`;
3324
+ }
3325
+ return `**Create PR**: Create a pull request using the GitHub CLI:
3326
+ \`gh pr create --title "<conventional-commit-title>" --body "<markdown-summary>"${base}\`
3327
+ Capture the PR URL from the output.`;
3328
+ }
3329
+
3330
+ // src/prompt.ts
3331
+ function detectPackageManager(cwd) {
3332
+ if (existsSync3(join5(cwd, "bun.lockb")) || existsSync3(join5(cwd, "bun.lock"))) return "bun";
3333
+ if (existsSync3(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
3334
+ if (existsSync3(join5(cwd, "yarn.lock"))) return "yarn";
3335
+ return "npm";
3336
+ }
3337
+ function detectTestRunner(cwd) {
3338
+ const packageJsonPath = join5(cwd, "package.json");
3339
+ if (!existsSync3(packageJsonPath)) return null;
3340
+ try {
3341
+ const content = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
3342
+ const deps = { ...content.dependencies, ...content.devDependencies };
3343
+ if ("vitest" in deps) return "vitest";
3344
+ if ("jest" in deps) return "jest";
3345
+ return null;
3346
+ } catch {
3347
+ return null;
3348
+ }
3349
+ }
3350
+ function extractReadmeHeadings(cwd) {
3351
+ const readmePath = join5(cwd, "README.md");
3352
+ if (!existsSync3(readmePath)) return [];
3353
+ try {
3354
+ const content = readFileSync3(readmePath, "utf-8");
3355
+ return content.split("\n").filter((line) => /^#{1,6}\s/.test(line)).map((line) => line.trim());
3356
+ } catch {
3357
+ return [];
3358
+ }
3359
+ }
3360
+ function buildPrompt(opts) {
3361
+ const {
3362
+ issue,
3363
+ variant,
3364
+ testRunner,
3365
+ pm,
3366
+ baseBranch,
3367
+ projectContext,
3368
+ cwd,
3369
+ platform: platform2 = "cli",
3370
+ repoContextMd,
3371
+ config,
3372
+ step,
3373
+ previousResults = [],
3374
+ isLastStep = false
3375
+ } = opts;
3376
+ let manifestPath = opts.manifestPath;
3377
+ if (!manifestPath && variant === "branch" && config) {
3378
+ manifestPath = getManifestPath(resolve(config.workspace));
3379
+ }
3380
+ const testBlock = buildTestInstructions(testRunner ?? null, pm);
3381
+ const headings = cwd ? extractReadmeHeadings(cwd) : [];
3382
+ const readmeBlock = buildReadmeInstructions(headings);
3383
+ const hookBlock = buildPreCommitHookInstructions();
3384
+ const contextBlock = projectContext ? formatProjectContext(projectContext) : "";
3385
+ const depBlock = issue.dependency ? buildDependencyContext(issue.dependency) : "";
3386
+ const specWarningBlock = buildSpecWarningBlock(issue.specWarning);
3387
+ const contextMdBlock = buildContextMdBlock(repoContextMd ?? null);
3388
+ const prBase = issue.dependency ? issue.dependency.branch : baseBranch;
3389
+ const prCreateBlock = buildPrCreateInstruction(platform2, prBase);
3390
+ const manifestLocation = manifestPath ? `\`${manifestPath}\`` : "`.lisa/manifests/default.json` in the **current directory**";
3391
+ let preamble = "You are an autonomous implementation agent. You MUST complete ALL steps below \u2014 implementation, commit, push, PR creation, and manifest file \u2014 before finishing.\nDo NOT stop after implementing code. The task is NOT complete until the manifest file is written with the PR URL.\nDo NOT use interactive skills, ask clarifying questions, or wait for user input. You are running unattended. No human will see your output or respond to questions.";
3392
+ if (variant === "scoped") {
3393
+ } else {
3394
+ preamble += " If the issue is too ambiguous to implement, you MUST STOP and provide a clear explanation.";
3395
+ }
3396
+ let workContext = "";
3397
+ if (variant === "worktree") {
3398
+ workContext = `
3399
+ You are already inside the correct repository worktree on the correct branch.
3400
+ Do NOT create a new branch \u2014 just work on the current one.
3401
+ ${cwd ? `
3402
+ **Working directory:** \`${cwd}\`
3403
+ All file paths are relative to this directory. Use this as the base for any absolute paths.
3404
+ ` : ""}`;
3405
+ } else if (variant === "native-worktree" || variant === "scoped") {
3406
+ workContext = `
3407
+ You are working inside a git worktree that was automatically created for this task.
3408
+ Work on the current branch \u2014 it was created for you.
3409
+ ${cwd ? `
3410
+ **Working directory:** \`${cwd}\`
3411
+ All file paths are relative to this directory. Use this as the base for any absolute paths.
3412
+ ` : ""}`;
3413
+ }
3414
+ let scopeSection = "";
3415
+ if (variant === "scoped" && step) {
3416
+ const previousBlock = previousResults.length > 0 ? `
3417
+ ## Previous Steps
3418
+
3419
+ The following repos have already been implemented as part of this issue:
3420
+
3421
+ ${previousResults.map((r) => `- **${r.repoPath}**: branch \`${r.branch}\`${r.prUrl ? ` \u2014 PR: ${r.prUrl}` : ""}`).join("\n")}
3422
+
3423
+ Use this context if the current step depends on changes from previous steps.
3424
+ ` : "";
3425
+ scopeSection = `
3426
+ ## Your Scope
3427
+
3428
+ You are responsible for **this specific part** of the issue:
3429
+
3430
+ > ${step.scope}
3431
+
3432
+ Focus only on this scope. Do NOT implement changes outside this scope.
3433
+ ${previousBlock}`;
3434
+ }
3435
+ let instructions = "";
3436
+ if (variant === "branch" && config) {
3437
+ const workspace = resolve(config.workspace);
3438
+ const repoEntries = config.repos.map(
3439
+ (r) => ` - If it says "Repo: ${r.name}" or title starts with "${r.match}" \u2192 \`${resolve(workspace, r.path)}\` (base branch: \`${r.base_branch}\`)`
3440
+ ).join("\n");
3441
+ const baseBranchInstruction = issue.dependency ? `From \`${issue.dependency.branch}\` (dependency branch)` : config.repos.length > 0 ? "From the repo's base branch (listed above)" : `From \`${baseBranch}\``;
3442
+ instructions = `## Instructions
3443
+
3444
+ 1. **Identify the repo**: Look at the issue description for relevant files or repo references.
3445
+ ${repoEntries}
3446
+ - If it references multiple repos, pick the PRIMARY one (the one with the most files listed).
3447
+
3448
+ 2. **Create a branch**: ${baseBranchInstruction}, create a branch with an **English** slug:
3449
+ \`feat/${issue.id.toLowerCase()}-short-english-description\`
3450
+ The description MUST be in English \u2014 translate or summarize the issue title if it's in another language.
3451
+ Example: "Implementar rate limiting na API" \u2192 \`feat/${issue.id.toLowerCase()}-add-rate-limiting-to-api\`
3452
+
3453
+ 3. **Implement**: Follow the issue description exactly:
3454
+ - Read all relevant files listed in the description first (if present)
3455
+ - Follow the implementation instructions exactly
3456
+ - Verify each acceptance criteria (if present)
3457
+ - Respect any stack or technical constraints (if present)
3458
+ ${testBlock}${hookBlock}
3459
+ 4. **Validate**: Run the project's linter/typecheck/tests if available:
3460
+ - Check \`package.json\` (or equivalent) for lint, typecheck, check, or test scripts.
3461
+ - Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`).
3462
+ - Fix any errors before proceeding.
3463
+ ${readmeBlock}
3464
+ **CRITICAL \u2014 Do NOT stop here. The following steps (commit, push, PR, manifest) are MANDATORY. Skipping them means the task has FAILED.**
3465
+
3466
+ 5. **Commit & Push**: Make atomic commits with conventional commit messages.
3467
+ Push the branch to origin:
3468
+ \`git push -u origin <branch-name>\`
3469
+ If the push fails due to a pre-push hook, read the error, fix the root cause, amend the commit, and retry. Do NOT use \`--no-verify\`.
3470
+ **IMPORTANT \u2014 Language rules:**
3471
+ - All commit messages MUST be in English.
3472
+ - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
3473
+
3474
+ 6. ${prCreateBlock}
3475
+
3476
+ 7. **Update tracker**: Call the lisa CLI to mark the issue as done:
3477
+ \`lisa issue done ${issue.id} --pr-url <pr-url>\`
3478
+ Wait 1 second before calling this command.
3479
+
3480
+ 8. **Write manifest**: Before finishing, create \`${manifestPath ?? getManifestPath(resolve(config.workspace))}\` with JSON:
3481
+ \`\`\`json
3482
+ {"repoPath": "<absolute path to this repo>", "branch": "<branch name>", "prUrl": "<pull request URL>"}
3483
+ \`\`\`
3484
+ Do NOT commit this file.`;
3485
+ } else if (variant === "scoped") {
3486
+ const trackerStep = isLastStep ? `
3487
+ 6. **Update tracker**: Call \`lisa issue done ${issue.id} --pr-url <pr-url>\` (wait 1 second before calling).
3488
+ ` : `
3489
+ 6. **Skip tracker update**: This is not the last step. The caller handles the tracker update after all steps complete.
3490
+ `;
3491
+ instructions = `## Instructions
3492
+
3493
+ 1. **Implement**: Follow the scope above. Read the full issue description for context, but only implement what is described in "Your Scope":
3494
+ - Read all relevant files first
3495
+ - Follow the implementation instructions exactly
3496
+ - Verify each acceptance criteria relevant to your scope
3497
+ ${testBlock}${hookBlock}
3498
+ 2. **Validate**: Run the project's linter/typecheck/tests if available:
3499
+ - Check \`package.json\` (or equivalent) for lint, typecheck, check, or test scripts.
3500
+ - Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`).
3501
+ - Fix any errors before proceeding.
3502
+ ${readmeBlock}
3503
+ **CRITICAL \u2014 Do NOT stop here. The following steps (commit, push, PR, manifest) are MANDATORY. Skipping them means the task has FAILED.**
3504
+
3505
+ 3. **Commit**: Make atomic commits with conventional commit messages.
3506
+ **Branch name must be in English.** If the current branch name contains non-English words,
3507
+ rename it: \`git branch -m <current-name> feat/${issue.id.toLowerCase()}-short-english-slug\`
3508
+ **IMPORTANT \u2014 Language rules:**
3509
+ - All commit messages MUST be in English.
3510
+ - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
3511
+
3512
+ 4. **Push**: Push the branch to origin:
3513
+ \`git push -u origin <branch-name>\`
3514
+ If the push fails due to a pre-push hook, read the error, fix the root cause, amend the commit, and retry. Do NOT use \`--no-verify\`.
3515
+
3516
+ 5. ${prCreateBlock}
3517
+ ${trackerStep}
3518
+ 7. **Write manifest**: Create ${manifestLocation} with JSON:
3519
+ \`\`\`json
3520
+ {"branch": "<final English branch name>", "prUrl": "<pull request URL>"}
3521
+ \`\`\`
3522
+ Do NOT commit this file.`;
3523
+ } else {
3524
+ const branchRenameInstruction = variant === "worktree" ? ` rename it before committing using the single-argument form:
3525
+ \`git branch -m feat/${issue.id.toLowerCase()}-short-english-slug\`` : ` rename it: \`git branch -m <current-name> feat/${issue.id.toLowerCase()}-short-english-slug\``;
3526
+ instructions = `## Instructions
3527
+
3528
+ 1. **Implement**: Follow the issue description exactly:
3529
+ - Read all relevant files listed in the description first (if present)
3530
+ - Follow the implementation instructions exactly
3531
+ - Verify each acceptance criteria (if present)
3532
+ - Respect any stack or technical constraints (if present)
3533
+ ${testBlock}${hookBlock}
3534
+ 2. **Validate**: Run the project's linter/typecheck/tests if available:
3535
+ - Check \`package.json\` (or equivalent) for lint, typecheck, check, or test scripts.
3536
+ - Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`).
3537
+ - Fix any errors before proceeding.
3538
+ ${readmeBlock}
3539
+ **CRITICAL \u2014 Do NOT stop here. The following steps (commit, push, PR, manifest) are MANDATORY. Skipping them means the task has FAILED.**
3540
+
3541
+ 3. **Commit**: Make atomic commits with conventional commit messages.
3542
+ **Branch name must be in English.** If the current branch name contains non-English words,
3543
+ ${branchRenameInstruction}
3544
+ **IMPORTANT \u2014 Language rules:**
3545
+ - All commit messages MUST be in English.
3546
+ - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
3547
+
3548
+ 4. **Push**: Push the branch to origin:
3549
+ \`git push -u origin <branch-name>\`
3550
+ If the push fails due to a pre-push hook, read the error, fix the root cause, amend the commit, and retry. Do NOT use \`--no-verify\`.
3551
+
3552
+ 5. ${prCreateBlock}
3553
+
3554
+ 6. **Update tracker**: Call the lisa CLI to mark the issue as done:
3555
+ \`lisa issue done ${issue.id} --pr-url <pr-url>\`
3556
+ Wait 1 second before calling this command.
3557
+
3558
+ 7. **Write manifest**: Create ${manifestLocation} with JSON:
3559
+ \`\`\`json
3560
+ {"branch": "<final English branch name>", "prUrl": "<pull request URL>"}
3561
+ \`\`\`
3562
+ Do NOT commit this file.`;
3563
+ }
3564
+ let rulesSection;
3565
+ if (variant === "branch") {
3566
+ rulesSection = buildRulesSection(
3567
+ projectContext?.environment,
3568
+ "issue",
3569
+ "- Do NOT modify files outside the target repo.\n"
3570
+ );
3571
+ } else if (variant === "scoped") {
3572
+ rulesSection = buildRulesSection(projectContext?.environment, "scope");
3573
+ } else {
3574
+ rulesSection = buildRulesSection(projectContext?.environment);
3575
+ }
3576
+ return `${preamble}
3577
+ ${workContext}
3578
+ ## Issue
3579
+
3580
+ - **ID:** ${issue.id}
3581
+ - **Title:** ${issue.title}
3582
+ - **URL:** ${issue.url}
3583
+
3584
+ ### Description
3585
+
3586
+ ${issue.description}
3587
+ ${specWarningBlock}${contextBlock ? `
3588
+ ${contextBlock}
3589
+ ` : ""}${contextMdBlock}${depBlock ? `
3590
+ ${depBlock}
3591
+ ` : ""}${scopeSection}
3592
+ ${instructions}
3593
+
3594
+ ${rulesSection}
3595
+
3596
+ ## Completion Checklist
3597
+
3598
+ Before finishing, verify ALL of the following are true:
3599
+ - [ ] Code changes are committed (no uncommitted changes)
3600
+ - [ ] Branch is pushed to origin
3601
+ - [ ] Pull request is created and URL is captured
3602
+ - [ ] Manifest file is written with \`prUrl\` field
3603
+ If ANY item is unchecked, go back and complete it. Do NOT finish with incomplete steps.`;
3604
+ }
3605
+ function buildImplementPrompt(issue, config, testRunner, pm, projectContext, cwd, manifestPath, repoContextMd) {
3606
+ const workspace = resolve(config.workspace);
3607
+ const resolvedManifestPath = manifestPath ?? getManifestPath(workspace);
3608
+ if (config.workflow === "worktree") {
3609
+ return buildPrompt({
3610
+ issue,
3611
+ variant: "worktree",
3612
+ testRunner,
3613
+ pm,
3614
+ baseBranch: config.base_branch,
3615
+ projectContext,
3616
+ manifestPath: resolvedManifestPath,
3617
+ cwd,
3618
+ platform: config.platform,
3619
+ repoContextMd
3620
+ });
3621
+ }
3622
+ return buildPrompt({
3623
+ issue,
3624
+ variant: "branch",
3625
+ testRunner,
3626
+ pm,
3627
+ baseBranch: config.base_branch,
3628
+ projectContext,
3629
+ manifestPath: resolvedManifestPath,
3630
+ cwd,
3631
+ platform: config.platform,
3632
+ repoContextMd,
3633
+ config
3634
+ });
3635
+ }
3636
+ function buildNativeWorktreePrompt(issue, repoPath, testRunner, pm, baseBranch, projectContext, manifestPath, platform2 = "cli", repoContextMd) {
3637
+ return buildPrompt({
3638
+ issue,
3639
+ variant: "native-worktree",
3640
+ testRunner,
3641
+ pm,
3642
+ baseBranch,
3643
+ projectContext,
3644
+ manifestPath,
3645
+ cwd: repoPath,
3646
+ platform: platform2,
3647
+ repoContextMd
3648
+ });
3649
+ }
3650
+ function buildScopedImplementPrompt(issue, step, previousResults, testRunner, pm, isLastStep = false, baseBranch, projectContext, manifestPath, cwd, platform2 = "cli", repoContextMd) {
3651
+ return buildPrompt({
3652
+ issue,
3653
+ variant: "scoped",
3654
+ testRunner,
3655
+ pm,
3656
+ baseBranch,
3657
+ projectContext,
3658
+ manifestPath,
3659
+ cwd,
3660
+ platform: platform2,
3661
+ repoContextMd,
3662
+ step,
3663
+ previousResults,
3664
+ isLastStep
3665
+ });
3666
+ }
3667
+ function buildTestInstructions(testRunner, pm = "npm") {
3668
+ if (!testRunner) return "";
3669
+ const testCmd = pm === "bun" ? "bun run test" : `${pm} run test`;
3670
+ return `
3671
+ **MANDATORY \u2014 Unit Tests:**
3672
+ This project uses **${testRunner}** as its test runner.
3673
+ - You MUST write unit tests (\`*.test.ts\`) for every new file or module you create.
3674
+ - Tests should cover the main functionality, edge cases, and error scenarios.
3675
+ - Run \`${testCmd}\` and ensure ALL tests pass before committing.
3676
+ - Do NOT skip writing tests \u2014 the PR will be blocked if tests are missing or failing.
3677
+ `;
3678
+ }
3679
+ function buildSpecWarningBlock(warning) {
3680
+ if (!warning) return "";
3681
+ return `
3682
+ > **Warning \u2014 incomplete spec:** ${warning}
3683
+ > Proceed using reasonable assumptions based on the title and description.
3684
+ > If the issue is genuinely too ambiguous to implement, STOP and explain what is missing.
3685
+ `;
3686
+ }
3687
+ function buildRulesSection(env, variant = "issue", extraRules = "") {
3688
+ const envRule = buildEnvironmentDependencyRule(env);
3689
+ const scopeRule = variant === "scope" ? "- One scope only. Do not pick up additional work outside your scope." : "- One issue only. Do not pick up additional issues.";
3690
+ return `## Rules
3691
+
3692
+ - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
3693
+ - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
3694
+ - Do NOT install new dependencies unless the issue explicitly requires it.
3695
+ - Do NOT use documentation lookup MCP tools (e.g., Context7, codesearch, Exa) \u2014 they have free-tier rate limits that will block your execution. Read files directly from the repository. Web search is allowed only when strictly necessary (e.g., looking up an external API format not available in the codebase).
3696
+ ${envRule}${extraRules}- If you get stuck or the issue is unclear, STOP and explain why.
3697
+ ${scopeRule}
3698
+ - If the repo has a CLAUDE.md, read it first and follow its conventions.`;
3699
+ }
3700
+ function buildEnvironmentDependencyRule(env) {
3701
+ if (env === "cli") {
3702
+ return "- **Environment**: This is a CLI (Node.js) project. Do NOT install browser/DOM packages (`jsdom`, `happy-dom`, `@testing-library/dom`, `@testing-library/react`). All dependencies and tests must be Node.js-compatible.\n";
3703
+ }
3704
+ if (env === "mobile") {
3705
+ return "- **Environment**: This is a mobile project (React Native/Flutter). Do NOT install browser/DOM packages or web-only libraries. Use only packages compatible with the mobile runtime.\n";
3706
+ }
3707
+ if (env === "server") {
3708
+ return "- **Environment**: This is a server-side (Node.js) project. Do NOT install browser/DOM packages. Use only Node.js-compatible packages.\n";
3709
+ }
3710
+ return "";
3711
+ }
3712
+ function buildPreCommitHookInstructions() {
3713
+ return `
3714
+ **Pre-commit hooks:**
3715
+ If \`git commit\` fails due to a pre-commit hook (e.g. husky), read the error output carefully and fix the underlying issue:
3716
+ - Linter/formatter failures \u2192 run the project's lint/format commands, then re-stage and retry the commit.
3717
+ - Code generation errors (e.g. stale Prisma client) \u2192 run the required generation command (e.g. \`npx prisma generate\`), then re-stage and retry.
3718
+ - Type errors \u2192 fix the type issues in the source files, then re-stage and retry.
3719
+ Do NOT skip or bypass hooks (no \`--no-verify\`). Fix the root cause and retry.
3720
+ `;
3721
+ }
3722
+ function buildReadmeInstructions(headings) {
3723
+ if (headings.length === 0) return "";
3724
+ const headingList = headings.map((h) => ` - ${h}`).join("\n");
3725
+ return `
3726
+ **README.md Validation:**
3727
+ The current README.md documents these sections:
3728
+ ${headingList}
3729
+
3730
+ Review your implementation diff against these sections. Update README.md if your changes affect any documented behavior:
3731
+ - CLI commands, flags, or usage examples
3732
+ - Providers, sources, or integrations
3733
+ - Configuration fields or schema
3734
+ - Pipeline stages, workflow modes, or architecture
3735
+ - Environment variables
3736
+
3737
+ Do NOT update README.md for internal refactors, bug fixes, test-only changes, logging, or dependency updates.
3738
+
3739
+ If an update is needed, modify only the affected sections. Keep the existing style and structure. Include the README change in the same commit.
3740
+ `;
3741
+ }
3742
+ function buildContextMdBlock(content) {
3743
+ if (!content?.trim()) return "";
3744
+ return `
3745
+ ## Project Conventions
3746
+
3747
+ ${content.trim()}
3748
+ `;
3749
+ }
3750
+ function buildDependencyContext(dep) {
3751
+ const fileList = dep.changedFiles.length > 0 ? dep.changedFiles.map((f) => ` - \`${f}\``).join("\n") : " (no files detected)";
3752
+ return `## Dependency Context
3753
+
3754
+ **This branch was created from the branch of issue ${dep.issueId}** (\`${dep.branch}\`), which has an open PR: ${dep.prUrl}
3755
+
3756
+ The following files were changed by the dependency and are already available in your working tree:
3757
+ ${fileList}
3758
+
3759
+ **Important:**
3760
+ - Do NOT reimplement or modify code that was introduced by ${dep.issueId} \u2014 it already exists in your branch.
3761
+ - Your PR must target \`${dep.branch}\` as its base branch (not \`main\`), so the diff only shows YOUR changes.
3762
+ - When ${dep.issueId}'s PR is merged, your PR will be automatically re-targeted to \`main\`.
3763
+ `;
3764
+ }
3765
+ function buildPlanningPrompt(issue, config, planPath, globalContextMd) {
3766
+ const workspace = resolve(config.workspace);
3767
+ const repoBlock = config.repos.map((r) => {
3768
+ const absPath = resolve(workspace, r.path);
3769
+ return `- **${r.name}**: \`${absPath}\` (base branch: \`${r.base_branch}\`)`;
3770
+ }).join("\n");
3771
+ const resolvedPlanPath = planPath ?? getPlanPath(workspace);
3772
+ const globalContextBlock = buildContextMdBlock(globalContextMd ?? null);
3773
+ 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.
3774
+
3775
+ **Do NOT implement anything.** Only analyze the issue and produce the plan file.
3776
+
3777
+ ## Issue
3778
+
3779
+ - **ID:** ${issue.id}
3780
+ - **Title:** ${issue.title}
3781
+ - **URL:** ${issue.url}
3782
+
3783
+ ### Description
3784
+
3785
+ ${issue.description}
3786
+
3787
+ ## Available Repositories
3788
+
3789
+ ${repoBlock}
3790
+ ${globalContextBlock}
3791
+ ## Instructions
3792
+
3793
+ 1. **Analyze the issue**: Read the title and description carefully. Determine which repositories above are affected by this change.
3794
+ Consider:
3795
+ - File paths or module names mentioned in the description
3796
+ - Technologies and frameworks referenced
3797
+ - Dependencies between repos (e.g., backend API changes needed before frontend can consume them)
3798
+
3799
+ 2. **Determine execution order**: If multiple repos are affected, decide the order. Repos that produce APIs, schemas, or shared libraries should come first. Repos that consume them should come later.
3800
+
3801
+ 3. **Write the plan file to disk**: Use a bash command or file-write tool to write the plan to \`${resolvedPlanPath}\`.
3802
+ **You MUST write the file to disk. Do NOT print the JSON to stdout or in a code block.**
3803
+
3804
+ The file must be valid JSON with this structure (replace angle-bracket placeholders with real values):
3805
+ - \`repoPath\`: absolute path to the affected repository
3806
+ - \`scope\`: concise English description of what to implement in that repo
3807
+ - \`order\`: integer starting at 1 (lower = executes first)
3808
+
3809
+ Use your write_file tool, or a bash command such as:
3810
+ \`\`\`bash
3811
+ printf '%s' '{"steps":[{"repoPath":"/absolute/path","scope":"description of work","order":1}]}' > '${resolvedPlanPath}'
3812
+ \`\`\`
3813
+
3814
+ ## Rules
3815
+
3816
+ - Only include repos that are actually affected by the issue. Do NOT include repos that don't need changes.
3817
+ - The \`scope\` field should be a concise English description of what needs to be done in that specific repo.
3818
+ - Order matters: lower order numbers execute first.
3819
+ - Do NOT implement anything. Do NOT create branches, write code, or commit.
3820
+ - If only one repo is affected, the plan should have a single step.`;
3821
+ }
3822
+ function buildContinuationPrompt(opts) {
3823
+ const { issue, diffStat, platform: platform2, baseBranch, manifestPath } = opts;
3824
+ const prCreateBlock = buildPrCreateInstruction(platform2, baseBranch);
3825
+ const outputLines = opts.previousOutput.split("\n");
3826
+ const truncatedOutput = outputLines.length > 50 ? outputLines.slice(-50).join("\n") : opts.previousOutput;
3827
+ return `You are an autonomous implementation agent resuming incomplete work.
3828
+ A previous agent session made code changes but did NOT complete the delivery steps (commit, push, PR, manifest).
3829
+ You MUST finish the job. Do NOT re-implement or undo existing changes \u2014 they are already in the working tree.
3830
+ Do NOT use interactive skills, ask clarifying questions, or wait for user input. You are running unattended. No human will see your output or respond to questions.
3831
+
3832
+ ## Issue
3833
+
3834
+ - **ID:** ${issue.id}
3835
+ - **Title:** ${issue.title}
3836
+
3837
+ ## Current State
3838
+
3839
+ The following files have been modified:
3840
+ \`\`\`
3841
+ ${diffStat}
3842
+ \`\`\`
3843
+
3844
+ ## Previous Session Output (last lines)
3845
+
3846
+ \`\`\`
3847
+ ${truncatedOutput}
3848
+ \`\`\`
3849
+
3850
+ ## Instructions
3851
+
3852
+ 1. **Review**: Run \`git diff\` to understand what was changed. Check if any work is obviously incomplete or broken. If critical files are missing changes, make minimal fixes.
3853
+
3854
+ 2. **Validate**: Run the project's linter/typecheck/tests if available:
3855
+ - Check \`package.json\` (or equivalent) for lint, typecheck, check, or test scripts.
3856
+ - Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`).
3857
+ - Fix any errors before proceeding.
3858
+
3859
+ **CRITICAL \u2014 Do NOT stop here. The following steps (commit, push, PR, manifest) are MANDATORY. Skipping them means the task has FAILED.**
3860
+
3861
+ 3. **Commit**: Make atomic commits with conventional commit messages.
3862
+ **IMPORTANT \u2014 Language rules:**
3863
+ - All commit messages MUST be in English.
3864
+ - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
3865
+
3866
+ 4. **Push**: Push the branch to origin:
3867
+ \`git push -u origin <branch-name>\`
3868
+ If the push fails due to a pre-push hook, read the error, fix the root cause, amend the commit, and retry. Do NOT use \`--no-verify\`.
3869
+
3870
+ 5. ${prCreateBlock}
3871
+
3872
+ 6. **Update tracker**: Call the lisa CLI to mark the issue as done:
3873
+ \`lisa issue done ${issue.id} --pr-url <pr-url>\`
3874
+ Wait 1 second before calling this command.
3875
+
3876
+ 7. **Write manifest**: Create \`${manifestPath}\` with JSON:
3877
+ \`\`\`json
3878
+ {"branch": "<final English branch name>", "prUrl": "<pull request URL>"}
3879
+ \`\`\`
3880
+ Do NOT commit this file.
3881
+
3882
+ ## Rules
3883
+
3884
+ - **ALL git commits, branch names, PR titles, and PR descriptions MUST be in English.**
3885
+ - Do NOT install new dependencies.
3886
+ - Do NOT use documentation lookup MCP tools.
3887
+ - One issue only. Do not pick up additional issues.
3888
+
3889
+ ## Completion Checklist
3890
+
3891
+ Before finishing, verify ALL of the following are true:
3892
+ - [ ] Code changes are committed (no uncommitted changes)
3893
+ - [ ] Branch is pushed to origin
3894
+ - [ ] Pull request is created and URL is captured
3895
+ - [ ] Manifest file is written with \`prUrl\` field
3896
+ If ANY item is unchecked, go back and complete it. Do NOT finish with incomplete steps.`;
3897
+ }
3898
+
3899
+ export {
3900
+ getAllProvidersWithAvailability,
3901
+ createProvider,
3902
+ isCompleteProviderExhaustion,
3903
+ runWithFallback,
3904
+ createSource,
3905
+ getContextPath,
3906
+ contextExists,
3907
+ readContext,
3908
+ WATCH_POLL_INTERVAL_MS,
3909
+ resolveModels,
3910
+ analyzeProject,
3911
+ runValidationCommands,
3912
+ buildValidationRecoveryPrompt,
3913
+ isProofOfWorkEnabled,
3914
+ appendPlatformAttribution,
3915
+ appendPlatformProofOfWork,
3916
+ detectPackageManager,
3917
+ detectTestRunner,
3918
+ buildImplementPrompt,
3919
+ buildNativeWorktreePrompt,
3920
+ buildScopedImplementPrompt,
3921
+ buildContextMdBlock,
3922
+ buildPlanningPrompt,
3923
+ buildContinuationPrompt
3924
+ };