@difflab/pi 0.1.0 → 0.2.0-rc.202609170958.44c1e9f

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.
Files changed (70) hide show
  1. package/README.md +36 -1
  2. package/agents/diffpi-autonomous.md +15 -0
  3. package/agents/diffpi-copilot.md +22 -0
  4. package/agents/diffpi-orchestrator.md +23 -0
  5. package/agents/diffpi-planner.md +15 -0
  6. package/agents/diffpi-reviewer.md +27 -0
  7. package/agents/diffpi-tutor.md +20 -0
  8. package/agents/diffpi-worker.md +19 -0
  9. package/dist/assets.d.ts +4 -0
  10. package/dist/assets.d.ts.map +1 -0
  11. package/dist/config.d.ts +28 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/environment.d.ts +40 -0
  14. package/dist/environment.d.ts.map +1 -0
  15. package/dist/extensions/index.js +2919 -183
  16. package/dist/forge.d.ts +48 -0
  17. package/dist/forge.d.ts.map +1 -0
  18. package/dist/fsx.d.ts +6 -0
  19. package/dist/fsx.d.ts.map +1 -0
  20. package/dist/gates.d.ts +12 -0
  21. package/dist/gates.d.ts.map +1 -0
  22. package/dist/index.d.ts +26 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2162 -162
  25. package/dist/modes.d.ts +59 -0
  26. package/dist/modes.d.ts.map +1 -0
  27. package/dist/pi.d.ts +21 -5
  28. package/dist/pi.d.ts.map +1 -1
  29. package/dist/process.d.ts +2 -0
  30. package/dist/process.d.ts.map +1 -1
  31. package/dist/review-backend.d.ts +15 -0
  32. package/dist/review-backend.d.ts.map +1 -0
  33. package/dist/review-publication.d.ts +17 -0
  34. package/dist/review-publication.d.ts.map +1 -0
  35. package/dist/review-types.d.ts +49 -0
  36. package/dist/review-types.d.ts.map +1 -0
  37. package/dist/review.d.ts +62 -0
  38. package/dist/review.d.ts.map +1 -0
  39. package/dist/setup.d.ts +11 -0
  40. package/dist/setup.d.ts.map +1 -1
  41. package/dist/store.d.ts +15 -0
  42. package/dist/store.d.ts.map +1 -0
  43. package/dist/templates.d.ts +14 -0
  44. package/dist/templates.d.ts.map +1 -0
  45. package/dist/tools/index.d.ts +6 -2
  46. package/dist/tools/index.d.ts.map +1 -1
  47. package/dist/tools/index.js +2631 -172
  48. package/dist/tools/modes.d.ts +4 -0
  49. package/dist/tools/modes.d.ts.map +1 -0
  50. package/dist/tools/review.d.ts +7 -0
  51. package/dist/tools/review.d.ts.map +1 -0
  52. package/dist/tools/setup.d.ts.map +1 -1
  53. package/dist/tools/templates.d.ts +3 -0
  54. package/dist/tools/templates.d.ts.map +1 -0
  55. package/dist/tuicr.d.ts +55 -0
  56. package/dist/tuicr.d.ts.map +1 -0
  57. package/dist/zed.d.ts +11 -0
  58. package/dist/zed.d.ts.map +1 -0
  59. package/package.json +4 -2
  60. package/skills/diffpi-setup/SKILL.md +15 -1
  61. package/skills/mode/SKILL.md +38 -0
  62. package/skills/review/SKILL.md +15 -0
  63. package/skills/review/references/workflows/address.md +10 -0
  64. package/skills/review/references/workflows/edit.md +9 -0
  65. package/skills/review/references/workflows/help.md +12 -0
  66. package/skills/review/references/workflows/merge.md +6 -0
  67. package/skills/review/references/workflows/new.md +9 -0
  68. package/skills/review/references/workflows/open.md +7 -0
  69. package/skills/review/references/workflows/publish.md +8 -0
  70. package/templates/review/draft-pr.md +18 -0
package/dist/index.js CHANGED
@@ -1,10 +1,1541 @@
1
+ // src/config.ts
2
+ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
3
+ import { z } from "zod";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ // src/fsx.ts
8
+ import { readdir, readFile } from "node:fs/promises";
9
+ async function readDirectoryIfExists(path) {
10
+ try {
11
+ return await readdir(path, { withFileTypes: true });
12
+ } catch (error) {
13
+ if (isMissingPath(error))
14
+ return [];
15
+ throw error;
16
+ }
17
+ }
18
+ async function readTextIfExists(path) {
19
+ try {
20
+ return await readFile(path, "utf8");
21
+ } catch (error) {
22
+ if (isMissingPath(error))
23
+ return;
24
+ throw error;
25
+ }
26
+ }
27
+ function isMissingPath(error) {
28
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
29
+ }
30
+
31
+ // src/config.ts
32
+ var modelReferenceSchema = z.string().trim().min(1);
33
+ var agentConfigSchema = z.object({
34
+ models: z.array(modelReferenceSchema).optional()
35
+ }).strict();
36
+ var diffpiConfigSchema = z.object({
37
+ agents: z.record(z.string(), agentConfigSchema).optional()
38
+ }).strict();
39
+ function diffpiConfigPaths(homeDir = homedir()) {
40
+ const directory = join(homeDir, ".difflab", "diffpi");
41
+ return {
42
+ yaml: join(directory, "config.yaml"),
43
+ json: join(directory, "config.json")
44
+ };
45
+ }
46
+ async function loadDiffpiConfig(options = {}) {
47
+ const paths = diffpiConfigPaths(options.homeDir);
48
+ for (const [format, path] of [
49
+ ["yaml", paths.yaml],
50
+ ["json", paths.json]
51
+ ]) {
52
+ const content = await readTextIfExists(path);
53
+ if (content === undefined)
54
+ continue;
55
+ try {
56
+ const value = format === "yaml" ? parseYamlConfig(content) : JSON.parse(content);
57
+ return { config: diffpiConfigSchema.parse(value ?? {}), path };
58
+ } catch (error) {
59
+ const reason = error instanceof Error ? error.message : String(error);
60
+ throw new Error(`Invalid Diffpi config at ${path}: ${reason}`, { cause: error });
61
+ }
62
+ }
63
+ return { config: {} };
64
+ }
65
+ function resolveAgentModelPreferences(agentId, profilePreferences, config) {
66
+ const override = config.agents?.[agentId];
67
+ if (override && Object.hasOwn(override, "models"))
68
+ return [...override.models ?? []];
69
+ return [...profilePreferences];
70
+ }
71
+ function findPreferredModel(models, preference) {
72
+ const normalizedPreference = normalizeModelReference(preference);
73
+ const exactReference = models.find((model) => normalizeModelReference(`${model.provider}/${model.id}`) === normalizedPreference);
74
+ if (exactReference)
75
+ return exactReference;
76
+ const idPreference = preference.includes("/") ? preference.slice(preference.indexOf("/") + 1) : preference;
77
+ const normalizedIdPreference = normalizeModelReference(idPreference);
78
+ const exactId = models.find((model) => normalizeModelReference(model.id) === normalizedIdPreference);
79
+ if (exactId)
80
+ return exactId;
81
+ const preferenceTokens = normalizedIdPreference.split("-").filter(Boolean);
82
+ return models.find((model) => {
83
+ const modelTokens = new Set(normalizeModelReference(model.id).split("-").filter(Boolean));
84
+ return preferenceTokens.every((token) => modelTokens.has(token));
85
+ });
86
+ }
87
+ function parseYamlConfig(content) {
88
+ const document = content.replace(/^\uFEFF/, "").replace(/^---[^\S\r\n]*(?:#.*)?(?:\r?\n|$)/, "");
89
+ return parseFrontmatter(`---
90
+ ${document}
91
+ ---
92
+ `).frontmatter;
93
+ }
94
+ function normalizeModelReference(value) {
95
+ return value.toLowerCase().replace(/^~/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
96
+ }
97
+ // src/environment.ts
98
+ import { basename } from "node:path";
99
+
100
+ // src/process.ts
101
+ import { constants } from "node:fs";
102
+ import { access } from "node:fs/promises";
103
+ import { delimiter, join as join2 } from "node:path";
104
+ import { spawn } from "node:child_process";
105
+ var MAX_CAPTURED_OUTPUT_LENGTH = 65536;
106
+ async function findExecutable(name) {
107
+ if (name.includes("/")) {
108
+ try {
109
+ await access(name, constants.X_OK);
110
+ return name;
111
+ } catch {
112
+ return;
113
+ }
114
+ }
115
+ for (const directory of (process.env.PATH ?? "").split(delimiter)) {
116
+ if (!directory)
117
+ continue;
118
+ const candidate = join2(directory, name);
119
+ try {
120
+ await access(candidate, constants.X_OK);
121
+ return candidate;
122
+ } catch {}
123
+ }
124
+ return;
125
+ }
126
+ function run(command, args, options = {}) {
127
+ return new Promise((resolve, reject) => {
128
+ const child = spawn(command, args, {
129
+ cwd: options.cwd,
130
+ env: options.env ?? process.env,
131
+ stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"]
132
+ });
133
+ let stdout = "";
134
+ let stderr = "";
135
+ const stdoutChunks = [];
136
+ const stderrChunks = [];
137
+ const unbounded = options.capture === "unbounded";
138
+ child.stdout?.on("data", (chunk) => {
139
+ const text = chunk.toString();
140
+ if (unbounded)
141
+ stdoutChunks.push(text);
142
+ else
143
+ stdout = appendBounded(stdout, text);
144
+ });
145
+ child.stderr?.on("data", (chunk) => {
146
+ const text = chunk.toString();
147
+ if (unbounded)
148
+ stderrChunks.push(text);
149
+ else
150
+ stderr = appendBounded(stderr, text);
151
+ });
152
+ child.on("error", reject);
153
+ child.on("close", (code) => resolve({
154
+ code: code ?? 1,
155
+ stdout: unbounded ? stdoutChunks.join("") : stdout,
156
+ stderr: unbounded ? stderrChunks.join("") : stderr
157
+ }));
158
+ if (options.input !== undefined && child.stdin)
159
+ child.stdin.end(options.input);
160
+ });
161
+ }
162
+ async function runChecked(command, args, options = {}) {
163
+ const result = await run(command, args, options);
164
+ if (result.code === 0)
165
+ return result;
166
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
167
+ throw new Error(`${command} ${args.join(" ")} failed: ${detail}`);
168
+ }
169
+ function appendBounded(current, next) {
170
+ const combined = current + next;
171
+ return combined.length <= MAX_CAPTURED_OUTPUT_LENGTH ? combined : combined.slice(-MAX_CAPTURED_OUTPUT_LENGTH);
172
+ }
173
+
174
+ // src/zed.ts
175
+ import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
176
+ import { homedir as homedir2 } from "node:os";
177
+ import { dirname, join as join3 } from "node:path";
178
+ var ZED_REVIEW_TASK_NAME = "diffpi: tuicr review";
179
+ var REVIEW_KEYBINDING = "cmd-alt-r";
180
+ var REVIEW_TASK = {
181
+ label: ZED_REVIEW_TASK_NAME,
182
+ command: "tuicr",
183
+ args: ["-w"],
184
+ cwd: "$ZED_WORKTREE_ROOT",
185
+ use_new_terminal: true,
186
+ reveal: "always"
187
+ };
188
+ function zedTasksPath(homeDir = homedir2()) {
189
+ return join3(homeDir, ".config", "zed", "tasks.json");
190
+ }
191
+ function zedKeymapPath(homeDir = homedir2()) {
192
+ return join3(homeDir, ".config", "zed", "keymap.json");
193
+ }
194
+ async function ensureZedReviewTask(homeDir = homedir2()) {
195
+ const path = zedTasksPath(homeDir);
196
+ const currentText = await readOptional(path);
197
+ const tasks = parseJsonArray(currentText, path);
198
+ const index = tasks.findIndex((task) => task.label === ZED_REVIEW_TASK_NAME);
199
+ const next = [...tasks];
200
+ if (index >= 0)
201
+ next[index] = { ...tasks[index], ...REVIEW_TASK };
202
+ else
203
+ next.push(REVIEW_TASK);
204
+ const changed = JSON.stringify(tasks) !== JSON.stringify(next);
205
+ if (changed)
206
+ await writeJson(path, next);
207
+ return { path, changed, existed: currentText !== undefined };
208
+ }
209
+ async function ensureZedReviewKeybinding(homeDir = homedir2()) {
210
+ const path = zedKeymapPath(homeDir);
211
+ const currentText = await readOptional(path);
212
+ const entries = parseJsonArray(currentText, path);
213
+ const alreadyBound = entries.some((entry) => Object.values(entry.bindings ?? {}).some((action) => Array.isArray(action) && action[0] === "task::Spawn" && bindsReviewTask(action[1])));
214
+ if (alreadyBound)
215
+ return { path, changed: false, existed: currentText !== undefined };
216
+ const next = [
217
+ ...entries,
218
+ { context: "Workspace", bindings: { [REVIEW_KEYBINDING]: ["task::Spawn", { task_name: ZED_REVIEW_TASK_NAME }] } }
219
+ ];
220
+ await writeJson(path, next);
221
+ return { path, changed: true, existed: currentText !== undefined };
222
+ }
223
+ function bindsReviewTask(payload) {
224
+ return typeof payload === "object" && payload !== null && payload.task_name === ZED_REVIEW_TASK_NAME;
225
+ }
226
+ async function readOptional(path) {
227
+ try {
228
+ return await readFile2(path, "utf8");
229
+ } catch (error) {
230
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
231
+ return;
232
+ throw error;
233
+ }
234
+ }
235
+ function parseJsonArray(content, path) {
236
+ if (!content?.trim())
237
+ return [];
238
+ let value;
239
+ try {
240
+ value = JSON.parse(content);
241
+ } catch {
242
+ throw new Error(`Cannot safely edit ${path}: not strict JSON (it may contain JSONC comments).`);
243
+ }
244
+ if (!Array.isArray(value))
245
+ throw new Error(`Expected a JSON array in ${path}.`);
246
+ return value;
247
+ }
248
+ async function writeJson(path, value) {
249
+ await mkdir(dirname(path), { recursive: true });
250
+ await writeFile(path, `${JSON.stringify(value, null, 2)}
251
+ `, "utf8");
252
+ }
253
+
254
+ // src/environment.ts
255
+ function detectIde(env = process.env) {
256
+ const program = (env.TERM_PROGRAM ?? "").toLowerCase();
257
+ if (env.ZED_TERM === "true" || program === "zed")
258
+ return "zed";
259
+ if (env.CURSOR_TRACE_ID || program === "cursor")
260
+ return "cursor";
261
+ if (env.WINDSURF_ENV || program === "windsurf")
262
+ return "windsurf";
263
+ if (env.TERMINAL_EMULATOR?.toLowerCase().includes("jetbrains"))
264
+ return "jetbrains";
265
+ if (env.VSCODE_PID || env.VSCODE_GIT_IPC_HANDLE || program === "vscode")
266
+ return "vscode";
267
+ return "unknown";
268
+ }
269
+ function detectMux(env = process.env) {
270
+ if (env.ZELLIJ || env.ZELLIJ_SESSION_NAME)
271
+ return "zellij";
272
+ if (env.TMUX)
273
+ return "tmux";
274
+ if (env.STY)
275
+ return "screen";
276
+ return "none";
277
+ }
278
+ function detectShell(env = process.env) {
279
+ return env.SHELL ? basename(env.SHELL) : "unknown";
280
+ }
281
+ async function detectVcs(cwd) {
282
+ const root = (await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"])).stdout.trim() || cwd;
283
+ const branch = (await run("git", ["-C", root, "rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
284
+ const remote = (await run("git", ["-C", root, "remote", "get-url", "origin"])).stdout.trim();
285
+ return { ...parseRemote(remote), branch, root };
286
+ }
287
+ function parseRemote(remote) {
288
+ const empty = { provider: "none", host: "", owner: "", repo: "" };
289
+ if (!remote)
290
+ return empty;
291
+ const scp = remote.match(/^[^@]+@([^:]+):(.+?)(?:\.git)?$/);
292
+ const url = remote.match(/^[a-z]+:\/\/(?:[^@]+@)?([^/]+)\/(.+?)(?:\.git)?$/i);
293
+ const match = scp ?? url;
294
+ if (!match)
295
+ return empty;
296
+ const host = match[1];
297
+ const segments = match[2].split("/").filter(Boolean);
298
+ if (segments.length < 2)
299
+ return { ...empty, host };
300
+ const repo = segments.at(-1) ?? "";
301
+ const owner = segments.slice(0, -1).join("/");
302
+ const provider = /github/i.test(host) ? "github" : /gitlab/i.test(host) ? "gitlab" : "none";
303
+ return { provider, host, owner, repo };
304
+ }
305
+ async function openInNewTab(command, opts) {
306
+ const env = opts.env ?? process.env;
307
+ const name = opts.name ?? "review";
308
+ const printable = command.join(" ");
309
+ const mux = detectMux(env);
310
+ if (mux !== "none") {
311
+ const opened = await openMuxTab(mux, command, opts.cwd, name, printable);
312
+ if (opened)
313
+ return opened;
314
+ }
315
+ if (detectIde(env) === "zed") {
316
+ try {
317
+ await ensureZedReviewTask(opts.homeDir);
318
+ return {
319
+ launched: false,
320
+ configured: true,
321
+ via: "zed-task",
322
+ command: printable,
323
+ taskName: ZED_REVIEW_TASK_NAME,
324
+ instruction: `Run the Zed task "${ZED_REVIEW_TASK_NAME}".`
325
+ };
326
+ } catch {}
327
+ }
328
+ return { launched: false, via: "print", command: printable };
329
+ }
330
+ async function openMuxTab(mux, command, cwd, name, printable) {
331
+ if (mux === "zellij" && await findExecutable("zellij")) {
332
+ const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
333
+ if (result.code === 0)
334
+ return { launched: true, via: "zellij", command: printable };
335
+ const fallback = await run("zellij", ["run", "--cwd", cwd, "--name", name, "--", ...command]);
336
+ if (fallback.code === 0)
337
+ return { launched: true, via: "zellij-run", command: printable };
338
+ }
339
+ if (mux === "tmux" && await findExecutable("tmux")) {
340
+ const result = await run("tmux", ["new-window", "-c", cwd, "-n", name, printable]);
341
+ if (result.code === 0)
342
+ return { launched: true, via: "tmux", command: printable };
343
+ }
344
+ if (mux === "screen" && await findExecutable("screen")) {
345
+ const result = await run("screen", screenWindowArgs(command, cwd, name));
346
+ if (result.code === 0)
347
+ return { launched: true, via: "screen", command: printable };
348
+ }
349
+ return;
350
+ }
351
+ function screenWindowArgs(command, cwd, name) {
352
+ return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
353
+ }
354
+ // src/forge.ts
355
+ function createForge(vcs) {
356
+ if (vcs.provider === "github")
357
+ return new GithubForge(vcs);
358
+ if (vcs.provider === "gitlab")
359
+ return new GitlabForge(vcs);
360
+ throw new Error("No forge detected from the git remote. Use --local for an offline review.");
361
+ }
362
+
363
+ class GithubForge {
364
+ vcs;
365
+ provider = "github";
366
+ constructor(vcs) {
367
+ this.vcs = vcs;
368
+ }
369
+ repoFlag() {
370
+ return ["--repo", `${this.vcs.owner}/${this.vcs.repo}`];
371
+ }
372
+ async createDraftPr(options) {
373
+ const args = [
374
+ "pr",
375
+ "create",
376
+ ...this.repoFlag(),
377
+ "--title",
378
+ options.title,
379
+ "--body",
380
+ options.body,
381
+ "--base",
382
+ options.base,
383
+ "--head",
384
+ options.head
385
+ ];
386
+ if (options.draft !== false)
387
+ args.push("--draft");
388
+ await runChecked("gh", args);
389
+ const ref = await this.viewPr(options.head);
390
+ if (!ref)
391
+ throw new Error("Draft PR created but could not be resolved.");
392
+ return ref;
393
+ }
394
+ async viewPr(idOrBranch) {
395
+ const args = [
396
+ "pr",
397
+ "view",
398
+ idOrBranch,
399
+ ...this.repoFlag(),
400
+ "--json",
401
+ "number,title,url,isDraft,baseRefName,headRefName,headRefOid"
402
+ ];
403
+ const result = await run("gh", args);
404
+ if (result.code !== 0) {
405
+ if (isConfirmedMissingChange("github", result.stderr || result.stdout))
406
+ return;
407
+ throw commandFailure("gh", args, result);
408
+ }
409
+ if (!result.stdout.trim())
410
+ throw new Error("GitHub returned an empty pull request response.");
411
+ let data;
412
+ try {
413
+ data = JSON.parse(result.stdout);
414
+ } catch {
415
+ throw new Error("Cannot parse the GitHub pull request response as JSON.");
416
+ }
417
+ if (typeof data.number !== "number" || typeof data.title !== "string" || typeof data.url !== "string" || typeof data.isDraft !== "boolean" || typeof data.baseRefName !== "string" || typeof data.headRefName !== "string") {
418
+ throw new Error("GitHub returned an invalid pull request response.");
419
+ }
420
+ return {
421
+ number: data.number,
422
+ title: data.title,
423
+ url: data.url,
424
+ isDraft: data.isDraft,
425
+ baseRef: data.baseRefName,
426
+ headRef: data.headRefName,
427
+ headSha: data.headRefOid
428
+ };
429
+ }
430
+ async defaultBranch() {
431
+ const result = await runChecked("gh", [
432
+ "repo",
433
+ "view",
434
+ `${this.vcs.owner}/${this.vcs.repo}`,
435
+ "--json",
436
+ "defaultBranchRef",
437
+ "--jq",
438
+ ".defaultBranchRef.name"
439
+ ]);
440
+ return requireBranchName(result.stdout, "GitHub");
441
+ }
442
+ async prDiff(id) {
443
+ const result = await runChecked("gh", ["pr", "diff", String(id), ...this.repoFlag()], { capture: "unbounded" });
444
+ return result.stdout;
445
+ }
446
+ async prChecks(id) {
447
+ const result = await run("gh", ["pr", "checks", String(id), ...this.repoFlag()]);
448
+ return result.stdout;
449
+ }
450
+ async markReady(id) {
451
+ await runChecked("gh", ["pr", "ready", String(id), ...this.repoFlag()]);
452
+ }
453
+ async closePr(id, comment) {
454
+ const args = ["pr", "close", String(id), ...this.repoFlag()];
455
+ if (comment)
456
+ args.push("--comment", comment);
457
+ await runChecked("gh", args);
458
+ }
459
+ async mergePr(id, subject) {
460
+ const readiness = await runChecked("gh", [
461
+ "pr",
462
+ "view",
463
+ String(id),
464
+ ...this.repoFlag(),
465
+ "--json",
466
+ "isDraft,state,reviewDecision,mergeStateStatus,statusCheckRollup"
467
+ ]);
468
+ assertGitHubMergeReady(readiness.stdout);
469
+ await runChecked("gh", ["pr", "merge", String(id), ...this.repoFlag(), "--squash", "--subject", subject]);
470
+ }
471
+ }
472
+
473
+ class GitlabForge {
474
+ vcs;
475
+ provider = "gitlab";
476
+ constructor(vcs) {
477
+ this.vcs = vcs;
478
+ }
479
+ project() {
480
+ return `${this.vcs.owner}/${this.vcs.repo}`;
481
+ }
482
+ async createDraftPr(options) {
483
+ await runChecked("glab", [
484
+ "mr",
485
+ "create",
486
+ "--repo",
487
+ this.project(),
488
+ "--title",
489
+ `Draft: ${options.title}`,
490
+ "--description",
491
+ options.body,
492
+ "--target-branch",
493
+ options.base,
494
+ "--source-branch",
495
+ options.head,
496
+ "--yes"
497
+ ]);
498
+ const ref = await this.viewPr(options.head);
499
+ if (!ref)
500
+ throw new Error("Draft MR created but could not be resolved.");
501
+ return ref;
502
+ }
503
+ async viewPr(idOrBranch) {
504
+ const args = ["mr", "view", idOrBranch, "--repo", this.project(), "--output", "json"];
505
+ const result = await run("glab", args);
506
+ if (result.code !== 0) {
507
+ if (isConfirmedMissingChange("gitlab", result.stderr || result.stdout))
508
+ return;
509
+ throw commandFailure("glab", args, result);
510
+ }
511
+ if (!result.stdout.trim())
512
+ throw new Error("GitLab returned an empty merge request response.");
513
+ let data;
514
+ try {
515
+ data = JSON.parse(result.stdout);
516
+ } catch {
517
+ throw new Error("Cannot parse the GitLab merge request response as JSON.");
518
+ }
519
+ if (typeof data.iid !== "number" || typeof data.title !== "string" || typeof data.web_url !== "string" || typeof data.target_branch !== "string" || typeof data.source_branch !== "string") {
520
+ throw new Error("GitLab returned an invalid merge request response.");
521
+ }
522
+ return {
523
+ number: data.iid,
524
+ title: data.title,
525
+ url: data.web_url,
526
+ isDraft: Boolean(data.draft ?? data.work_in_progress),
527
+ baseRef: data.target_branch,
528
+ headRef: data.source_branch,
529
+ headSha: data.sha
530
+ };
531
+ }
532
+ async defaultBranch() {
533
+ const result = await runChecked("glab", [
534
+ "api",
535
+ `projects/${encodeURIComponent(this.project())}`,
536
+ "--jq",
537
+ ".default_branch"
538
+ ]);
539
+ return requireBranchName(result.stdout, "GitLab");
540
+ }
541
+ async prDiff(id) {
542
+ return (await runChecked("glab", ["mr", "diff", String(id), "--repo", this.project()], { capture: "unbounded" })).stdout;
543
+ }
544
+ async prChecks(id) {
545
+ return (await runChecked("glab", [
546
+ "api",
547
+ `projects/${encodeURIComponent(this.project())}/merge_requests/${id}/pipelines?per_page=100`
548
+ ])).stdout;
549
+ }
550
+ async markReady(id) {
551
+ await runChecked("glab", ["mr", "update", String(id), "--repo", this.project(), "--ready"]);
552
+ }
553
+ async closePr(id, comment) {
554
+ if (comment)
555
+ await runChecked("glab", ["mr", "note", String(id), "--repo", this.project(), "--message", comment]);
556
+ await runChecked("glab", ["mr", "close", String(id), "--repo", this.project()]);
557
+ }
558
+ async mergePr() {
559
+ throw new Error("Merge is not supported by the GitLab forge adapter.");
560
+ }
561
+ }
562
+ function assertGitHubMergeReady(input) {
563
+ let data;
564
+ try {
565
+ data = JSON.parse(input);
566
+ } catch {
567
+ throw new Error("Merge blocked: GitHub readiness response was not valid JSON.");
568
+ }
569
+ const blockers = [];
570
+ if (data.state !== "OPEN")
571
+ blockers.push(`pull request state is ${data.state ?? "unknown"}`);
572
+ if (data.isDraft)
573
+ blockers.push("pull request is still a draft");
574
+ if (data.reviewDecision !== "APPROVED")
575
+ blockers.push(`review decision is ${data.reviewDecision || "not approved"}`);
576
+ if (data.mergeStateStatus !== "CLEAN")
577
+ blockers.push(`merge state is ${data.mergeStateStatus ?? "unknown"}`);
578
+ for (const check of data.statusCheckRollup ?? []) {
579
+ const name = check.name ?? check.context ?? "unnamed check";
580
+ if (check.__typename === "CheckRun") {
581
+ if (check.status !== "COMPLETED")
582
+ blockers.push(`${name} is ${check.status?.toLowerCase() ?? "pending"}`);
583
+ else if (!["SUCCESS", "SKIPPED", "NEUTRAL"].includes(check.conclusion ?? "")) {
584
+ blockers.push(`${name} concluded ${(check.conclusion ?? "unknown").toLowerCase()}`);
585
+ }
586
+ } else if (check.state !== "SUCCESS")
587
+ blockers.push(`${name} is ${(check.state ?? "pending").toLowerCase()}`);
588
+ }
589
+ if (blockers.length > 0)
590
+ throw new Error(`Merge blocked: ${blockers.join("; ")}.`);
591
+ }
592
+ function isConfirmedMissingChange(provider, output) {
593
+ const message = output.toLowerCase();
594
+ if (provider === "github") {
595
+ return message.includes("no pull requests found for branch") || message.includes("could not find pull request") || message.includes("could not resolve to a pullrequest");
596
+ }
597
+ if (provider === "gitlab") {
598
+ return message.includes("no open merge request") || /failed to get open merge request/.test(message) && /404(?: not found)?/.test(message);
599
+ }
600
+ return false;
601
+ }
602
+ function commandFailure(command, args, result) {
603
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
604
+ return new Error(`${command} ${args.join(" ")} failed: ${detail}`);
605
+ }
606
+ function requireBranchName(output, provider) {
607
+ const branch = output.trim();
608
+ if (!branch || branch === "null")
609
+ throw new Error(`${provider} did not return a default branch.`);
610
+ return branch;
611
+ }
612
+ // src/gates.ts
613
+ var CONVENTIONAL_COMMIT = /^(feat|fix|perf|refactor|docs|chore|test|build|ci|style|revert)(\([^)]+\))?!?: .+/;
614
+ var MISE_GATES = ["format:check", "lint", "test"];
615
+ function checkConventionalSubject(subject) {
616
+ const trimmed = subject.trim();
617
+ const ok = CONVENTIONAL_COMMIT.test(trimmed);
618
+ return {
619
+ name: "conventional-subject",
620
+ status: ok ? "pass" : "warn",
621
+ detail: ok ? trimmed : `not a conventional-commit subject: "${trimmed}"`
622
+ };
623
+ }
624
+ async function runMiseGates(cwd) {
625
+ const tasks = await discoverMiseTasks(cwd);
626
+ const results = [];
627
+ for (const gate of MISE_GATES) {
628
+ const targets = tasks.get(gate) ?? [];
629
+ if (targets.length === 0) {
630
+ results.push({ name: gate, status: "skip", detail: "no mise recipe" });
631
+ continue;
632
+ }
633
+ const invocations = targets.flatMap((target, index) => index === 0 ? [target] : [":::", target]);
634
+ const result = await run("mise", ["run", ...invocations], { cwd });
635
+ results.push({
636
+ name: gate,
637
+ status: result.code === 0 ? "pass" : "fail",
638
+ detail: result.code === 0 ? "clean" : (result.stderr.trim() || result.stdout.trim()).slice(-400)
639
+ });
640
+ }
641
+ return results;
642
+ }
643
+ function ciGate(checksOutput) {
644
+ const text = checksOutput.toLowerCase();
645
+ if (!text.trim())
646
+ return { name: "ci", status: "skip", detail: "no CI output" };
647
+ if (/\bfail|error\b/.test(text))
648
+ return { name: "ci", status: "warn", detail: "CI failing" };
649
+ if (/\bpending|in progress|queued\b/.test(text))
650
+ return { name: "ci", status: "warn", detail: "CI pending" };
651
+ return { name: "ci", status: "pass", detail: "CI green" };
652
+ }
653
+ async function discoverMiseTasks(cwd) {
654
+ const result = await run("mise", ["tasks", "--json", "--all"], { cwd });
655
+ if (result.code !== 0)
656
+ return new Map;
657
+ return parseMiseTasks(result.stdout);
658
+ }
659
+ function parseMiseTasks(input) {
660
+ let tasks;
661
+ try {
662
+ tasks = JSON.parse(input);
663
+ } catch {
664
+ return new Map;
665
+ }
666
+ if (!Array.isArray(tasks))
667
+ return new Map;
668
+ const found = new Map;
669
+ for (const gate of MISE_GATES) {
670
+ const targets = tasks.flatMap((task) => {
671
+ if (typeof task.name !== "string")
672
+ return [];
673
+ return task.name === gate || task.name.endsWith(`:${gate}`) || task.aliases?.includes(gate) ? [task.name] : [];
674
+ });
675
+ if (targets.length > 0)
676
+ found.set(gate, [...new Set(targets)]);
677
+ }
678
+ return found;
679
+ }
680
+ // src/review-backend.ts
681
+ import { readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
682
+
683
+ // src/review.ts
684
+ import { z as z2 } from "zod";
685
+ var severitySchema = z2.enum(["BLOCKING", "CONSIDER", "NOTE"]);
686
+ var findingSchema = z2.object({
687
+ file: z2.string().min(1),
688
+ line: z2.number().int().nonnegative(),
689
+ severity: severitySchema,
690
+ body: z2.string().min(1),
691
+ reference: z2.string().optional().default("")
692
+ });
693
+ var findingsSchema = z2.array(findingSchema);
694
+ var reviewThreadRecordSchema = z2.object({
695
+ id: z2.string().min(1),
696
+ file: z2.string().optional(),
697
+ line: z2.number().int().positive().optional(),
698
+ body: z2.string(),
699
+ author: z2.string().optional(),
700
+ resolved: z2.boolean(),
701
+ question: z2.boolean(),
702
+ replies: z2.array(z2.string()).optional()
703
+ });
704
+ function reviewSlug(input) {
705
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
706
+ }
707
+ function yymmdd(date = new Date) {
708
+ const yy = String(date.getFullYear() % 100).padStart(2, "0");
709
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
710
+ const dd = String(date.getDate()).padStart(2, "0");
711
+ return `${yy}${mm}${dd}`;
712
+ }
713
+ function reviewRecordName(target, date = new Date) {
714
+ return `${yymmdd(date)}-${reviewSlug(target) || "local"}`;
715
+ }
716
+ function dedupeFindings(findings) {
717
+ const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
718
+ const byKey = new Map;
719
+ for (const finding of findings) {
720
+ const key = `${finding.file}:${finding.line}`;
721
+ const existing = byKey.get(key);
722
+ if (!existing || rank[finding.severity] > rank[existing.severity])
723
+ byKey.set(key, finding);
724
+ }
725
+ return [...byKey.values()].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || rank[b.severity] - rank[a.severity]);
726
+ }
727
+ function toReviewComments(findings, model) {
728
+ const comments = [];
729
+ for (const finding of findings) {
730
+ if (finding.line <= 0)
731
+ continue;
732
+ const body = renderCommentBody(finding);
733
+ comments.push({
734
+ file: finding.file,
735
+ line: finding.line,
736
+ side: "RIGHT",
737
+ body: model ? withRemoteProvenance(body, model) : body
738
+ });
739
+ }
740
+ return comments;
741
+ }
742
+ function withRemoteProvenance(body, model) {
743
+ const normalized = body.trimEnd();
744
+ if (/Generated review by Diffpi using `[^`]+`\.$/.test(normalized))
745
+ return body;
746
+ return `${normalized}
747
+
748
+ Generated review by Diffpi using \`${model}\`.`;
749
+ }
750
+ function localReviewAuthor(model) {
751
+ return `Agent: ${model}`;
752
+ }
753
+ function renderReviewDoc(input) {
754
+ const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
755
+ const lines = [`# Review: ${input.title}`, "", "## Metadata"];
756
+ if (input.number !== undefined)
757
+ lines.push(`- **PR/MR**: #${input.number}${input.url ? ` — ${input.url}` : ""}`);
758
+ if (input.author)
759
+ lines.push(`- **Author**: ${input.author}`);
760
+ if (input.model)
761
+ lines.push(`- **Review agent**: ${input.model}`);
762
+ if (input.headRef && input.baseRef)
763
+ lines.push(`- **Branch**: ${input.headRef} → ${input.baseRef}`);
764
+ if (input.additions !== undefined)
765
+ lines.push(`- **Stats**: +${input.additions} -${input.deletions ?? 0} across ${input.changedFiles ?? 0} files`);
766
+ lines.push(`- **Reviewed**: ${input.timestamp ?? new Date().toISOString()}`, "");
767
+ if (input.overallIssues.length > 0) {
768
+ lines.push("## Overall issues", "");
769
+ for (const issue of input.overallIssues)
770
+ lines.push(`- ${issue}`);
771
+ lines.push("");
772
+ }
773
+ lines.push("## Verification", "");
774
+ for (const gate of input.gates)
775
+ lines.push(`- ${gate.name}: ${gate.status} — ${gate.detail}`);
776
+ lines.push("");
777
+ if (input.notVerified.length > 0) {
778
+ lines.push("## What was NOT verified", "");
779
+ for (const item of input.notVerified)
780
+ lines.push(`- ${item}`);
781
+ lines.push("");
782
+ }
783
+ lines.push("## Inline Comments", "");
784
+ for (const finding of anchored) {
785
+ lines.push(`### ${finding.file}:${finding.line} — ${finding.severity}`, "", finding.body, "");
786
+ if (finding.reference)
787
+ lines.push(`> **Reference:** ${finding.reference}`, "");
788
+ lines.push("---", "");
789
+ }
790
+ return `${lines.join(`
791
+ `).trimEnd()}
792
+ `;
793
+ }
794
+ function renderThreadArtifact(title, target, threads, options = {}) {
795
+ const records = threads.map((thread) => ({
796
+ id: thread.id,
797
+ file: thread.file,
798
+ line: thread.line,
799
+ body: thread.body,
800
+ author: thread.author,
801
+ resolved: thread.resolved,
802
+ question: thread.question,
803
+ replies: thread.replies
804
+ }));
805
+ const payload = Buffer.from(JSON.stringify(records), "utf8").toString("base64url");
806
+ const lines = [
807
+ `<!-- diffpi-threads:${payload} -->`,
808
+ `# Review threads: ${title}`,
809
+ "",
810
+ "## Metadata",
811
+ "",
812
+ `- Target: ${target}`,
813
+ `- Pulled: ${options.timestamp ?? new Date().toISOString()}`
814
+ ];
815
+ if (options.number !== undefined)
816
+ lines.push(`- PR/MR: #${options.number}${options.url ? ` — ${options.url}` : ""}`);
817
+ lines.push("", "## Replies", "");
818
+ for (const thread of threads) {
819
+ const id = Buffer.from(thread.id, "utf8").toString("base64url");
820
+ lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} (${thread.id})`, "", `<!-- diffpi-reply-start:${id} -->`, thread.reply ?? "", `<!-- diffpi-reply-end:${id} -->`, "");
821
+ }
822
+ lines.push("## Source comments", "");
823
+ for (const thread of threads) {
824
+ lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, "", thread.body, "", "---", "");
825
+ }
826
+ return `${lines.join(`
827
+ `).trimEnd()}
828
+ `;
829
+ }
830
+ function parseThreadArtifact(content) {
831
+ const payload = content.match(/^<!-- diffpi-threads:([A-Za-z0-9_-]+) -->$/m)?.[1];
832
+ if (!payload)
833
+ throw new Error("This file is not a Diffpi thread artifact.");
834
+ let threads;
835
+ try {
836
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
837
+ threads = z2.array(reviewThreadRecordSchema).parse(decoded);
838
+ } catch {
839
+ throw new Error("Cannot parse the Diffpi thread artifact payload.");
840
+ }
841
+ const replies = content.split(/^## Source comments$/m, 1)[0] ?? "";
842
+ return threads.map((thread) => {
843
+ const id = Buffer.from(thread.id, "utf8").toString("base64url");
844
+ const startMarker = `<!-- diffpi-reply-start:${id} -->
845
+ `;
846
+ const endMarker = `
847
+ <!-- diffpi-reply-end:${id} -->`;
848
+ const start = replies.indexOf(startMarker);
849
+ const end = start < 0 ? -1 : replies.indexOf(endMarker, start + startMarker.length);
850
+ const reply = start >= 0 && end >= 0 ? replies.slice(start + startMarker.length, end).trim() : "";
851
+ return reply ? { ...thread, reply } : thread;
852
+ });
853
+ }
854
+ function upsertThreadReply(content, threadId, body, question) {
855
+ const threads = parseThreadArtifact(content);
856
+ const thread = threads.find((candidate) => candidate.id === threadId);
857
+ if (!thread)
858
+ throw new Error(`Review thread ${threadId} was not found in the local artifact.`);
859
+ thread.reply = body;
860
+ if (question !== undefined)
861
+ thread.question = question;
862
+ const title = content.match(/^# Review threads: (.+)$/m)?.[1] ?? "review";
863
+ const target = content.match(/^- Target: (.+)$/m)?.[1] ?? "local";
864
+ const timestamp = content.match(/^- Pulled: (.+)$/m)?.[1];
865
+ const pr = content.match(/^- PR\/MR: #(\d+)(?: — (.+))?$/m);
866
+ return renderThreadArtifact(title, target, threads, {
867
+ timestamp,
868
+ number: pr ? Number.parseInt(pr[1], 10) : undefined,
869
+ url: pr?.[2]
870
+ });
871
+ }
872
+ function renderCommentBody(finding) {
873
+ const prefix = finding.severity === "BLOCKING" ? "**BLOCKING** " : "";
874
+ const reference = finding.reference ? `
875
+
876
+ > **Reference:** ${finding.reference}` : "";
877
+ return `${prefix}${finding.body}${reference}`;
878
+ }
879
+
880
+ // src/tuicr.ts
881
+ import { readFile as readFile3, realpath as realpath2 } from "node:fs/promises";
882
+ import { resolve as resolve2 } from "node:path";
883
+
884
+ // src/store.ts
885
+ import { createHash } from "node:crypto";
886
+ import { lstat, mkdir as mkdir2, readlink, realpath, symlink, unlink } from "node:fs/promises";
887
+ import { homedir as homedir3 } from "node:os";
888
+ import { dirname as dirname2, isAbsolute, join as join4, resolve } from "node:path";
889
+ var STORE_LINK = ".diffpi";
890
+ var LEGACY_STORE_LINK = join4(".pi", "diffpi");
891
+ async function gitToplevel(cwd) {
892
+ const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
893
+ const top = result.stdout.trim();
894
+ return result.code === 0 && top ? top : resolve(cwd);
895
+ }
896
+ async function computeProjectSlug(cwd) {
897
+ const root = await gitToplevel(cwd);
898
+ const remoteResult = await run("git", ["-C", root, "remote", "get-url", "origin"]);
899
+ const remote = remoteResult.code === 0 ? remoteResult.stdout.trim() : "";
900
+ const commonResult = await run("git", ["-C", root, "rev-parse", "--git-common-dir"]);
901
+ const common = commonResult.stdout.trim();
902
+ let commonPath = root;
903
+ if (commonResult.code === 0 && common) {
904
+ const resolvedCommon = isAbsolute(common) ? common : join4(root, common);
905
+ commonPath = resolve(resolvedCommon);
906
+ }
907
+ const canonicalCommon = await canonicalPath(commonPath);
908
+ const identity = remote ? `remote:${normalizeRemote(remote)}` : `git-common-dir:${canonicalCommon}`;
909
+ const name = remote ? repositoryName(remote) : basename2(resolve(canonicalCommon, "..")) || basename2(root);
910
+ const readable = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
911
+ const digest = createHash("sha256").update(identity).digest("hex").slice(0, 12);
912
+ return `${readable}-${digest}`;
913
+ }
914
+ function storeGlobalRoot(homeDir = homedir3()) {
915
+ return join4(homeDir, ".difflab", "diffpi", "projects");
916
+ }
917
+ async function ensureStore(cwd, homeDir = homedir3()) {
918
+ const root = await gitToplevel(cwd);
919
+ const slug = await computeProjectSlug(root);
920
+ const dest = join4(storeGlobalRoot(homeDir), slug);
921
+ const link = join4(root, STORE_LINK);
922
+ await mkdir2(dest, { recursive: true });
923
+ try {
924
+ await assertStoreLink(link, dest);
925
+ } catch (error) {
926
+ if (error.code !== "ENOENT")
927
+ throw error;
928
+ await symlink(dest, link);
929
+ }
930
+ await removeLegacyStoreLink(join4(root, LEGACY_STORE_LINK), dest);
931
+ return { slug, root, dest, link, linked: true };
932
+ }
933
+ async function storeDir(cwd, homeDir = homedir3()) {
934
+ const store = await ensureStore(cwd, homeDir);
935
+ return store.dest;
936
+ }
937
+ async function reviewsDir(cwd, homeDir = homedir3()) {
938
+ const store = await ensureStore(cwd, homeDir);
939
+ const dir = join4(store.link, "reviews");
940
+ await mkdir2(dir, { recursive: true });
941
+ return dir;
942
+ }
943
+ async function sessionsDir(cwd, homeDir = homedir3()) {
944
+ const store = await ensureStore(cwd, homeDir);
945
+ const dir = join4(store.link, "sessions");
946
+ await mkdir2(dir, { recursive: true });
947
+ return dir;
948
+ }
949
+ async function assertStoreLink(path, dest) {
950
+ const entry = await lstat(path);
951
+ if (!entry.isSymbolicLink())
952
+ throw new Error(`${path} exists and is not a symlink.`);
953
+ const target = await symlinkTarget(path);
954
+ if (target !== await canonicalPath(dest))
955
+ throw new Error(`${path} points to ${target}, not ${dest}.`);
956
+ }
957
+ async function removeLegacyStoreLink(path, dest) {
958
+ try {
959
+ const entry = await lstat(path);
960
+ if (!entry.isSymbolicLink())
961
+ return;
962
+ const target = await symlinkTarget(path);
963
+ if (target === await canonicalPath(dest))
964
+ await unlink(path);
965
+ } catch (error) {
966
+ if (error.code !== "ENOENT")
967
+ throw error;
968
+ }
969
+ }
970
+ async function symlinkTarget(path) {
971
+ const target = await readlink(path);
972
+ return canonicalPath(isAbsolute(target) ? target : resolve(dirname2(path), target));
973
+ }
974
+ async function canonicalPath(path) {
975
+ try {
976
+ return await realpath(path);
977
+ } catch {
978
+ return resolve(path);
979
+ }
980
+ }
981
+ function normalizeRemote(remote) {
982
+ return remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "").toLowerCase();
983
+ }
984
+ function repositoryName(remote) {
985
+ const normalized = remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "");
986
+ return normalized.split(/[/\\:]/).filter(Boolean).at(-1) ?? "";
987
+ }
988
+ function basename2(path) {
989
+ const parts = resolve(path).split(/[/\\]/).filter(Boolean);
990
+ return parts.at(-1) ?? "";
991
+ }
992
+
993
+ // src/tuicr.ts
994
+ async function tuicrAvailable() {
995
+ const result = await run("tuicr", ["--version"]);
996
+ return result.code === 0;
997
+ }
998
+ async function listSessions(repo = ".") {
999
+ const result = await run("tuicr", ["review", "list", "--repo", repo]);
1000
+ if (result.code !== 0 || !result.stdout.trim())
1001
+ return [];
1002
+ let raw;
1003
+ try {
1004
+ raw = JSON.parse(result.stdout);
1005
+ } catch {
1006
+ return [];
1007
+ }
1008
+ return raw.map((entry) => ({
1009
+ slug: entry.slug,
1010
+ kind: entry.kind,
1011
+ path: entry.path,
1012
+ updatedAt: entry.updated_at,
1013
+ commentCount: entry.comment_count,
1014
+ anchor: entry.anchor,
1015
+ active: entry.active
1016
+ }));
1017
+ }
1018
+ async function resolveSession(cwd, branch) {
1019
+ const sessions = await listSessions(cwd);
1020
+ return findMatchingSession(sessions, cwd, branch);
1021
+ }
1022
+ async function resolveReviewSession(cwd, target) {
1023
+ if (!target.workingTree && target.owner && target.repo && target.number !== undefined) {
1024
+ return resolvePrSession(cwd, target.owner, target.repo, target.number);
1025
+ }
1026
+ return resolveSession(cwd, target.branch);
1027
+ }
1028
+ async function resolvePrSession(cwd, owner, repo, number) {
1029
+ const coordinate = `${owner}/${repo}`.toLowerCase();
1030
+ const sessions = await listSessions(cwd);
1031
+ return sessions.find((session) => {
1032
+ const slug = session.slug.toLowerCase();
1033
+ return session.kind === "pr" && slug.includes(coordinate) && (slug.endsWith(`/pr/${number}`) || slug.endsWith(`/mr/${number}`));
1034
+ });
1035
+ }
1036
+ async function findMatchingSession(sessions, cwd, branch) {
1037
+ const repository = await canonicalPath2(await gitToplevel(cwd));
1038
+ for (const session of sessions) {
1039
+ if (session.kind !== "local")
1040
+ continue;
1041
+ try {
1042
+ const data = await readSession(session.path);
1043
+ if (data.branch_name !== branch || !data.repo_path)
1044
+ continue;
1045
+ if (await canonicalPath2(data.repo_path) === repository)
1046
+ return session;
1047
+ } catch {}
1048
+ }
1049
+ return;
1050
+ }
1051
+ async function readSession(path) {
1052
+ const content = await readFile3(path, "utf8");
1053
+ try {
1054
+ return JSON.parse(content);
1055
+ } catch {
1056
+ throw new Error(`Cannot parse tuicr session JSON: ${path}`);
1057
+ }
1058
+ }
1059
+ async function addComment(session, body, opts = {}) {
1060
+ const args = ["review", "add", "--session", session, body];
1061
+ if (opts.targetFile)
1062
+ args.push("--target-file", opts.targetFile);
1063
+ if (opts.line !== undefined)
1064
+ args.push("--line", String(opts.line));
1065
+ if (opts.side)
1066
+ args.push("--side", opts.side);
1067
+ if (opts.username)
1068
+ args.push("--username", opts.username);
1069
+ await runChecked("tuicr", args);
1070
+ }
1071
+ async function launch(cwd, pr) {
1072
+ const command = pr === undefined ? ["tuicr", "-w"] : ["tuicr", "pr", String(pr)];
1073
+ return openInNewTab(command, { cwd, name: "tuicr" });
1074
+ }
1075
+ function toFindings(session, options = {}) {
1076
+ const comments = [];
1077
+ const include = (comment) => !options.agentOnly || commentAuthor(comment)?.startsWith("Agent: ");
1078
+ const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [comment.content] : []);
1079
+ for (const [file, entry] of Object.entries(session.files ?? {})) {
1080
+ const fileComments = (entry.file_comments ?? []).flatMap((comment) => include(comment) ? [comment.content] : []);
1081
+ if (fileComments.length > 0)
1082
+ bodyParts.push(`File: ${file}
1083
+
1084
+ ${fileComments.join(`
1085
+
1086
+ `)}`);
1087
+ for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
1088
+ const line = Number.parseInt(lineKey, 10);
1089
+ if (!Number.isFinite(line))
1090
+ continue;
1091
+ for (const lineComment of lineComments) {
1092
+ if (!include(lineComment))
1093
+ continue;
1094
+ const comment = {
1095
+ file,
1096
+ line,
1097
+ side: lineComment.side === "old" ? "LEFT" : "RIGHT",
1098
+ body: lineComment.content
1099
+ };
1100
+ const author = commentAuthor(lineComment);
1101
+ if (author)
1102
+ comment.author = author;
1103
+ comments.push(comment);
1104
+ }
1105
+ }
1106
+ }
1107
+ return {
1108
+ comments,
1109
+ body: bodyParts.join(`
1110
+
1111
+ `)
1112
+ };
1113
+ }
1114
+ function commentAuthor(comment) {
1115
+ return comment.username ?? comment.author;
1116
+ }
1117
+ async function canonicalPath2(path) {
1118
+ try {
1119
+ return await realpath2(path);
1120
+ } catch {
1121
+ return resolve2(path);
1122
+ }
1123
+ }
1124
+
1125
+ // src/review-backend.ts
1126
+ function createRemoteReviewBackend(vcs, number) {
1127
+ if (vcs.provider === "github")
1128
+ return new GithubReviewBackend(vcs, number);
1129
+ if (vcs.provider === "gitlab")
1130
+ return new GitlabReviewBackend(vcs, number);
1131
+ throw new Error("A remote review backend requires a GitHub or GitLab repository.");
1132
+ }
1133
+ function createLocalReviewBackend(options) {
1134
+ return new LocalReviewBackend(options);
1135
+ }
1136
+
1137
+ class LocalReviewBackend {
1138
+ options;
1139
+ kind = "local";
1140
+ constructor(options) {
1141
+ this.options = options;
1142
+ }
1143
+ async stage(draft) {
1144
+ for (const comment of draft.comments) {
1145
+ await addComment(this.options.session, comment.body, {
1146
+ targetFile: comment.file,
1147
+ line: comment.line,
1148
+ side: comment.side === "LEFT" ? "old" : "new",
1149
+ username: this.options.author
1150
+ });
1151
+ }
1152
+ if (draft.body.trim()) {
1153
+ await addComment(this.options.session, draft.body, { username: this.options.author });
1154
+ }
1155
+ }
1156
+ async readDraft() {
1157
+ return toFindings(await readSession(this.options.session), { agentOnly: true });
1158
+ }
1159
+ async listThreads() {
1160
+ try {
1161
+ return parseThreadArtifact(await readFile4(this.options.artifactPath, "utf8"));
1162
+ } catch (error) {
1163
+ if (error.code === "ENOENT")
1164
+ return [];
1165
+ throw error;
1166
+ }
1167
+ }
1168
+ async reply(input) {
1169
+ const content = await readFile4(this.options.artifactPath, "utf8");
1170
+ await writeFile2(this.options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question), "utf8");
1171
+ }
1172
+ async publish() {
1173
+ throw new Error("Promote a local draft through a remote review backend before publishing it.");
1174
+ }
1175
+ }
1176
+
1177
+ class GithubReviewBackend {
1178
+ vcs;
1179
+ number;
1180
+ kind = "remote";
1181
+ constructor(vcs, number) {
1182
+ this.vcs = vcs;
1183
+ this.number = number;
1184
+ }
1185
+ async stage(draft) {
1186
+ const pending = await this.pendingReview();
1187
+ if (!pending) {
1188
+ const payload = {
1189
+ body: draft.body,
1190
+ comments: draft.comments.map((comment) => ({
1191
+ path: comment.file,
1192
+ line: comment.line,
1193
+ side: comment.side ?? "RIGHT",
1194
+ body: comment.body
1195
+ }))
1196
+ };
1197
+ await runChecked("gh", [
1198
+ "api",
1199
+ "--method",
1200
+ "POST",
1201
+ `/repos/${this.vcs.owner}/${this.vcs.repo}/pulls/${this.number}/reviews`,
1202
+ "--input",
1203
+ "-"
1204
+ ], { input: JSON.stringify(payload) });
1205
+ return;
1206
+ }
1207
+ if (draft.body.trim()) {
1208
+ await runChecked("gh", [
1209
+ "api",
1210
+ "--method",
1211
+ "PUT",
1212
+ `/repos/${this.vcs.owner}/${this.vcs.repo}/pulls/${this.number}/reviews/${pending.id}`,
1213
+ "-f",
1214
+ `body=${draft.body}`
1215
+ ]);
1216
+ }
1217
+ for (const comment of draft.comments) {
1218
+ await runChecked("gh", [
1219
+ "api",
1220
+ "graphql",
1221
+ "-f",
1222
+ `query=${GITHUB_ADD_THREAD_MUTATION}`,
1223
+ "-f",
1224
+ `reviewId=${pending.nodeId}`,
1225
+ "-f",
1226
+ `body=${comment.body}`,
1227
+ "-f",
1228
+ `path=${comment.file}`,
1229
+ "-F",
1230
+ `line=${comment.line}`,
1231
+ "-f",
1232
+ `side=${comment.side ?? "RIGHT"}`
1233
+ ]);
1234
+ }
1235
+ }
1236
+ async readDraft() {
1237
+ const pending = await this.pendingReview();
1238
+ if (!pending)
1239
+ return { comments: [], body: "" };
1240
+ const review = await runChecked("gh", [
1241
+ "api",
1242
+ `/repos/${this.vcs.owner}/${this.vcs.repo}/pulls/${this.number}/reviews/${pending.id}`
1243
+ ]);
1244
+ const comments = await runChecked("gh", [
1245
+ "api",
1246
+ `/repos/${this.vcs.owner}/${this.vcs.repo}/pulls/${this.number}/reviews/${pending.id}/comments`
1247
+ ]);
1248
+ let reviewData;
1249
+ let commentData;
1250
+ try {
1251
+ reviewData = JSON.parse(review.stdout);
1252
+ commentData = JSON.parse(comments.stdout);
1253
+ } catch {
1254
+ throw new Error("Cannot parse the pending GitHub review as JSON.");
1255
+ }
1256
+ return {
1257
+ body: reviewData.body ?? "",
1258
+ comments: commentData.map((comment) => ({
1259
+ file: comment.path,
1260
+ line: comment.line ?? comment.original_line ?? 1,
1261
+ side: comment.side,
1262
+ body: comment.body
1263
+ }))
1264
+ };
1265
+ }
1266
+ async listThreads() {
1267
+ const threads = [];
1268
+ let cursor;
1269
+ do {
1270
+ const args = [
1271
+ "api",
1272
+ "graphql",
1273
+ "-f",
1274
+ `query=${GITHUB_THREADS_QUERY}`,
1275
+ "-f",
1276
+ `owner=${this.vcs.owner}`,
1277
+ "-f",
1278
+ `repo=${this.vcs.repo}`,
1279
+ "-F",
1280
+ `number=${this.number}`
1281
+ ];
1282
+ if (cursor)
1283
+ args.push("-f", `after=${cursor}`);
1284
+ const result = await runChecked("gh", args);
1285
+ let data;
1286
+ try {
1287
+ data = JSON.parse(result.stdout);
1288
+ } catch {
1289
+ throw new Error("Cannot parse GitHub review threads as JSON.");
1290
+ }
1291
+ const connection = data.data?.repository?.pullRequest?.reviewThreads;
1292
+ threads.push(...connection?.nodes ?? []);
1293
+ cursor = connection?.pageInfo?.hasNextPage ? connection.pageInfo.endCursor : undefined;
1294
+ if (connection?.pageInfo?.hasNextPage && !cursor) {
1295
+ throw new Error("GitHub review thread pagination did not return an end cursor.");
1296
+ }
1297
+ } while (cursor);
1298
+ return threads.map((thread) => {
1299
+ const nodes = thread.comments?.nodes ?? [];
1300
+ const comment = nodes[0];
1301
+ const body = comment?.body ?? "";
1302
+ return {
1303
+ id: thread.id,
1304
+ file: thread.path,
1305
+ line: thread.line,
1306
+ body,
1307
+ author: comment?.author?.login,
1308
+ resolved: thread.isResolved,
1309
+ question: /\?\s*$/.test(body.trim()),
1310
+ replies: nodes.slice(1).map((node) => node.body)
1311
+ };
1312
+ });
1313
+ }
1314
+ async reply(input) {
1315
+ const threads = await this.listThreads();
1316
+ const existing = threads.find((thread) => thread.id === input.threadId);
1317
+ if (!existing?.replies?.includes(input.body)) {
1318
+ await runChecked("gh", [
1319
+ "api",
1320
+ "graphql",
1321
+ "-f",
1322
+ `query=${GITHUB_REPLY_MUTATION}`,
1323
+ "-f",
1324
+ `threadId=${input.threadId}`,
1325
+ "-f",
1326
+ `body=${input.body}`
1327
+ ]);
1328
+ }
1329
+ if (input.resolve) {
1330
+ await runChecked("gh", [
1331
+ "api",
1332
+ "graphql",
1333
+ "-f",
1334
+ `query=${GITHUB_RESOLVE_MUTATION}`,
1335
+ "-f",
1336
+ `threadId=${input.threadId}`
1337
+ ]);
1338
+ }
1339
+ }
1340
+ async publish(event) {
1341
+ const pending = await this.pendingReview();
1342
+ if (!pending && event === "COMMENT")
1343
+ return;
1344
+ if (!pending && event === "REQUEST_CHANGES") {
1345
+ throw new Error("GitHub requires pending comments before publishing a request-changes review without a body.");
1346
+ }
1347
+ const endpoint = githubReviewSubmissionEndpoint(this.vcs.owner, this.vcs.repo, this.number, pending?.id ?? "");
1348
+ await runChecked("gh", ["api", "--method", "POST", endpoint, "-f", `event=${event}`]);
1349
+ }
1350
+ async pendingReview() {
1351
+ const result = await runChecked("gh", [
1352
+ "api",
1353
+ `/repos/${this.vcs.owner}/${this.vcs.repo}/pulls/${this.number}/reviews`,
1354
+ "--jq",
1355
+ '[.[] | select(.state=="PENDING")] | last | {id: (.id | tostring), nodeId: .node_id}'
1356
+ ]);
1357
+ if (!result.stdout.trim())
1358
+ return;
1359
+ let pending;
1360
+ try {
1361
+ pending = JSON.parse(result.stdout);
1362
+ } catch {
1363
+ throw new Error("Cannot parse the pending GitHub review identifier as JSON.");
1364
+ }
1365
+ if (!pending.id || !pending.nodeId)
1366
+ return;
1367
+ return { id: pending.id, nodeId: pending.nodeId };
1368
+ }
1369
+ }
1370
+
1371
+ class GitlabReviewBackend {
1372
+ vcs;
1373
+ number;
1374
+ kind = "remote";
1375
+ constructor(vcs, number) {
1376
+ this.vcs = vcs;
1377
+ this.number = number;
1378
+ }
1379
+ async stage(draft) {
1380
+ const endpoint = `${this.mergeRequestEndpoint()}/draft_notes`;
1381
+ if (draft.body.trim()) {
1382
+ await runChecked("glab", ["api", "--method", "POST", endpoint, "--input", "-"], {
1383
+ input: JSON.stringify({ note: draft.body })
1384
+ });
1385
+ }
1386
+ if (draft.comments.length === 0)
1387
+ return;
1388
+ const response = await runChecked("glab", ["api", this.mergeRequestEndpoint()]);
1389
+ const diffRefs = parseGitlabDiffRefs(response.stdout);
1390
+ for (const comment of draft.comments) {
1391
+ const payload = {
1392
+ note: comment.body,
1393
+ position: {
1394
+ ...diffRefs,
1395
+ position_type: "text",
1396
+ new_path: comment.file,
1397
+ old_path: comment.file,
1398
+ new_line: comment.side === "LEFT" ? undefined : comment.line,
1399
+ old_line: comment.side === "LEFT" ? comment.line : undefined
1400
+ }
1401
+ };
1402
+ await runChecked("glab", ["api", "--method", "POST", endpoint, "--input", "-"], {
1403
+ input: JSON.stringify(payload)
1404
+ });
1405
+ }
1406
+ }
1407
+ async readDraft() {
1408
+ const result = await runChecked("glab", ["api", `${this.mergeRequestEndpoint()}/draft_notes`]);
1409
+ let notes;
1410
+ try {
1411
+ notes = JSON.parse(result.stdout);
1412
+ } catch {
1413
+ throw new Error("Cannot parse GitLab draft notes as JSON.");
1414
+ }
1415
+ const comments = [];
1416
+ const body = [];
1417
+ for (const note of notes) {
1418
+ if (!note.note)
1419
+ continue;
1420
+ const file = note.position?.new_path ?? note.position?.old_path;
1421
+ const line = note.position?.new_line ?? note.position?.old_line;
1422
+ if (file && line) {
1423
+ comments.push({
1424
+ file,
1425
+ line,
1426
+ side: note.position?.new_line ? "RIGHT" : "LEFT",
1427
+ body: note.note
1428
+ });
1429
+ } else {
1430
+ body.push(note.note);
1431
+ }
1432
+ }
1433
+ return { comments, body: body.join(`
1434
+
1435
+ `) };
1436
+ }
1437
+ async listThreads() {
1438
+ const discussions = [];
1439
+ for (let page = 1;; page += 1) {
1440
+ const result = await runChecked("glab", [
1441
+ "api",
1442
+ `${this.mergeRequestEndpoint()}/discussions?per_page=100&page=${page}`
1443
+ ]);
1444
+ let batch;
1445
+ try {
1446
+ batch = JSON.parse(result.stdout);
1447
+ } catch {
1448
+ throw new Error("Cannot parse GitLab review discussions as JSON.");
1449
+ }
1450
+ discussions.push(...batch);
1451
+ if (batch.length < 100)
1452
+ break;
1453
+ }
1454
+ return discussions.map((discussion) => {
1455
+ const notes = discussion.notes ?? [];
1456
+ const note = notes[0];
1457
+ const body = note?.body ?? "";
1458
+ return {
1459
+ id: discussion.id,
1460
+ file: note?.position?.new_path ?? note?.position?.old_path,
1461
+ line: note?.position?.new_line ?? note?.position?.old_line,
1462
+ body,
1463
+ author: note?.author?.username,
1464
+ resolved: Boolean(discussion.resolved),
1465
+ question: /\?\s*$/.test(body.trim()),
1466
+ replies: notes.slice(1).map((reply) => reply.body)
1467
+ };
1468
+ });
1469
+ }
1470
+ async reply(input) {
1471
+ const endpoint = `${this.mergeRequestEndpoint()}/discussions/${encodeURIComponent(input.threadId)}`;
1472
+ const threads = await this.listThreads();
1473
+ const existing = threads.find((thread) => thread.id === input.threadId);
1474
+ if (!existing?.replies?.includes(input.body)) {
1475
+ await runChecked("glab", ["api", "--method", "POST", `${endpoint}/notes`, "--input", "-"], {
1476
+ input: JSON.stringify({ body: input.body })
1477
+ });
1478
+ }
1479
+ if (input.resolve)
1480
+ await runChecked("glab", ["api", "--method", "PUT", `${endpoint}?resolved=true`]);
1481
+ }
1482
+ async publish(event) {
1483
+ assertReviewEventSupported(this.vcs.provider, event);
1484
+ const drafts = await runChecked("glab", ["api", `${this.mergeRequestEndpoint()}/draft_notes`]);
1485
+ if (hasGitlabDraftNotes(drafts.stdout)) {
1486
+ await runChecked("glab", ["api", "--method", "POST", `${this.mergeRequestEndpoint()}/draft_notes/bulk_publish`]);
1487
+ }
1488
+ if (event === "APPROVE") {
1489
+ await runChecked("glab", ["mr", "approve", String(this.number), "--repo", this.project()]);
1490
+ }
1491
+ }
1492
+ project() {
1493
+ return `${this.vcs.owner}/${this.vcs.repo}`;
1494
+ }
1495
+ mergeRequestEndpoint() {
1496
+ return `projects/${encodeURIComponent(this.project())}/merge_requests/${this.number}`;
1497
+ }
1498
+ }
1499
+ function assertReviewEventSupported(provider, event) {
1500
+ if (provider === "gitlab" && event === "REQUEST_CHANGES") {
1501
+ throw new Error("GitLab does not support REQUEST_CHANGES reviews; post a comment or reject the merge request manually.");
1502
+ }
1503
+ }
1504
+ function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
1505
+ return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
1506
+ }
1507
+ function parseGitlabDiffRefs(input) {
1508
+ let data;
1509
+ try {
1510
+ data = JSON.parse(input);
1511
+ } catch {
1512
+ throw new Error("Cannot create positioned GitLab draft notes: the merge request response was not valid JSON.");
1513
+ }
1514
+ const { base_sha, start_sha, head_sha } = data.diff_refs ?? {};
1515
+ if (!base_sha || !start_sha || !head_sha) {
1516
+ throw new Error("Cannot create positioned GitLab draft notes: merge request diff refs are unavailable.");
1517
+ }
1518
+ return { base_sha, start_sha, head_sha };
1519
+ }
1520
+ function hasGitlabDraftNotes(input) {
1521
+ try {
1522
+ const data = JSON.parse(input);
1523
+ return Array.isArray(data) && data.length > 0;
1524
+ } catch {
1525
+ throw new Error("Cannot publish the GitLab review: the draft notes response was not valid JSON.");
1526
+ }
1527
+ }
1528
+ var GITHUB_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){nodes{id,isResolved,path,line,comments(first:100){nodes{body,author{login}}}}pageInfo{hasNextPage,endCursor}}}}}`;
1529
+ var GITHUB_ADD_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!,$line:Int!,$side:DiffSide!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,line:$line,side:$side}){thread{id}}}`;
1530
+ var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
1531
+ var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
1
1532
  // src/mcp.ts
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
1533
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
1534
+ import { homedir as homedir4 } from "node:os";
1535
+ import { dirname as dirname3, join as join5 } from "node:path";
5
1536
  var mcp = {
6
- globalConfigPath(homeDir = homedir()) {
7
- return join(homeDir, ".config", "mcp", "mcp.json");
1537
+ globalConfigPath(homeDir = homedir4()) {
1538
+ return join5(homeDir, ".config", "mcp", "mcp.json");
8
1539
  },
9
1540
  async serversEnsure(servers, options = {}) {
10
1541
  const path = options.path ?? mcp.globalConfigPath();
@@ -17,8 +1548,8 @@ var mcp = {
17
1548
  const next = { ...current, mcpServers: nextServers };
18
1549
  const changed = JSON.stringify(current) !== JSON.stringify(next);
19
1550
  if (changed && !options.dryRun) {
20
- await mkdir(dirname(path), { recursive: true });
21
- await writeFile(path, `${JSON.stringify(next, null, 2)}
1551
+ await mkdir3(dirname3(path), { recursive: true });
1552
+ await writeFile3(path, `${JSON.stringify(next, null, 2)}
22
1553
  `, "utf8");
23
1554
  }
24
1555
  return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
@@ -47,7 +1578,7 @@ function getParsedConfig(content, path) {
47
1578
  }
48
1579
  async function getOptionalFile(path) {
49
1580
  try {
50
- return await readFile(path, "utf8");
1581
+ return await readFile5(path, "utf8");
51
1582
  } catch (error) {
52
1583
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
53
1584
  return;
@@ -58,68 +1589,9 @@ function isRecord(value) {
58
1589
  return value !== null && typeof value === "object" && !Array.isArray(value);
59
1590
  }
60
1591
  // src/mise.ts
61
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
62
- import { homedir as homedir2 } from "node:os";
63
- import { basename, dirname as dirname2, join as join3 } from "node:path";
64
-
65
- // src/process.ts
66
- import { constants } from "node:fs";
67
- import { access } from "node:fs/promises";
68
- import { delimiter, join as join2 } from "node:path";
69
- import { spawn } from "node:child_process";
70
- var MAX_CAPTURED_OUTPUT_LENGTH = 65536;
71
- async function findExecutable(name) {
72
- if (name.includes("/")) {
73
- try {
74
- await access(name, constants.X_OK);
75
- return name;
76
- } catch {
77
- return;
78
- }
79
- }
80
- for (const directory of (process.env.PATH ?? "").split(delimiter)) {
81
- if (!directory)
82
- continue;
83
- const candidate = join2(directory, name);
84
- try {
85
- await access(candidate, constants.X_OK);
86
- return candidate;
87
- } catch {}
88
- }
89
- return;
90
- }
91
- function run(command, args, options = {}) {
92
- return new Promise((resolve, reject) => {
93
- const child = spawn(command, args, {
94
- cwd: options.cwd,
95
- env: options.env ?? process.env,
96
- stdio: ["ignore", "pipe", "pipe"]
97
- });
98
- let stdout = "";
99
- let stderr = "";
100
- child.stdout.on("data", (chunk) => {
101
- stdout = appendBounded(stdout, chunk.toString());
102
- });
103
- child.stderr.on("data", (chunk) => {
104
- stderr = appendBounded(stderr, chunk.toString());
105
- });
106
- child.on("error", reject);
107
- child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
108
- });
109
- }
110
- async function runChecked(command, args, options = {}) {
111
- const result = await run(command, args, options);
112
- if (result.code === 0)
113
- return result;
114
- const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
115
- throw new Error(`${command} ${args.join(" ")} failed: ${detail}`);
116
- }
117
- function appendBounded(current, next) {
118
- const combined = current + next;
119
- return combined.length <= MAX_CAPTURED_OUTPUT_LENGTH ? combined : combined.slice(-MAX_CAPTURED_OUTPUT_LENGTH);
120
- }
121
-
122
- // src/mise.ts
1592
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
1593
+ import { homedir as homedir5 } from "node:os";
1594
+ import { basename as basename3, dirname as dirname4, join as join6 } from "node:path";
123
1595
  var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
124
1596
  var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
125
1597
  var mise = {
@@ -127,11 +1599,11 @@ var mise = {
127
1599
  return findExecutable(name);
128
1600
  },
129
1601
  async install(options = {}) {
130
- const homeDir = options.homeDir ?? homedir2();
1602
+ const homeDir = options.homeDir ?? homedir5();
131
1603
  const platform = options.platform ?? process.platform;
132
1604
  if (platform === "win32")
133
1605
  throw new Error("Automatic mise installation supports macOS and Linux only.");
134
- const installedPath = join3(homeDir, ".local", "bin", "mise");
1606
+ const installedPath = join6(homeDir, ".local", "bin", "mise");
135
1607
  if (options.dryRun)
136
1608
  return installedPath;
137
1609
  await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
@@ -141,8 +1613,8 @@ var mise = {
141
1613
  return executable;
142
1614
  },
143
1615
  async hookEnsure(executable, options = {}) {
144
- const homeDir = options.homeDir ?? homedir2();
145
- const hook = getShellHook(basename(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
1616
+ const homeDir = options.homeDir ?? homedir5();
1617
+ const hook = getShellHook(basename3(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
146
1618
  const current = await getOptionalFile2(hook.path);
147
1619
  if (current.includes(MISE_HOOK_START))
148
1620
  return { path: hook.path, changed: false, planned: false };
@@ -151,8 +1623,8 @@ var mise = {
151
1623
  const separator = current.length === 0 || current.endsWith(`
152
1624
  `) ? "" : `
153
1625
  `;
154
- await mkdir2(dirname2(hook.path), { recursive: true });
155
- await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
1626
+ await mkdir4(dirname4(hook.path), { recursive: true });
1627
+ await writeFile4(hook.path, `${current}${separator}${hook.content}`, "utf8");
156
1628
  return { path: hook.path, changed: true, planned: false };
157
1629
  },
158
1630
  async toolCheckGlobal(executable, tool, minimumVersion) {
@@ -169,7 +1641,7 @@ var mise = {
169
1641
  async toolInstallLocal(executable, specification, cwd = process.cwd()) {
170
1642
  await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
171
1643
  },
172
- async toolUpdateAllGlobal(executable, homeDir = homedir2()) {
1644
+ async toolUpdateAllGlobal(executable, homeDir = homedir5()) {
173
1645
  await runChecked(executable, ["upgrade"], { cwd: homeDir });
174
1646
  }
175
1647
  };
@@ -178,7 +1650,7 @@ function getShellHook(shell, executable, homeDir) {
178
1650
  switch (shell.toLowerCase()) {
179
1651
  case "zsh":
180
1652
  return {
181
- path: join3(homeDir, ".zshrc"),
1653
+ path: join6(homeDir, ".zshrc"),
182
1654
  content: `${MISE_HOOK_START}
183
1655
  eval "$(${command} activate zsh)"
184
1656
  ${MISE_HOOK_END}
@@ -186,7 +1658,7 @@ ${MISE_HOOK_END}
186
1658
  };
187
1659
  case "fish":
188
1660
  return {
189
- path: join3(homeDir, ".config", "fish", "config.fish"),
1661
+ path: join6(homeDir, ".config", "fish", "config.fish"),
190
1662
  content: `${MISE_HOOK_START}
191
1663
  ${command} activate fish | source
192
1664
  ${MISE_HOOK_END}
@@ -195,7 +1667,7 @@ ${MISE_HOOK_END}
195
1667
  case "nu":
196
1668
  case "nushell":
197
1669
  return {
198
- path: join3(homeDir, ".config", "nushell", "config.nu"),
1670
+ path: join6(homeDir, ".config", "nushell", "config.nu"),
199
1671
  content: `${MISE_HOOK_START}
200
1672
  let mise_bin = ${command}
201
1673
  let mise_path = $nu.default-config-dir | path join mise.nu
@@ -206,7 +1678,7 @@ ${MISE_HOOK_END}
206
1678
  };
207
1679
  case "xonsh":
208
1680
  return {
209
- path: join3(homeDir, ".xonshrc"),
1681
+ path: join6(homeDir, ".xonshrc"),
210
1682
  content: `${MISE_HOOK_START}
211
1683
  execx($(${command} activate xonsh))
212
1684
  ${MISE_HOOK_END}
@@ -214,7 +1686,7 @@ ${MISE_HOOK_END}
214
1686
  };
215
1687
  case "elvish":
216
1688
  return {
217
- path: join3(homeDir, ".config", "elvish", "rc.elv"),
1689
+ path: join6(homeDir, ".config", "elvish", "rc.elv"),
218
1690
  content: `${MISE_HOOK_START}
219
1691
  var mise: = (ns [&])
220
1692
  eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
@@ -225,7 +1697,7 @@ ${MISE_HOOK_END}
225
1697
  case "pwsh":
226
1698
  case "powershell":
227
1699
  return {
228
- path: join3(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
1700
+ path: join6(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
229
1701
  content: `${MISE_HOOK_START}
230
1702
  (& ${command} activate pwsh) | Out-String | Invoke-Expression
231
1703
  ${MISE_HOOK_END}
@@ -234,7 +1706,7 @@ ${MISE_HOOK_END}
234
1706
  case "bash":
235
1707
  default:
236
1708
  return {
237
- path: join3(homeDir, ".bashrc"),
1709
+ path: join6(homeDir, ".bashrc"),
238
1710
  content: `${MISE_HOOK_START}
239
1711
  eval "$(${command} activate bash)"
240
1712
  ${MISE_HOOK_END}
@@ -273,7 +1745,7 @@ function isVersionAtLeast(version, minimumVersion) {
273
1745
  }
274
1746
  async function getOptionalFile2(path) {
275
1747
  try {
276
- return await readFile2(path, "utf8");
1748
+ return await readFile6(path, "utf8");
277
1749
  } catch (error) {
278
1750
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
279
1751
  return "";
@@ -283,80 +1755,356 @@ async function getOptionalFile2(path) {
283
1755
  function getShellQuoted(value) {
284
1756
  return `'${value.replaceAll("'", "'\\''")}'`;
285
1757
  }
286
- // src/pi.ts
287
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
288
- import { homedir as homedir3 } from "node:os";
289
- import { dirname as dirname3, join as join4 } from "node:path";
290
- var pi = {
291
- async executableCheck() {
292
- return findExecutable("pi");
293
- },
294
- async packageList(executable) {
295
- return (await runChecked(executable, ["list"])).stdout;
296
- },
297
- packageCheck(listOutput, source) {
298
- if (listOutput.includes(source))
299
- return true;
300
- return source.startsWith("https://") && listOutput.includes(source.slice("https://".length));
301
- },
302
- async packageInstall(executable, source) {
303
- await runChecked(executable, ["install", source]);
304
- },
305
- agentDir(homeDir = homedir3()) {
306
- return getAgentDir(homeDir);
307
- },
308
- async skillCheckGlobal(name, agentDir = getAgentDir(), sharedSkillsDir = join4(homedir3(), ".agents", "skills")) {
309
- const roots = [join4(agentDir, "skills"), sharedSkillsDir];
310
- for (const root of roots) {
311
- if (await getOptionalFile3(join4(root, name, "SKILL.md")) !== undefined)
312
- return true;
1758
+ // src/modes.ts
1759
+ import {
1760
+ getAgentDir,
1761
+ parseFrontmatter as parseFrontmatter2
1762
+ } from "@earendil-works/pi-coding-agent";
1763
+ import { readFile as readFile7 } from "node:fs/promises";
1764
+ import { homedir as homedir6 } from "node:os";
1765
+ import { basename as basename4, extname, join as join8 } from "node:path";
1766
+
1767
+ // src/assets.ts
1768
+ import { existsSync } from "node:fs";
1769
+ import { dirname as dirname5, join as join7 } from "node:path";
1770
+ import { fileURLToPath } from "node:url";
1771
+ function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
1772
+ const moduleDir = dirname5(fileURLToPath(moduleUrl));
1773
+ const candidates = [join7(moduleDir, name), join7(moduleDir, "..", name), join7(moduleDir, "..", "..", name)];
1774
+ return candidates.find((path) => existsSync(path)) ?? candidates[1];
1775
+ }
1776
+ function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
1777
+ return resolveBundledAssetDir("agents", moduleUrl);
1778
+ }
1779
+ function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
1780
+ return resolveBundledAssetDir("templates", moduleUrl);
1781
+ }
1782
+
1783
+ // src/modes.ts
1784
+ async function discoverAgentModes(options) {
1785
+ const agentDir = options.agentDir ?? getAgentDir();
1786
+ const homeDir = options.homeDir ?? homedir6();
1787
+ const modes = new Map;
1788
+ const diagnostics = [];
1789
+ await loadAgentModes(options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR, "diffpi agent", modes, diagnostics);
1790
+ await loadAgentModes(join8(agentDir, "agents"), "user agent", modes, diagnostics);
1791
+ if (options.includeSkills) {
1792
+ await loadSkillModes(join8(homeDir, ".agents", "skills"), "user skill", modes, diagnostics);
1793
+ await loadSkillModes(join8(agentDir, "skills"), "pi user skill", modes, diagnostics);
1794
+ }
1795
+ if (options.projectTrusted === true) {
1796
+ if (options.includeSkills) {
1797
+ await loadSkillModes(join8(options.cwd, ".agents", "skills"), "project skill", modes, diagnostics);
1798
+ await loadSkillModes(join8(options.cwd, ".pi", "skills"), "pi project skill", modes, diagnostics);
313
1799
  }
314
- return false;
315
- },
316
- async skillInstallGlobal(miseExecutable, source, names) {
317
- const selection = names.flatMap((name) => ["--skill", name]);
318
- await runChecked(miseExecutable, [
319
- "x",
320
- "node@22",
321
- "--",
322
- "npx",
323
- "-y",
324
- "skills",
325
- "add",
326
- source,
327
- ...selection,
328
- "--global",
329
- "--agent",
330
- "pi",
331
- "--yes"
332
- ]);
333
- },
334
- async configEnsure(path, update, dryRun = false) {
335
- const currentText = await getOptionalFile3(path);
336
- const current = getParsedObject(currentText, path);
337
- const next = update(current);
338
- const changed = JSON.stringify(current) !== JSON.stringify(next);
339
- if (changed && !dryRun) {
340
- await mkdir3(dirname3(path), { recursive: true });
341
- await writeFile3(path, `${JSON.stringify(next, null, 2)}
342
- `, "utf8");
1800
+ await loadAgentModes(join8(options.cwd, ".agents", "agents"), "project agent", modes, diagnostics);
1801
+ await loadAgentModes(join8(options.cwd, ".pi", "agents"), "pi project agent", modes, diagnostics);
1802
+ }
1803
+ const userConfig = await loadDiffpiConfig({ homeDir });
1804
+ const configuredModes = [...modes.values()].map((mode) => ({
1805
+ ...mode,
1806
+ modelPreferences: resolveAgentModelPreferences(mode.id, mode.modelPreferences, userConfig.config)
1807
+ }));
1808
+ return {
1809
+ modes: configuredModes.sort((left, right) => left.id.localeCompare(right.id)),
1810
+ diagnostics
1811
+ };
1812
+ }
1813
+ function resolveAgentMode(modes, requested) {
1814
+ const name = requested.trim();
1815
+ if (!name)
1816
+ return { ok: false, message: "Agent name is required." };
1817
+ const exact = modes.find((mode) => mode.id === name);
1818
+ if (exact)
1819
+ return { ok: true, active: exact, message: `Active inline agent: ${exact.id}.` };
1820
+ const lowerName = name.toLowerCase();
1821
+ const matches = modes.filter((mode) => mode.id.toLowerCase() === lowerName);
1822
+ if (matches.length === 1) {
1823
+ const active = matches[0];
1824
+ return { ok: true, active, message: `Active inline agent: ${active.id}.` };
1825
+ }
1826
+ if (matches.length > 1) {
1827
+ return {
1828
+ ok: false,
1829
+ message: `Inline agent "${name}" is ambiguous. Use one of: ${matches.map((mode) => mode.id).join(", ")}.`
1830
+ };
1831
+ }
1832
+ return { ok: false, message: `Unknown inline agent "${name}". Run /skill:mode or diffpi_modes_list.` };
1833
+ }
1834
+ function createModeController(pi, options = {}) {
1835
+ let active;
1836
+ let baseline;
1837
+ const updateStatus = (ctx) => {
1838
+ ctx.ui.setStatus(MODE_STATUS_KEY, active ? `mode: ${active.id}` : undefined);
1839
+ };
1840
+ const list = (ctx, listOptions = {}) => discoverAgentModes({
1841
+ cwd: ctx.cwd,
1842
+ agentDir: options.agentDir,
1843
+ bundledAgentsDir: options.bundledAgentsDir,
1844
+ homeDir: options.homeDir,
1845
+ projectTrusted: ctx.isProjectTrusted(),
1846
+ includeSkills: listOptions.includeSkills
1847
+ });
1848
+ return {
1849
+ list,
1850
+ async set(agent, ctx) {
1851
+ const catalog = await list(ctx, { includeSkills: agent.includes(":") });
1852
+ const result = resolveAgentMode(catalog.modes, agent);
1853
+ if (!result.ok || !result.active)
1854
+ return result;
1855
+ baseline ??= captureRuntime(pi, ctx);
1856
+ if (active && baseline)
1857
+ await restoreRuntime(pi, baseline, ctx);
1858
+ active = result.active;
1859
+ const runtimeMessage = await applyModeRuntime(pi, active, ctx);
1860
+ pi.appendEntry(MODE_STATE_ENTRY, { active, baseline });
1861
+ updateStatus(ctx);
1862
+ return { ...result, message: `${result.message} ${runtimeMessage}` };
1863
+ },
1864
+ async unset(ctx) {
1865
+ if (!active)
1866
+ return { ok: true, message: "Inline agent is already clear." };
1867
+ if (baseline)
1868
+ await restoreRuntime(pi, baseline, ctx);
1869
+ active = undefined;
1870
+ pi.appendEntry(MODE_STATE_ENTRY, { active: null });
1871
+ baseline = undefined;
1872
+ updateStatus(ctx);
1873
+ return { ok: true, message: "Inline agent cleared. The previous model, thinking, tools, and prompt resume." };
1874
+ },
1875
+ async restore(ctx) {
1876
+ const previousActive = active;
1877
+ const previousBaseline = baseline;
1878
+ const entry = [...ctx.sessionManager.getBranch()].reverse().find((candidate) => candidate.type === "custom" && candidate.customType === MODE_STATE_ENTRY);
1879
+ const restored = entry?.data?.active;
1880
+ const restoredBaseline = entry?.data?.baseline;
1881
+ if (isAgentModeSnapshot(restored)) {
1882
+ active = restored;
1883
+ baseline = isModeBaseline(restoredBaseline) ? restoredBaseline : previousBaseline;
1884
+ await applyModeRuntime(pi, active, ctx);
1885
+ } else {
1886
+ if (previousActive && previousBaseline)
1887
+ pi.setActiveTools(previousBaseline.tools);
1888
+ active = undefined;
1889
+ baseline = undefined;
1890
+ }
1891
+ updateStatus(ctx);
1892
+ },
1893
+ apply(systemPrompt) {
1894
+ if (!active)
1895
+ return systemPrompt;
1896
+ if (active.promptStrategy === "replace")
1897
+ return active.systemPrompt;
1898
+ return `${systemPrompt}
1899
+
1900
+ ## Active inline agent: ${active.label}
1901
+
1902
+ ${active.systemPrompt}`;
1903
+ },
1904
+ getActive() {
1905
+ return active;
1906
+ }
1907
+ };
1908
+ }
1909
+ var MODE_STATE_ENTRY = "diffpi-mode-state";
1910
+ var MODE_STATUS_KEY = "diffpi-mode";
1911
+ var MODE_CONTROL_TOOLS = ["ask_user_question", "diffpi_modes_list", "diffpi_modes_set", "diffpi_modes_unset"];
1912
+ var BUNDLED_AGENTS_DIR = resolveBundledAgentsDir();
1913
+ var THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
1914
+ async function applyModeRuntime(pi, mode, ctx) {
1915
+ let selectedModel;
1916
+ if (mode.modelPreferences.length > 0) {
1917
+ const scoped = ctx.scopedModels.length > 0 ? ctx.scopedModels.map((entry) => entry.model) : undefined;
1918
+ const availableModels = scoped ?? ctx.modelRegistry.getAvailable();
1919
+ for (const preference of mode.modelPreferences) {
1920
+ const model = findPreferredModel(availableModels, preference);
1921
+ if (model && await pi.setModel(model)) {
1922
+ selectedModel = `${model.provider}/${model.id}`;
1923
+ break;
1924
+ }
1925
+ }
1926
+ }
1927
+ if (mode.thinkingLevel)
1928
+ pi.setThinkingLevel(mode.thinkingLevel);
1929
+ if (mode.tools.length > 0) {
1930
+ const availableTools = new Set(pi.getAllTools().map((tool) => tool.name));
1931
+ const selectedTools = [...new Set([...mode.tools, ...MODE_CONTROL_TOOLS])].filter((tool) => availableTools.has(tool));
1932
+ if (selectedTools.length > 0)
1933
+ pi.setActiveTools(selectedTools);
1934
+ }
1935
+ const parts = [];
1936
+ if (mode.modelPreferences.length > 0) {
1937
+ parts.push(selectedModel ? `Model: ${selectedModel}.` : "No preferred model was available; kept the current model.");
1938
+ }
1939
+ if (mode.thinkingLevel)
1940
+ parts.push(`Thinking: ${mode.thinkingLevel}.`);
1941
+ if (mode.tools.length > 0)
1942
+ parts.push("Applied the profile tool set.");
1943
+ return parts.join(" ") || "The profile changes the prompt only.";
1944
+ }
1945
+ async function restoreRuntime(pi, state, ctx) {
1946
+ if (state.model) {
1947
+ const model = ctx.modelRegistry.find(state.model.provider, state.model.id);
1948
+ if (model)
1949
+ await pi.setModel(model);
1950
+ }
1951
+ pi.setThinkingLevel(state.thinkingLevel);
1952
+ pi.setActiveTools(state.tools);
1953
+ }
1954
+ function captureRuntime(pi, ctx) {
1955
+ return {
1956
+ model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
1957
+ thinkingLevel: pi.getThinkingLevel(),
1958
+ tools: pi.getActiveTools()
1959
+ };
1960
+ }
1961
+ async function loadSkillModes(skillsDir, source, modes, diagnostics) {
1962
+ const entries = await readDirectoryIfExists(skillsDir);
1963
+ for (const entry of entries.filter((item) => item.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
1964
+ await loadAgentModes(join8(skillsDir, entry.name, "agents"), `${source} ${entry.name}`, modes, diagnostics, entry.name);
1965
+ }
1966
+ }
1967
+ async function loadAgentModes(directory, source, modes, diagnostics, skillName) {
1968
+ const entries = await readDirectoryIfExists(directory);
1969
+ for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
1970
+ const path = join8(directory, entry.name);
1971
+ try {
1972
+ const content = await readFile7(path, "utf8");
1973
+ const { frontmatter, body } = parseFrontmatter2(content.startsWith("\uFEFF") ? content.slice(1) : content);
1974
+ if (frontmatter.enabled === false || frontmatter.inline === false)
1975
+ continue;
1976
+ const name = getFrontmatterText(frontmatter.name) ?? basename4(path, extname(path));
1977
+ const systemPrompt = body.trim();
1978
+ if (!name || name.includes(":") || !systemPrompt) {
1979
+ diagnostics.push(`Skipped ${path}: agent name must not contain ":" and prompt body is required.`);
1980
+ continue;
1981
+ }
1982
+ const id = skillName ? `${skillName}:${name}` : name;
1983
+ modes.set(id, {
1984
+ id,
1985
+ label: getFrontmatterText(frontmatter.display_name) ?? name,
1986
+ description: getFrontmatterText(frontmatter.description) ?? `Inline agent from ${basename4(path)}`,
1987
+ systemPrompt,
1988
+ promptStrategy: frontmatter.prompt_mode === "append" ? "append" : "replace",
1989
+ modelPreferences: [
1990
+ ...getFrontmatterList(frontmatter.model),
1991
+ ...getFrontmatterList(frontmatter.model_fallbacks)
1992
+ ],
1993
+ thinkingLevel: getThinkingLevel(frontmatter.thinking),
1994
+ tools: getFrontmatterList(frontmatter.tools),
1995
+ source,
1996
+ sourcePath: path
1997
+ });
1998
+ } catch (error) {
1999
+ diagnostics.push(`Skipped ${path}: ${error instanceof Error ? error.message : String(error)}`);
343
2000
  }
344
- return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
345
2001
  }
2002
+ }
2003
+ function getFrontmatterText(value) {
2004
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
2005
+ }
2006
+ function getFrontmatterList(value) {
2007
+ const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
2008
+ return values.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
2009
+ }
2010
+ function getThinkingLevel(value) {
2011
+ const level = getFrontmatterText(value);
2012
+ return level && THINKING_LEVELS.has(level) ? level : undefined;
2013
+ }
2014
+ function isAgentModeSnapshot(value) {
2015
+ if (!value || typeof value !== "object")
2016
+ return false;
2017
+ const candidate = value;
2018
+ return typeof candidate.id === "string" && typeof candidate.label === "string" && typeof candidate.description === "string" && typeof candidate.systemPrompt === "string" && (candidate.promptStrategy === "append" || candidate.promptStrategy === "replace") && Array.isArray(candidate.modelPreferences) && (candidate.thinkingLevel === undefined || THINKING_LEVELS.has(candidate.thinkingLevel)) && Array.isArray(candidate.tools) && typeof candidate.source === "string" && typeof candidate.sourcePath === "string";
2019
+ }
2020
+ function isModeBaseline(value) {
2021
+ if (!value || typeof value !== "object")
2022
+ return false;
2023
+ const candidate = value;
2024
+ const model = candidate.model;
2025
+ return (model === undefined || typeof model.provider === "string" && typeof model.id === "string") && candidate.thinkingLevel !== undefined && THINKING_LEVELS.has(candidate.thinkingLevel) && Array.isArray(candidate.tools) && candidate.tools.every((tool) => typeof tool === "string");
2026
+ }
2027
+ // src/pi.ts
2028
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
2029
+ import { homedir as homedir7 } from "node:os";
2030
+ import { dirname as dirname6, join as join9 } from "node:path";
2031
+ var pi = {
2032
+ executableCheck: findPiExecutable,
2033
+ packageList: listPiPackages,
2034
+ packageCheck: hasPiPackage,
2035
+ packageInstall: installPiPackage,
2036
+ agentDir: resolvePiAgentDir,
2037
+ agentEnsure: ensurePiAgent,
2038
+ skillCheckGlobal: checkGlobalPiSkill,
2039
+ skillInstallGlobal: installGlobalPiSkills,
2040
+ configEnsure: ensurePiConfig
346
2041
  };
347
- function getAgentDir(homeDir = homedir3()) {
348
- return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join4(process.env.XDG_CONFIG_HOME, "pi") : join4(homeDir, ".pi", "agent"));
2042
+ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
2043
+ const path = join9(agentDir, "agents", filename);
2044
+ const currentText = await readTextIfExists(path);
2045
+ const changed = currentText !== content;
2046
+ if (changed && !dryRun) {
2047
+ await mkdir5(dirname6(path), { recursive: true });
2048
+ await writeFile5(path, content, "utf8");
2049
+ }
2050
+ return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
349
2051
  }
350
- async function getOptionalFile3(path) {
351
- try {
352
- return await readFile3(path, "utf8");
353
- } catch (error) {
354
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
355
- return;
356
- throw error;
2052
+ async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join9(homedir7(), ".agents", "skills")) {
2053
+ const roots = [join9(agentDir, "skills"), sharedSkillsDir];
2054
+ for (const root of roots) {
2055
+ if (await readTextIfExists(join9(root, name, "SKILL.md")) !== undefined)
2056
+ return true;
2057
+ }
2058
+ return false;
2059
+ }
2060
+ async function installGlobalPiSkills(miseExecutable, source, names) {
2061
+ const selection = names.flatMap((name) => ["--skill", name]);
2062
+ await runChecked(miseExecutable, [
2063
+ "x",
2064
+ "node@22",
2065
+ "--",
2066
+ "npx",
2067
+ "-y",
2068
+ "skills",
2069
+ "add",
2070
+ source,
2071
+ ...selection,
2072
+ "--global",
2073
+ "--agent",
2074
+ "pi",
2075
+ "--yes"
2076
+ ]);
2077
+ }
2078
+ async function ensurePiConfig(path, update, dryRun = false) {
2079
+ const currentText = await readTextIfExists(path);
2080
+ const current = parseJsonObject(currentText, path);
2081
+ const next = update(current);
2082
+ const changed = JSON.stringify(current) !== JSON.stringify(next);
2083
+ if (changed && !dryRun) {
2084
+ await mkdir5(dirname6(path), { recursive: true });
2085
+ await writeFile5(path, `${JSON.stringify(next, null, 2)}
2086
+ `, "utf8");
357
2087
  }
2088
+ return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
2089
+ }
2090
+ async function findPiExecutable() {
2091
+ return findExecutable("pi");
2092
+ }
2093
+ async function listPiPackages(executable) {
2094
+ return (await runChecked(executable, ["list"])).stdout;
2095
+ }
2096
+ function hasPiPackage(listOutput, source) {
2097
+ if (listOutput.includes(source))
2098
+ return true;
2099
+ return source.startsWith("https://") && listOutput.includes(source.slice("https://".length));
2100
+ }
2101
+ async function installPiPackage(executable, source) {
2102
+ await runChecked(executable, ["install", source]);
2103
+ }
2104
+ function resolvePiAgentDir(homeDir = homedir7()) {
2105
+ return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join9(process.env.XDG_CONFIG_HOME, "pi") : join9(homeDir, ".pi", "agent"));
358
2106
  }
359
- function getParsedObject(content, path) {
2107
+ function parseJsonObject(content, path) {
360
2108
  if (!content?.trim())
361
2109
  return {};
362
2110
  try {
@@ -366,9 +2114,56 @@ function getParsedObject(content, path) {
366
2114
  } catch {}
367
2115
  throw new Error(`Expected valid JSON object in ${path}.`);
368
2116
  }
2117
+ // src/review-publication.ts
2118
+ import { createHash as createHash2 } from "node:crypto";
2119
+ import { readFile as readFile8, rename, writeFile as writeFile6 } from "node:fs/promises";
2120
+ import { join as join10 } from "node:path";
2121
+ import { z as z3 } from "zod";
2122
+ var reviewPublicationStateSchema = z3.object({
2123
+ target: z3.string().optional(),
2124
+ comments: z3.array(z3.string()).default([]),
2125
+ replies: z3.array(z3.string()).default([]),
2126
+ overlayPath: z3.string().optional()
2127
+ });
2128
+ function reviewCommentFingerprint(comment) {
2129
+ return digest([comment.file, String(comment.line), comment.side ?? "RIGHT", comment.body].join("\x00"));
2130
+ }
2131
+ function reviewReplyFingerprint(threadId, body) {
2132
+ return digest(`${threadId}\x00${body}`);
2133
+ }
2134
+ function unpublishedReviewComments(comments, knownFingerprints) {
2135
+ return comments.filter((comment) => !knownFingerprints.has(reviewCommentFingerprint(comment)));
2136
+ }
2137
+ async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
2138
+ const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
2139
+ const path = join10(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
2140
+ try {
2141
+ const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile8(path, "utf8")));
2142
+ return { path, state: { ...parsed, target } };
2143
+ } catch (error) {
2144
+ if (error.code === "ENOENT") {
2145
+ return { path, state: { target, comments: [], replies: [] } };
2146
+ }
2147
+ if (error instanceof SyntaxError || error instanceof z3.ZodError) {
2148
+ throw new Error(`Cannot parse review publication state: ${path}`);
2149
+ }
2150
+ throw error;
2151
+ }
2152
+ }
2153
+ async function saveReviewPublicationState(path, state) {
2154
+ const temp = `${path}.${process.pid}.tmp`;
2155
+ await writeFile6(temp, `${JSON.stringify(state, null, 2)}
2156
+ `, "utf8");
2157
+ await rename(temp, path);
2158
+ }
2159
+ function digest(value) {
2160
+ return createHash2("sha256").update(value).digest("hex");
2161
+ }
369
2162
  // src/setup.ts
370
- import { homedir as homedir4 } from "node:os";
371
- import { join as join5 } from "node:path";
2163
+ import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
2164
+ import { readdir as readdir2, readFile as readFile9 } from "node:fs/promises";
2165
+ import { homedir as homedir8 } from "node:os";
2166
+ import { basename as basename5, join as join11 } from "node:path";
372
2167
  var MISE_DEPENDENCIES = [
373
2168
  { name: "node", tool: "node", spec: "node@22", minimumVersion: "22.19.0" },
374
2169
  { name: "zellij", tool: "zellij", spec: "zellij@latest", minimumVersion: undefined },
@@ -399,9 +2194,14 @@ var PI_SKILL_SOURCES = [
399
2194
  { repository: "AminBlg/SimpleEnglish", skills: ["simple-english"] }
400
2195
  ];
401
2196
  var MCP_ADAPTER_PACKAGE = "npm:pi-mcp-adapter";
2197
+ var BUNDLED_AGENTS_DIR2 = resolveBundledAgentsDir();
2198
+ var FORGE_DEPENDENCIES = {
2199
+ github: { name: "gh", tool: "gh", spec: "gh@latest", minimumVersion: undefined },
2200
+ gitlab: { name: "glab", tool: "glab", spec: "glab@latest", minimumVersion: undefined }
2201
+ };
402
2202
  async function ensureMise(options = {}) {
403
- const homeDir = options.homeDir ?? homedir4();
404
- const current = await mise.executableCheck() ?? await mise.executableCheck(join5(homeDir, ".local", "bin", "mise"));
2203
+ const homeDir = options.homeDir ?? homedir8();
2204
+ const current = await mise.executableCheck() ?? await mise.executableCheck(join11(homeDir, ".local", "bin", "mise"));
405
2205
  if (current)
406
2206
  return { executable: current, action: createSetupAction("mise", "ready", current) };
407
2207
  reportProgress(options, "Installing mise");
@@ -430,7 +2230,10 @@ async function ensureMiseHooks(miseExecutable, options = {}) {
430
2230
  async function ensureMiseDeps(miseExecutable, options = {}) {
431
2231
  const canRunMise = Boolean(await mise.executableCheck(miseExecutable));
432
2232
  const actions = [];
433
- for (const dependency of MISE_DEPENDENCIES) {
2233
+ const dependencies = [...MISE_DEPENDENCIES];
2234
+ if (options.forge && options.forge !== "none")
2235
+ dependencies.push(FORGE_DEPENDENCIES[options.forge]);
2236
+ for (const dependency of dependencies) {
434
2237
  const installed = canRunMise && await mise.toolCheckGlobal(miseExecutable, dependency.tool, dependency.minimumVersion);
435
2238
  if (installed) {
436
2239
  actions.push(createSetupAction(dependency.name, "ready", dependency.spec));
@@ -446,18 +2249,33 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
446
2249
  async function ensurePiPlugins(options = {}) {
447
2250
  const actions = await ensurePiPackages(PI_PACKAGES, options);
448
2251
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
449
- const webSearch = await pi.configEnsure(join5(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
2252
+ const webSearch = await pi.configEnsure(join11(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
450
2253
  actions.push(getConfigSetupAction("web search settings", webSearch));
451
- const lsp = await pi.configEnsure(join5(agentDir, "pi-lsp.json"), (config) => ({
2254
+ const lsp = await pi.configEnsure(join11(agentDir, "pi-lsp.json"), (config) => ({
452
2255
  ...config,
453
2256
  progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
454
2257
  }), options.dryRun);
455
2258
  actions.push(getConfigSetupAction("pi-lsp settings", lsp));
456
2259
  return actions;
457
2260
  }
2261
+ async function ensurePiAgents(options = {}) {
2262
+ const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
2263
+ const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR2;
2264
+ const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
2265
+ const entries = (await readdir2(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
2266
+ const actions = [];
2267
+ for (const entry of entries) {
2268
+ const id = basename5(entry.name, ".md").replace(/^diffpi-/, "");
2269
+ const source = await readFile9(join11(bundledAgentsDir, entry.name), "utf8");
2270
+ const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
2271
+ const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
2272
+ actions.push(getConfigSetupAction(`pi agent ${id}`, result));
2273
+ }
2274
+ return actions;
2275
+ }
458
2276
  async function ensurePiSkills(miseExecutable, options = {}) {
459
2277
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
460
- const sharedSkillsDir = join5(options.homeDir ?? homedir4(), ".agents", "skills");
2278
+ const sharedSkillsDir = join11(options.homeDir ?? homedir8(), ".agents", "skills");
461
2279
  const actions = [];
462
2280
  for (const source of PI_SKILL_SOURCES) {
463
2281
  const missing = [];
@@ -501,6 +2319,12 @@ async function ensureMcpAdapters(miseExecutable, options = {}) {
501
2319
  } else if (options.issueTracker === "jira") {
502
2320
  servers.atlassian = { url: "https://mcp.atlassian.com/v1/mcp", auth: "oauth", protocolVersion: "auto" };
503
2321
  }
2322
+ if (options.forge === "github") {
2323
+ servers.github = { url: "https://api.githubcopilot.com/mcp/", auth: "oauth", protocolVersion: "auto" };
2324
+ } else if (options.forge === "gitlab") {
2325
+ const host = (await detectVcs(projectDir)).host || "gitlab.com";
2326
+ servers.gitlab = { url: `https://${host}/api/v4/mcp`, auth: "oauth", protocolVersion: "auto" };
2327
+ }
504
2328
  const result = await mcp.serversEnsure(servers, {
505
2329
  dryRun: options.dryRun,
506
2330
  path: mcp.globalConfigPath(options.homeDir)
@@ -514,13 +2338,81 @@ async function setupPi(options = {}) {
514
2338
  actions.push(await ensureMiseHooks(miseResult.executable, options));
515
2339
  actions.push(...await ensureMiseDeps(miseResult.executable, options));
516
2340
  actions.push(...await ensurePiPlugins(options));
2341
+ actions.push(...await ensurePiAgents(options));
517
2342
  actions.push(...await ensurePiSkills(miseResult.executable, options));
518
2343
  actions.push(...await ensureMcpAdapters(miseResult.executable, options));
2344
+ if (options.bindZedKey)
2345
+ actions.push(...await ensureZedIntegration(options));
519
2346
  return {
520
2347
  actions,
521
- restartPi: actions.some((item) => (item.status === "installed" || item.status === "updated") && (item.name.startsWith("pi package ") || item.name.startsWith("pi skill ") || item.name === "MCP configuration" || item.name === "web search settings" || item.name === "pi-lsp settings"))
2348
+ restartPi: setupRequiresRestart(actions)
522
2349
  };
523
2350
  }
2351
+ function setupRequiresRestart(actions) {
2352
+ return actions.some((item) => (item.status === "installed" || item.status === "updated") && (item.name.startsWith("pi package ") || item.name.startsWith("pi agent ") || item.name.startsWith("pi skill ") || item.name === "MCP configuration" || item.name === "web search settings" || item.name === "pi-lsp settings"));
2353
+ }
2354
+ function materializeAgentModels(content, agentId, config, availableModels) {
2355
+ const { frontmatter } = parseFrontmatter3(content.startsWith("\uFEFF") ? content.slice(1) : content);
2356
+ const profilePreferences = [...getTextList(frontmatter.model), ...getTextList(frontmatter.model_fallbacks)];
2357
+ const preferences = resolveAgentModelPreferences(agentId, profilePreferences, config);
2358
+ let selectedIndex = availableModels === undefined && preferences.length > 0 ? 0 : -1;
2359
+ let selectedModel = selectedIndex === 0 ? preferences[0] : undefined;
2360
+ if (availableModels) {
2361
+ for (const [index, preference] of preferences.entries()) {
2362
+ const match = findPreferredModel(availableModels, preference);
2363
+ if (!match)
2364
+ continue;
2365
+ selectedIndex = index;
2366
+ selectedModel = `${match.provider}/${match.id}`;
2367
+ break;
2368
+ }
2369
+ }
2370
+ const fallbacks = preferences.filter((_preference, index) => index !== selectedIndex);
2371
+ return replaceAgentModelFields(content, selectedModel, fallbacks);
2372
+ }
2373
+ function replaceAgentModelFields(content, model, fallbacks) {
2374
+ const newline = content.includes(`\r
2375
+ `) ? `\r
2376
+ ` : `
2377
+ `;
2378
+ const lines = content.replaceAll(`\r
2379
+ `, `
2380
+ `).split(`
2381
+ `);
2382
+ const closingDelimiter = lines.indexOf("---", 1);
2383
+ if (lines[0] !== "---" || closingDelimiter < 0)
2384
+ return content;
2385
+ const frontmatter = lines.slice(1, closingDelimiter).filter((line) => !/^model(?:_fallbacks)?:/.test(line));
2386
+ if (model)
2387
+ frontmatter.push(`model: ${model}`);
2388
+ if (fallbacks.length > 0)
2389
+ frontmatter.push(`model_fallbacks: ${fallbacks.join(", ")}`);
2390
+ return ["---", ...frontmatter, "---", ...lines.slice(closingDelimiter + 1)].join(newline);
2391
+ }
2392
+ async function ensureZedIntegration(options = {}) {
2393
+ if (options.dryRun) {
2394
+ const actions = [createSetupAction("Zed review task", "planned", "tasks.json")];
2395
+ if (options.bindZedKey)
2396
+ actions.push(createSetupAction("Zed review keybinding", "planned", "keymap.json"));
2397
+ return actions;
2398
+ }
2399
+ const actions = [];
2400
+ try {
2401
+ const task = await ensureZedReviewTask(options.homeDir);
2402
+ actions.push(createSetupAction("Zed review task", task.changed ? "installed" : "ready", task.path));
2403
+ } catch (error) {
2404
+ actions.push(createSetupAction("Zed review task", "skipped", error instanceof Error ? error.message : String(error)));
2405
+ }
2406
+ if (options.bindZedKey) {
2407
+ try {
2408
+ const key = await ensureZedReviewKeybinding(options.homeDir);
2409
+ actions.push(createSetupAction("Zed review keybinding", key.changed ? "installed" : "ready", key.path));
2410
+ } catch (error) {
2411
+ actions.push(createSetupAction("Zed review keybinding", "skipped", error instanceof Error ? error.message : String(error)));
2412
+ }
2413
+ }
2414
+ return actions;
2415
+ }
524
2416
  async function ensurePiPackages(packages, options) {
525
2417
  const executable = await pi.executableCheck();
526
2418
  if (!executable && !options.dryRun)
@@ -542,6 +2434,10 @@ ${source}`;
542
2434
  }
543
2435
  return actions;
544
2436
  }
2437
+ function getTextList(value) {
2438
+ const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
2439
+ return values.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
2440
+ }
545
2441
  function getConfigSetupAction(name, result) {
546
2442
  if (!result.changed)
547
2443
  return createSetupAction(name, "ready", result.path);
@@ -558,15 +2454,119 @@ function reportProgress(options, message) {
558
2454
  function getRecord(value) {
559
2455
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
560
2456
  }
2457
+ // src/templates.ts
2458
+ import { readFile as readFile10 } from "node:fs/promises";
2459
+ import { homedir as homedir9 } from "node:os";
2460
+ import { join as join12, normalize } from "node:path";
2461
+ async function loadTemplate(name, options = {}) {
2462
+ const relative = templateRelativePath(name);
2463
+ const userPath = join12(options.homeDir ?? homedir9(), ".difflab", "diffpi", "templates", relative);
2464
+ const bundledPath = join12(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
2465
+ const user = await readOptionalFile(userPath);
2466
+ if (user !== undefined)
2467
+ return { name, path: userPath, source: "user", content: user };
2468
+ const bundled = await readOptionalFile(bundledPath);
2469
+ if (bundled !== undefined)
2470
+ return { name, path: bundledPath, source: "bundled", content: bundled };
2471
+ throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
2472
+ }
2473
+ function renderTemplate(content, variables) {
2474
+ return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
2475
+ }
2476
+ function templateRelativePath(name) {
2477
+ const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
2478
+ if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
2479
+ throw new Error(`Invalid template name: ${name}`);
2480
+ }
2481
+ return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
2482
+ }
2483
+ async function readOptionalFile(path) {
2484
+ try {
2485
+ return await readFile10(path, "utf8");
2486
+ } catch (error) {
2487
+ if (error.code === "ENOENT")
2488
+ return;
2489
+ throw error;
2490
+ }
2491
+ }
561
2492
  export {
2493
+ CONVENTIONAL_COMMIT,
2494
+ ZED_REVIEW_TASK_NAME,
2495
+ addComment,
2496
+ assertGitHubMergeReady,
2497
+ assertReviewEventSupported,
2498
+ checkConventionalSubject,
2499
+ ciGate,
2500
+ computeProjectSlug,
2501
+ createForge,
2502
+ createLocalReviewBackend,
2503
+ createModeController,
2504
+ createRemoteReviewBackend,
2505
+ dedupeFindings,
2506
+ detectIde,
2507
+ detectMux,
2508
+ detectShell,
2509
+ detectVcs,
2510
+ diffpiConfigPaths,
2511
+ discoverAgentModes,
562
2512
  ensureMcpAdapters,
563
2513
  ensureMise,
564
2514
  ensureMiseDeps,
565
2515
  ensureMiseHooks,
2516
+ ensurePiAgents,
566
2517
  ensurePiPlugins,
567
2518
  ensurePiSkills,
2519
+ ensureStore,
2520
+ ensureZedReviewKeybinding,
2521
+ ensureZedReviewTask,
2522
+ findPreferredModel,
2523
+ findingSchema,
2524
+ findingsSchema,
2525
+ gitToplevel,
2526
+ githubReviewSubmissionEndpoint,
2527
+ hasGitlabDraftNotes,
2528
+ launch,
2529
+ listSessions,
2530
+ loadDiffpiConfig,
2531
+ loadReviewPublicationState,
2532
+ loadTemplate,
2533
+ localReviewAuthor,
568
2534
  mcp,
569
2535
  mise,
2536
+ openInNewTab,
2537
+ parseGitlabDiffRefs,
2538
+ parseRemote,
2539
+ parseThreadArtifact,
570
2540
  pi,
571
- setupPi
2541
+ readSession,
2542
+ renderReviewDoc,
2543
+ renderTemplate,
2544
+ renderThreadArtifact,
2545
+ resolveAgentMode,
2546
+ resolveAgentModelPreferences,
2547
+ resolvePrSession,
2548
+ resolveReviewSession,
2549
+ resolveSession,
2550
+ reviewCommentFingerprint,
2551
+ reviewRecordName,
2552
+ reviewReplyFingerprint,
2553
+ reviewSlug,
2554
+ reviewsDir,
2555
+ runMiseGates,
2556
+ saveReviewPublicationState,
2557
+ sessionsDir,
2558
+ setupPi,
2559
+ severitySchema,
2560
+ storeDir,
2561
+ storeGlobalRoot,
2562
+ templateRelativePath,
2563
+ toFindings,
2564
+ toReviewComments,
2565
+ tuicrAvailable,
2566
+ unpublishedReviewComments,
2567
+ upsertThreadReply,
2568
+ withRemoteProvenance,
2569
+ yymmdd,
2570
+ zedKeymapPath,
2571
+ zedTasksPath
572
2572
  };