@difflab/pi 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +5 -5
  2. package/agents/diffpi-copilot.md +6 -3
  3. package/agents/diffpi-reviewer.md +3 -3
  4. package/dist/commands/review.d.ts +0 -1
  5. package/dist/commands/review.d.ts.map +1 -1
  6. package/dist/extensions/index.js +344 -810
  7. package/dist/extensions/tuicrx.d.ts +1 -8
  8. package/dist/extensions/tuicrx.d.ts.map +1 -1
  9. package/dist/index.d.ts +7 -8
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +197 -458
  12. package/dist/review/github-review-backend.d.ts.map +1 -1
  13. package/dist/review/gitlab-review-backend.d.ts.map +1 -1
  14. package/dist/review/index.d.ts +6 -6
  15. package/dist/review/index.d.ts.map +1 -1
  16. package/dist/review/local-reviews.d.ts +31 -0
  17. package/dist/review/local-reviews.d.ts.map +1 -0
  18. package/dist/review/review-backend.d.ts +2 -3
  19. package/dist/review/review-backend.d.ts.map +1 -1
  20. package/dist/review/review-markdown.d.ts +1 -4
  21. package/dist/review/review-markdown.d.ts.map +1 -1
  22. package/dist/review/types.d.ts +0 -28
  23. package/dist/review/types.d.ts.map +1 -1
  24. package/dist/store.d.ts +0 -2
  25. package/dist/store.d.ts.map +1 -1
  26. package/dist/tools/index.js +185 -783
  27. package/dist/tools/review.d.ts +0 -30
  28. package/dist/tools/review.d.ts.map +1 -1
  29. package/dist/workflow.d.ts +47 -0
  30. package/dist/workflow.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/skills/review/SKILL.md +6 -8
  33. package/workflows/review/address.md +8 -0
  34. package/workflows/review/auto.md +8 -0
  35. package/workflows/review/complete.md +7 -0
  36. package/workflows/review/edit.md +7 -0
  37. package/workflows/review/help.md +14 -0
  38. package/workflows/review/merge.md +6 -0
  39. package/workflows/review/new.md +7 -0
  40. package/workflows/review/publish.md +6 -0
  41. package/dist/review/local-review-backend.d.ts +0 -3
  42. package/dist/review/local-review-backend.d.ts.map +0 -1
  43. package/dist/review/review-state.d.ts +0 -20
  44. package/dist/review/review-state.d.ts.map +0 -1
  45. package/skills/review/references/workflows/address.md +0 -11
  46. package/skills/review/references/workflows/auto.md +0 -18
  47. package/skills/review/references/workflows/complete.md +0 -7
  48. package/skills/review/references/workflows/edit.md +0 -7
  49. package/skills/review/references/workflows/help.md +0 -30
  50. package/skills/review/references/workflows/merge.md +0 -6
  51. package/skills/review/references/workflows/new.md +0 -19
  52. package/skills/review/references/workflows/open.md +0 -3
  53. package/skills/review/references/workflows/publish.md +0 -8
@@ -1069,18 +1069,6 @@ async function reviewsDir(cwd, homeDir = homedir2()) {
1069
1069
  await mkdir2(dir, { recursive: true });
1070
1070
  return dir;
1071
1071
  }
1072
- async function completedReviewsDir(cwd, homeDir = homedir2()) {
1073
- const store = await ensureStore(cwd, homeDir);
1074
- const dir = join6(store.link, "reviews");
1075
- await mkdir2(dir, { recursive: true });
1076
- return dir;
1077
- }
1078
- async function sessionsDir(cwd, homeDir = homedir2()) {
1079
- const store = await ensureStore(cwd, homeDir);
1080
- const dir = join6(store.link, "sessions");
1081
- await mkdir2(dir, { recursive: true });
1082
- return dir;
1083
- }
1084
1072
  async function assertStoreLink(path, dest) {
1085
1073
  const entry = await lstat(path);
1086
1074
  if (!entry.isSymbolicLink())
@@ -7903,10 +7891,10 @@ function result2(text, details = {}) {
7903
7891
 
7904
7892
  // src/tools/review.ts
7905
7893
  import { existsSync as existsSync2 } from "node:fs";
7906
- import { mkdir as mkdir7, readdir as readdir2, readFile as readFile11, rename as rename3, stat, unlink as unlink2, writeFile as writeFile7 } from "node:fs/promises";
7894
+ import { mkdir as mkdir8, writeFile as writeFile6 } from "node:fs/promises";
7907
7895
  import { join as join16 } from "node:path";
7908
7896
  import { defineTool as defineTool6 } from "@earendil-works/pi-coding-agent";
7909
- import { z as z11 } from "zod";
7897
+ import { z as z9 } from "zod";
7910
7898
 
7911
7899
  // src/review/types.ts
7912
7900
  import { z as z8 } from "zod";
@@ -7931,21 +7919,6 @@ function yymmdd(date = new Date) {
7931
7919
  function reviewRecordName(target, date = new Date) {
7932
7920
  return `${yymmdd(date)}-${reviewSlug(target) || "uncommitted"}`;
7933
7921
  }
7934
- function localResponseMarker(threadId) {
7935
- return `<!-- diffpi -->`;
7936
- }
7937
- function isLocalResponse(body) {
7938
- return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
7939
- }
7940
- function parseReviewThreadAction(body) {
7941
- const match = body.match(/^\[(REOPEN|RESOLVE|DELETE)\]\s*/i);
7942
- if (!match)
7943
- return { body };
7944
- return {
7945
- action: match[1].toLowerCase(),
7946
- body: body.slice(match[0].length)
7947
- };
7948
- }
7949
7922
  function dedupeFindings(findings) {
7950
7923
  const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
7951
7924
  const byKey = new Map;
@@ -7991,18 +7964,6 @@ function renderCommentBody(finding) {
7991
7964
  return `${prefix}${finding.body}${reference}`;
7992
7965
  }
7993
7966
  // src/review/review-markdown.ts
7994
- import { z as z9 } from "zod";
7995
- var reviewThreadRecordSchema = z9.object({
7996
- id: z9.string().min(1),
7997
- file: z9.string().nullable().optional().transform((value) => value ?? undefined),
7998
- line: z9.number().int().positive().nullable().optional().transform((value) => value ?? undefined),
7999
- body: z9.string(),
8000
- author: z9.string().optional(),
8001
- resolved: z9.boolean(),
8002
- addressed: z9.boolean().optional(),
8003
- question: z9.boolean(),
8004
- replies: z9.array(z9.string()).optional()
8005
- });
8006
7967
  function renderReviewDoc(input) {
8007
7968
  const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
8008
7969
  const lines = [`# Review: ${input.title}`, "", "## Metadata"];
@@ -8044,88 +8005,6 @@ function renderReviewDoc(input) {
8044
8005
  `).trimEnd()}
8045
8006
  `;
8046
8007
  }
8047
- function renderThreadArtifact(title, target, threads, options = {}) {
8048
- const records = threads.map((thread) => ({
8049
- id: thread.id,
8050
- file: thread.file,
8051
- line: thread.line,
8052
- body: thread.body,
8053
- author: thread.author,
8054
- resolved: thread.resolved,
8055
- addressed: thread.addressed,
8056
- question: thread.question,
8057
- replies: thread.replies
8058
- }));
8059
- const payload = Buffer.from(JSON.stringify(records), "utf8").toString("base64url");
8060
- const lines = [
8061
- `<!-- diffpi-threads:${payload} -->`,
8062
- `# Review threads: ${title}`,
8063
- "",
8064
- "## Metadata",
8065
- "",
8066
- `- Target: ${target}`,
8067
- `- Pulled: ${options.timestamp ?? new Date().toISOString()}`
8068
- ];
8069
- if (options.number !== undefined)
8070
- lines.push(`- PR/MR: #${options.number}${options.url ? ` — ${options.url}` : ""}`);
8071
- lines.push("", "## Replies", "");
8072
- for (const thread of threads) {
8073
- const id = Buffer.from(thread.id, "utf8").toString("base64url");
8074
- lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} (${thread.id})`, "", `<!-- diffpi-reply-start:${id} -->`, thread.reply ?? "", `<!-- diffpi-reply-end:${id} -->`, "");
8075
- }
8076
- lines.push("## Source comments", "");
8077
- for (const thread of threads) {
8078
- lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, `Status: ${thread.resolved ? "resolved" : thread.addressed ? "addressed" : "open"}`, "", thread.body, "", "---", "");
8079
- }
8080
- return `${lines.join(`
8081
- `).trimEnd()}
8082
- `;
8083
- }
8084
- function parseThreadArtifact(content) {
8085
- const payload = content.match(/^<!-- diffpi-threads:([A-Za-z0-9_-]+) -->$/m)?.[1];
8086
- if (!payload)
8087
- throw new Error("This file is not a Diffpi thread artifact.");
8088
- let threads;
8089
- try {
8090
- const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
8091
- threads = z9.array(reviewThreadRecordSchema).parse(decoded);
8092
- } catch {
8093
- throw new Error("Cannot parse the Diffpi thread artifact payload.");
8094
- }
8095
- const replies = content.split(/^## Source comments$/m, 1)[0] ?? "";
8096
- return threads.map((thread) => {
8097
- const id = Buffer.from(thread.id, "utf8").toString("base64url");
8098
- const startMarker = `<!-- diffpi-reply-start:${id} -->
8099
- `;
8100
- const endMarker = `
8101
- <!-- diffpi-reply-end:${id} -->`;
8102
- const start = replies.indexOf(startMarker);
8103
- const end = start < 0 ? -1 : replies.indexOf(endMarker, start + startMarker.length);
8104
- const reply = start >= 0 && end >= 0 ? replies.slice(start + startMarker.length, end).trim() : "";
8105
- return reply ? { ...thread, reply } : thread;
8106
- });
8107
- }
8108
- function upsertThreadReply(content, threadId, body, question, resolved) {
8109
- const threads = parseThreadArtifact(content);
8110
- const thread = threads.find((candidate) => candidate.id === threadId);
8111
- if (!thread)
8112
- throw new Error(`Review thread ${threadId} was not found in the local artifact.`);
8113
- thread.reply = body;
8114
- thread.addressed = true;
8115
- if (question !== undefined)
8116
- thread.question = question;
8117
- if (resolved !== undefined)
8118
- thread.resolved = resolved;
8119
- const title = content.match(/^# Review threads: (.+)$/m)?.[1] ?? "review";
8120
- const target = content.match(/^- Target: (.+)$/m)?.[1] ?? "local";
8121
- const timestamp = content.match(/^- Pulled: (.+)$/m)?.[1];
8122
- const pr = content.match(/^- PR\/MR: #(\d+)(?: — (.+))?$/m);
8123
- return renderThreadArtifact(title, target, threads, {
8124
- timestamp,
8125
- number: pr ? Number.parseInt(pr[1], 10) : undefined,
8126
- url: pr?.[2]
8127
- });
8128
- }
8129
8008
  // src/review/github-review-backend.ts
8130
8009
  function GitHubReviewBackend(vcs, number) {
8131
8010
  const pendingReview = async () => {
@@ -8187,9 +8066,6 @@ function GitHubReviewBackend(vcs, number) {
8187
8066
  id: thread.id,
8188
8067
  file: thread.path ?? undefined,
8189
8068
  line: thread.line ?? undefined,
8190
- rootCommentId: comment?.id,
8191
- commentIds: nodes.flatMap((node) => [node.id, node.databaseId?.toString()].filter((id) => Boolean(id))),
8192
- commentNodeIds: nodes.flatMap((node) => node.id ? [node.id] : []),
8193
8069
  body,
8194
8070
  author: comment?.author?.login,
8195
8071
  resolved: thread.isResolved,
@@ -8199,7 +8075,6 @@ function GitHubReviewBackend(vcs, number) {
8199
8075
  });
8200
8076
  };
8201
8077
  return {
8202
- kind: "remote",
8203
8078
  async stage(draft) {
8204
8079
  const pending = await pendingReview();
8205
8080
  if (!pending) {
@@ -8257,33 +8132,6 @@ function GitHubReviewBackend(vcs, number) {
8257
8132
  await ghChecked(args);
8258
8133
  }
8259
8134
  },
8260
- async readDraft() {
8261
- const pending = await pendingReview();
8262
- if (!pending)
8263
- return { comments: [], body: "" };
8264
- const review = await ghChecked(["api", `/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews/${pending.id}`]);
8265
- const comments = await ghChecked([
8266
- "api",
8267
- `/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews/${pending.id}/comments`
8268
- ]);
8269
- let reviewData;
8270
- let commentData;
8271
- try {
8272
- reviewData = JSON.parse(review.stdout);
8273
- commentData = JSON.parse(comments.stdout);
8274
- } catch {
8275
- throw new Error("Cannot parse the pending GitHub review as JSON.");
8276
- }
8277
- return {
8278
- body: reviewData.body ?? "",
8279
- comments: commentData.map((comment) => ({
8280
- file: comment.path,
8281
- line: comment.line ?? comment.original_line ?? undefined,
8282
- side: comment.side ?? undefined,
8283
- body: comment.body
8284
- }))
8285
- };
8286
- },
8287
8135
  listThreads,
8288
8136
  async reply(input) {
8289
8137
  const threads = await listThreads();
@@ -8300,32 +8148,14 @@ function GitHubReviewBackend(vcs, number) {
8300
8148
  `body=${input.body}`
8301
8149
  ]);
8302
8150
  }
8303
- if (input.resolve)
8304
- await this.setResolved?.(input.threadId, true);
8305
- },
8306
- async setResolved(threadId, resolved) {
8307
- await ghChecked([
8308
- "api",
8309
- "graphql",
8310
- "-f",
8311
- `query=${resolved ? GITHUB_RESOLVE_MUTATION : GITHUB_UNRESOLVE_MUTATION}`,
8312
- "-f",
8313
- `threadId=${threadId}`
8314
- ]);
8315
- },
8316
- async deleteThread(threadId) {
8317
- const thread = (await listThreads()).find((candidate) => candidate.id === threadId);
8318
- const commentNodeIds = thread?.commentNodeIds ?? (thread?.rootCommentId ? [thread.rootCommentId] : []);
8319
- if (!commentNodeIds.length)
8320
- throw new Error(`Cannot delete GitHub thread ${threadId}: comment node IDs are missing.`);
8321
- for (const commentId of commentNodeIds) {
8151
+ if (input.resolve) {
8322
8152
  await ghChecked([
8323
8153
  "api",
8324
8154
  "graphql",
8325
8155
  "-f",
8326
- `query=${GITHUB_DELETE_COMMENT_MUTATION}`,
8156
+ `query=${GITHUB_RESOLVE_MUTATION}`,
8327
8157
  "-f",
8328
- `commentId=${commentId}`
8158
+ `threadId=${input.threadId}`
8329
8159
  ]);
8330
8160
  }
8331
8161
  },
@@ -8344,13 +8174,11 @@ function GitHubReviewBackend(vcs, number) {
8344
8174
  function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
8345
8175
  return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
8346
8176
  }
8347
- 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{id,databaseId,body,author{login}}}}pageInfo{hasNextPage,endCursor}}}}}`;
8177
+ 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}}}}}`;
8348
8178
  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}}}`;
8349
8179
  var GITHUB_ADD_FILE_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,subjectType:FILE}){thread{id}}}`;
8350
8180
  var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
8351
8181
  var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
8352
- var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
8353
- var GITHUB_DELETE_COMMENT_MUTATION = `mutation($commentId:ID!){deletePullRequestReviewComment(input:{id:$commentId}){clientMutationId}}`;
8354
8182
 
8355
8183
  // src/review/gitlab-review-backend.ts
8356
8184
  function GitLabReviewBackend(vcs, number) {
@@ -8387,7 +8215,6 @@ function GitLabReviewBackend(vcs, number) {
8387
8215
  });
8388
8216
  };
8389
8217
  return {
8390
- kind: "remote",
8391
8218
  async stage(draft) {
8392
8219
  const endpoint = `${mergeRequestEndpoint()}/draft_notes`;
8393
8220
  if (draft.body.trim()) {
@@ -8416,36 +8243,6 @@ function GitLabReviewBackend(vcs, number) {
8416
8243
  });
8417
8244
  }
8418
8245
  },
8419
- async readDraft() {
8420
- const result = await glabChecked(["api", `${mergeRequestEndpoint()}/draft_notes`]);
8421
- let notes;
8422
- try {
8423
- notes = JSON.parse(result.stdout);
8424
- } catch {
8425
- throw new Error("Cannot parse GitLab draft notes as JSON.");
8426
- }
8427
- const comments = [];
8428
- const body = [];
8429
- for (const note of notes) {
8430
- if (!note.note)
8431
- continue;
8432
- const file = note.position?.new_path ?? note.position?.old_path;
8433
- const line = note.position?.new_line ?? note.position?.old_line;
8434
- if (file && line) {
8435
- comments.push({
8436
- file,
8437
- line,
8438
- side: note.position?.new_line ? "RIGHT" : "LEFT",
8439
- body: note.note
8440
- });
8441
- } else {
8442
- body.push(note.note);
8443
- }
8444
- }
8445
- return { comments, body: body.join(`
8446
-
8447
- `) };
8448
- },
8449
8246
  listThreads,
8450
8247
  async reply(input) {
8451
8248
  const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(input.threadId)}`;
@@ -8457,15 +8254,7 @@ function GitLabReviewBackend(vcs, number) {
8457
8254
  });
8458
8255
  }
8459
8256
  if (input.resolve)
8460
- await this.setResolved?.(input.threadId, true);
8461
- },
8462
- async setResolved(threadId, resolved) {
8463
- const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
8464
- await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
8465
- },
8466
- async deleteThread(threadId) {
8467
- const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
8468
- await glabChecked(["api", "--method", "DELETE", endpoint]);
8257
+ await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=true`]);
8469
8258
  },
8470
8259
  async publish(event) {
8471
8260
  assertReviewEventSupported(vcs.provider, event);
@@ -8505,8 +8294,17 @@ function hasGitlabDraftNotes(input) {
8505
8294
  }
8506
8295
  }
8507
8296
 
8508
- // src/review/local-review-backend.ts
8509
- import { readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
8297
+ // src/review/review-backend.ts
8298
+ function createRemoteReviewBackend(vcs, number) {
8299
+ if (vcs.provider === "github")
8300
+ return GitHubReviewBackend(vcs, number);
8301
+ if (vcs.provider === "gitlab")
8302
+ return GitLabReviewBackend(vcs, number);
8303
+ throw new Error("A remote review backend requires a GitHub or GitLab repository.");
8304
+ }
8305
+ // src/review/local-reviews.ts
8306
+ import { mkdir as mkdir7, readdir as readdir2, readFile as readFile9, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
8307
+ import { join as join15 } from "node:path";
8510
8308
 
8511
8309
  // src/extensions/tuicrx.ts
8512
8310
  import { readFile as readFile8, realpath as realpath4 } from "node:fs/promises";
@@ -8577,11 +8375,6 @@ async function resolveReviewSession(cwd, target) {
8577
8375
  }
8578
8376
  return resolveSession(cwd, target.branch);
8579
8377
  }
8580
- async function resolvePublishSession(cwd, target) {
8581
- if (target.owner && target.repo && target.number !== undefined)
8582
- return resolvePrSession(cwd, target.owner, target.repo, target.number);
8583
- return resolveSession(cwd, target.branch);
8584
- }
8585
8378
  async function resolvePrSession(cwd, owner, repo, number) {
8586
8379
  const coordinate = `${owner}/${repo}`.toLowerCase();
8587
8380
  const sessions = await listSessions(cwd);
@@ -8616,8 +8409,6 @@ async function readSession(path) {
8616
8409
  function toLocalReviewThreads(session) {
8617
8410
  const threads = [];
8618
8411
  const add = (comment, location = {}) => {
8619
- if (isLocalResponse(comment.content))
8620
- return;
8621
8412
  const author = commentAuthor(comment);
8622
8413
  threads.push({
8623
8414
  id: comment.id ?? `local-${threads.length + 1}`,
@@ -8643,54 +8434,6 @@ function toLocalReviewThreads(session) {
8643
8434
  }
8644
8435
  return threads;
8645
8436
  }
8646
- function toFindings(session, options = {}) {
8647
- const comments = [];
8648
- const include = (comment) => (!options.agentOnly || commentAuthor(comment)?.startsWith("Agent: ")) && (!options.excludeLocalResponses || !isLocalResponse(comment.content));
8649
- const publishableBody = (comment) => {
8650
- const author = commentAuthor(comment);
8651
- const route = author?.match(/^Agent:\s*(.+)$/)?.[1];
8652
- return route ? withRemoteProvenance(comment.content, route) : comment.content;
8653
- };
8654
- const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [publishableBody(comment)] : []);
8655
- for (const [file, entry] of Object.entries(session.files ?? {})) {
8656
- for (const fileComment of entry.file_comments ?? []) {
8657
- if (!include(fileComment))
8658
- continue;
8659
- const comment = {
8660
- file,
8661
- body: fileComment.content,
8662
- ...fileComment.id ? { sourceCommentId: String(fileComment.id) } : {}
8663
- };
8664
- const author = commentAuthor(fileComment);
8665
- if (author)
8666
- comment.author = author;
8667
- comments.push(comment);
8668
- }
8669
- for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
8670
- const line = Number.parseInt(lineKey, 10);
8671
- if (!Number.isFinite(line))
8672
- continue;
8673
- for (const lineComment of lineComments) {
8674
- if (!include(lineComment))
8675
- continue;
8676
- const comment = {
8677
- file,
8678
- line,
8679
- side: lineComment.side === "old" ? "LEFT" : "RIGHT",
8680
- body: lineComment.content,
8681
- ...lineComment.id ? { sourceCommentId: String(lineComment.id) } : {}
8682
- };
8683
- const author = commentAuthor(lineComment);
8684
- if (author)
8685
- comment.author = author;
8686
- comments.push(comment);
8687
- }
8688
- }
8689
- }
8690
- return { comments, body: bodyParts.join(`
8691
-
8692
- `) };
8693
- }
8694
8437
  async function requireTuicr() {
8695
8438
  if (!await tuicrAvailable())
8696
8439
  throw new Error("tuicr is not installed or is not available on PATH.");
@@ -8706,167 +8449,83 @@ async function canonicalPath3(path) {
8706
8449
  }
8707
8450
  }
8708
8451
 
8709
- // src/review/local-review-backend.ts
8710
- function LocalReviewBackend(options) {
8711
- return {
8712
- kind: "local",
8713
- async stage(draft) {
8714
- for (const comment of draft.comments) {
8715
- await addComment(options.session, comment.body, {
8716
- targetFile: comment.file,
8717
- line: comment.line,
8718
- side: comment.side === "LEFT" ? "old" : "new",
8719
- username: options.author
8720
- });
8721
- }
8722
- if (draft.body.trim())
8723
- await addComment(options.session, draft.body, { username: options.author });
8724
- },
8725
- async readDraft() {
8726
- return toFindings(await readSession(options.session), { excludeLocalResponses: true });
8727
- },
8728
- async listThreads() {
8729
- try {
8730
- return parseThreadArtifact(await readFile9(options.artifactPath, "utf8"));
8731
- } catch (error) {
8732
- if (error.code === "ENOENT")
8733
- return [];
8734
- throw error;
8735
- }
8736
- },
8737
- async reply(input) {
8738
- const content = await readFile9(options.artifactPath, "utf8");
8739
- await writeFile5(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
8740
- },
8741
- async publish() {
8742
- throw new Error("Promote a local draft through a remote review backend before publishing it.");
8743
- }
8452
+ // src/review/local-reviews.ts
8453
+ async function captureLocalReview(input) {
8454
+ const id = reviewSlug(input.branch) || "local-review";
8455
+ const dir = join15(await reviewsDir(input.cwd, input.homeDir), id);
8456
+ await mkdir7(dir, { recursive: true });
8457
+ const revision = await nextRevision(dir);
8458
+ const output = await readFile9(input.session.path, "utf8");
8459
+ const comments = toLocalReviewThreads(await readSession(input.session.path));
8460
+ const dump = {
8461
+ schemaVersion: 1,
8462
+ id,
8463
+ revision,
8464
+ branch: input.branch,
8465
+ base: input.base,
8466
+ createdAt: (input.now ?? (() => new Date))().toISOString(),
8467
+ diff: input.diff,
8468
+ comments,
8469
+ tuicr: { slug: input.session.slug, output }
8744
8470
  };
8471
+ const path = join15(dir, `${revision}.json`);
8472
+ await writeFile5(path, `${JSON.stringify(dump, null, 2)}
8473
+ `, { encoding: "utf8", flag: "wx" });
8474
+ await unlink2(input.session.path);
8475
+ return { path, dump };
8745
8476
  }
8746
-
8747
- // src/review/review-backend.ts
8748
- function createRemoteReviewBackend(vcs, number) {
8749
- if (vcs.provider === "github")
8750
- return GitHubReviewBackend(vcs, number);
8751
- if (vcs.provider === "gitlab")
8752
- return GitLabReviewBackend(vcs, number);
8753
- throw new Error("A remote review backend requires a GitHub or GitLab repository.");
8754
- }
8755
- function createLocalReviewBackend(options) {
8756
- return LocalReviewBackend(options);
8757
- }
8758
- // src/review/review-state.ts
8759
- import { createHash as createHash2 } from "node:crypto";
8760
- import { readFile as readFile10, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
8761
- import { join as join15 } from "node:path";
8762
- import { z as z10 } from "zod";
8763
- var reviewPublicationStateSchema = z10.object({
8764
- target: z10.string().optional(),
8765
- bodies: z10.array(z10.string()).default([]),
8766
- comments: z10.array(z10.string()).default([]),
8767
- stagedComments: z10.array(z10.string()).default([]),
8768
- replies: z10.array(z10.string()).default([]),
8769
- overlayPath: z10.string().optional()
8770
- });
8771
- function reviewBodyFingerprint(body) {
8772
- return digest(body);
8773
- }
8774
- function reviewCommentFingerprint(comment) {
8775
- return digest([comment.file, String(comment.line), comment.side ?? "RIGHT", comment.body].join("\x00"));
8776
- }
8777
- function reviewReplyFingerprint(threadId, body) {
8778
- return digest(`${threadId}\x00${body}`);
8779
- }
8780
- function unpublishedReviewComments(comments, knownFingerprints) {
8781
- return comments.filter((comment) => !knownFingerprints.has(reviewCommentFingerprint(comment)));
8477
+ async function nextRevision(dir) {
8478
+ return await latestRevision(dir) + 1;
8782
8479
  }
8783
- async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
8784
- const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
8785
- const path = join15(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
8480
+ async function latestRevision(dir) {
8786
8481
  try {
8787
- const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile10(path, "utf8")));
8788
- return { path, state: { ...parsed, target } };
8482
+ return (await readdir2(dir)).reduce((latest, name) => {
8483
+ const match = name.match(/^(\d+)\.json$/);
8484
+ return match ? Math.max(latest, Number(match[1])) : latest;
8485
+ }, 0);
8789
8486
  } catch (error) {
8790
- if (error.code === "ENOENT") {
8791
- return { path, state: { target, bodies: [], comments: [], stagedComments: [], replies: [] } };
8792
- }
8793
- if (error instanceof SyntaxError || error instanceof z10.ZodError) {
8794
- throw new Error(`Cannot parse review publication state: ${path}`);
8795
- }
8487
+ if (error.code === "ENOENT")
8488
+ return 0;
8796
8489
  throw error;
8797
8490
  }
8798
8491
  }
8799
- async function saveReviewPublicationState(path, state) {
8800
- const temp = `${path}.${process.pid}.tmp`;
8801
- await writeFile6(temp, `${JSON.stringify(state, null, 2)}
8802
- `, "utf8");
8803
- await rename2(temp, path);
8804
- }
8805
- function digest(value) {
8806
- return createHash2("sha256").update(value).digest("hex");
8807
- }
8808
8492
  // src/tools/review.ts
8809
- var contextSchema = z11.object({
8810
- cwd: z11.string().optional(),
8811
- target: z11.string().optional(),
8812
- workingTree: z11.boolean().optional()
8493
+ var contextSchema = z9.object({
8494
+ cwd: z9.string().optional(),
8495
+ target: z9.string().optional(),
8496
+ workingTree: z9.boolean().optional()
8813
8497
  });
8814
- var localSchema = contextSchema.extend({ local: z11.boolean().optional() });
8498
+ var localSchema = contextSchema.extend({ local: z9.boolean().optional() });
8815
8499
  var openSchema = localSchema.extend({
8816
- title: z11.string().optional(),
8817
- intent: z11.string().optional(),
8818
- issueUrl: z11.string().url().optional(),
8819
- base: z11.string().optional()
8500
+ title: z9.string().optional(),
8501
+ intent: z9.string().optional(),
8502
+ issueUrl: z9.string().url().optional(),
8503
+ base: z9.string().optional()
8820
8504
  });
8821
8505
  var submitSchema = localSchema.extend({
8822
8506
  findings: findingsSchema,
8823
- overallIssues: z11.array(z11.string()).optional(),
8824
- notVerified: z11.array(z11.string()).optional(),
8825
- title: z11.string().optional()
8507
+ overallIssues: z9.array(z9.string()).optional(),
8508
+ notVerified: z9.array(z9.string()).optional(),
8509
+ title: z9.string().optional()
8826
8510
  });
8827
8511
  var addCommentSchema = localSchema.extend({
8828
- body: z11.string().min(1),
8829
- file: z11.string().min(1),
8830
- line: z11.number().int().positive(),
8831
- side: z11.enum(["LEFT", "RIGHT"]).optional()
8512
+ body: z9.string().min(1),
8513
+ file: z9.string().min(1),
8514
+ line: z9.number().int().positive(),
8515
+ side: z9.enum(["LEFT", "RIGHT"]).optional()
8832
8516
  });
8833
8517
  var respondSchema = localSchema.extend({
8834
- threadId: z11.string().min(1),
8835
- body: z11.string().min(1),
8836
- question: z11.boolean().optional(),
8837
- resolve: z11.boolean().optional()
8518
+ threadId: z9.string().min(1),
8519
+ body: z9.string().min(1),
8520
+ question: z9.boolean().optional(),
8521
+ resolve: z9.boolean().optional()
8838
8522
  });
8839
8523
  var publishSchema = localSchema.extend({
8840
- status: z11.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
8524
+ status: z9.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
8841
8525
  });
8842
8526
  var completeSchema = localSchema.extend({
8843
- action: z11.enum(["approve", "reject", "abandon"]).optional()
8527
+ action: z9.enum(["approve", "reject", "abandon"]).optional()
8844
8528
  });
8845
- function hasReviewDraft(comments, body) {
8846
- return comments.length > 0 || body.trim().length > 0;
8847
- }
8848
- function partitionReviewComments(comments, knownFingerprints, model) {
8849
- const normalized = comments.map((comment) => {
8850
- const published = { ...comment, body: withCommentProvenance(comment, model) };
8851
- return { source: comment, published, fingerprint: reviewCommentFingerprint(published) };
8852
- });
8853
- return {
8854
- checkpointed: normalized.filter((comment) => knownFingerprints.has(comment.fingerprint)),
8855
- candidates: normalized.filter((comment) => !knownFingerprints.has(comment.fingerprint))
8856
- };
8857
- }
8858
- function resolveReviewThread(comment, threads) {
8859
- const sourceCommentId = comment.sourceCommentId;
8860
- if (sourceCommentId) {
8861
- const identified = threads.find((thread) => thread.id === sourceCommentId || thread.rootCommentId === sourceCommentId || thread.commentIds?.includes(sourceCommentId));
8862
- if (identified)
8863
- return identified;
8864
- }
8865
- const matches = threads.filter((thread) => thread.file === comment.file && (thread.line ?? undefined) === (comment.line ?? undefined));
8866
- if (matches.length > 1)
8867
- throw new Error(`Ambiguous remote review threads at ${comment.file}:${comment.line ?? "file"}: ${matches.map((thread) => thread.id).join(", ")}.`);
8868
- return matches[0];
8869
- }
8870
8529
  async function workingTreeDiff(cwd) {
8871
8530
  const tracked = await run("git", ["-C", cwd, "diff", "HEAD"], { capture: "unbounded" });
8872
8531
  if (tracked.code !== 0)
@@ -9062,7 +8721,7 @@ ${openMessage}`, {
9062
8721
  const gates = await runMiseGates(review.cwd);
9063
8722
  const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
9064
8723
  const artifact = await newReviewArtifactPath(review.cwd, params.local || params.workingTree || !review.pr ? "uncommitted" : await reviewTargetId(review));
9065
- await writeFile7(artifact, renderReviewDoc({
8724
+ await writeFile6(artifact, renderReviewDoc({
9066
8725
  title: params.title ?? review.pr?.title ?? review.vcs.branch,
9067
8726
  number: useLocalBackend ? undefined : review.pr?.number,
9068
8727
  url: useLocalBackend ? undefined : review.pr?.url,
@@ -9085,17 +8744,21 @@ ${openMessage}`, {
9085
8744
  count: findings.length
9086
8745
  });
9087
8746
  }
9088
- const backend = createLocalReviewBackend({
9089
- session: session.path,
9090
- artifactPath: artifact,
9091
- author: localReviewAuthor(model)
9092
- });
9093
- await backend.stage({ comments, body });
8747
+ for (const comment of comments) {
8748
+ await addComment(session.path, comment.body, {
8749
+ targetFile: comment.file,
8750
+ line: comment.line,
8751
+ side: comment.side === "LEFT" ? "old" : "new",
8752
+ username: localReviewAuthor(model)
8753
+ });
8754
+ }
8755
+ if (body.trim())
8756
+ await addComment(session.path, body, { username: localReviewAuthor(model) });
9094
8757
  return result3(`Local review staged in tuicr: ${artifact}`, { artifact, count: findings.length, session });
9095
8758
  }
9096
8759
  if (!review.pr)
9097
8760
  return result3(`Review written: ${artifact}. No PR/MR matches this remote target.`, { artifact });
9098
- const hasDraft = hasReviewDraft(comments, body);
8761
+ const hasDraft = comments.length > 0 || body.trim().length > 0;
9099
8762
  if (hasDraft) {
9100
8763
  await createRemoteReviewBackend(review.vcs, review.pr.number).stage({ comments, body });
9101
8764
  }
@@ -9131,12 +8794,12 @@ ${openMessage}`, {
9131
8794
  const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
9132
8795
  if (!session)
9133
8796
  return result3("No matching tuicr session. Open review_new or review_launch_ui first.");
9134
- const backend = createLocalReviewBackend({
9135
- session: session.path,
9136
- artifactPath: "",
9137
- author: localReviewAuthor(model)
8797
+ await addComment(session.path, comment.body, {
8798
+ targetFile: comment.file,
8799
+ line: comment.line,
8800
+ side: comment.side === "LEFT" ? "old" : "new",
8801
+ username: localReviewAuthor(model)
9138
8802
  });
9139
- await backend.stage({ comments: [comment], body: "" });
9140
8803
  return result3(`Comment added to tuicr session ${session.slug}.`, { session, comment });
9141
8804
  }
9142
8805
  if (!review.pr)
@@ -9148,88 +8811,70 @@ ${openMessage}`, {
9148
8811
  defineTool6({
9149
8812
  name: "review_comments",
9150
8813
  label: "review comments",
9151
- description: "Pull review threads or local tuicr comments and write a target-named artifact.",
9152
- promptSnippet: "Call review_comments before addressing findings",
9153
- promptGuidelines: ["Pass local=true to prepare the local reply overlay."],
8814
+ description: "Read remote GitHub or GitLab review threads.",
8815
+ promptSnippet: "Call review_comments before addressing remote findings",
8816
+ promptGuidelines: ["Use review_dump for a local tuicr review."],
9154
8817
  parameters: parameters2(localSchema),
9155
- executionMode: "sequential",
8818
+ executionMode: "parallel",
9156
8819
  async execute(_id, input) {
9157
8820
  const params = localSchema.parse(input);
8821
+ if (params.local || params.workingTree)
8822
+ return result3("Use review_dump for a local tuicr review.");
9158
8823
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
9159
- let threads;
9160
- let artifact;
9161
- if (params.local || params.workingTree) {
9162
- const session = await resolveTuicrSession(review, true);
9163
- if (!session)
9164
- return result3("No matching tuicr session found.");
9165
- artifact = await localThreadArtifactPath(review.cwd, session.slug);
9166
- threads = await syncLocalThreadArtifact(artifact, review.pr?.title ?? review.vcs.branch, session.slug, toLocalReviewThreads(await readSession(session.path)));
9167
- } else if (review.pr && review.forge) {
9168
- threads = await createRemoteReviewBackend(review.vcs, review.pr.number).listThreads();
9169
- const target = await reviewTargetId(review);
9170
- artifact = await newReviewArtifactPath(review.cwd, target);
9171
- await writeFile7(artifact, renderThreadArtifact(review.pr.title, target, threads, { number: review.pr.number, url: review.pr.url }), "utf8");
9172
- } else {
8824
+ if (!review.pr || !review.forge)
9173
8825
  return result3(review.forge ? "No PR/MR matches this remote review target." : unsupportedForgeMessage());
9174
- }
8826
+ const threads = await createRemoteReviewBackend(review.vcs, review.pr.number).listThreads();
9175
8827
  return result3(threads.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
9176
- `) || "No comments.", { artifact, threads });
8828
+ `) || "No comments.", { pr: review.pr, threads });
8829
+ }
8830
+ }),
8831
+ defineTool6({
8832
+ name: "review_dump",
8833
+ label: "review dump",
8834
+ description: "Save one immutable local tuicr review revision and remove its completed session.",
8835
+ promptSnippet: "Call review_dump before applying local review feedback",
8836
+ promptGuidelines: ["Each dump advances the local review revision."],
8837
+ parameters: parameters2(localSchema),
8838
+ executionMode: "sequential",
8839
+ async execute(_id, input) {
8840
+ const params = localSchema.parse(input);
8841
+ const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
8842
+ const session = await resolveTuicrSession(review, true);
8843
+ if (!session)
8844
+ return result3("No matching local tuicr session found.");
8845
+ const base = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "HEAD");
8846
+ const captured = await captureLocalReview({
8847
+ cwd: review.cwd,
8848
+ branch: review.vcs.branch,
8849
+ base,
8850
+ diff: await workingTreeDiff(review.cwd),
8851
+ session
8852
+ });
8853
+ return result3(captured.dump.comments.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
8854
+ `) || "No comments.", { artifact: captured.path, review: captured.dump, threads: captured.dump.comments });
9177
8855
  }
9178
8856
  }),
9179
8857
  defineTool6({
9180
8858
  name: "review_respond",
9181
8859
  label: "review respond",
9182
- description: "Post local question replies to tuicr and record their publish overlay, or reply to remote threads.",
9183
- promptSnippet: "Call review_respond after addressing a comment",
9184
- promptGuidelines: ["Question replies remain unresolved."],
8860
+ description: "Reply directly to a remote GitHub or GitLab review thread.",
8861
+ promptSnippet: "Call review_respond after addressing a remote comment",
8862
+ promptGuidelines: ["Leave threads open unless the user explicitly requests resolution."],
9185
8863
  parameters: parameters2(respondSchema),
9186
8864
  executionMode: "sequential",
9187
8865
  async execute(_id, input, _signal, _onUpdate, ctx) {
9188
8866
  const params = respondSchema.parse(input);
8867
+ if (params.local)
8868
+ return result3("Local reviews use immutable revision dumps, not thread replies.");
9189
8869
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
9190
- const model = modelRoute(ctx);
9191
- if (params.local) {
9192
- const tuicrSession = await resolveTuicrSession(review, true);
9193
- if (!tuicrSession)
9194
- return result3("No matching tuicr session. Open review_new or review_launch_ui first.");
9195
- const artifact = await localThreadArtifactPath(review.cwd, tuicrSession.slug);
9196
- if (!existsSync2(artifact))
9197
- return result3("No local review artifact. Run review_comments with local=true first.");
9198
- const threads = await readThreadArtifact(artifact);
9199
- const known = threads.find((thread) => thread.id === params.threadId);
9200
- const question = known?.question === true || params.question === true || !known && params.question === undefined;
9201
- const awaitUserDeletion = !question && params.resolve === true;
9202
- await addComment(tuicrSession.path, `${localResponseMarker(params.threadId)}
9203
- ${params.body}`, {
9204
- targetFile: known?.file,
9205
- line: known?.line,
9206
- side: "new",
9207
- username: localReviewAuthor(model)
9208
- });
9209
- await createLocalReviewBackend({
9210
- session: tuicrSession.path,
9211
- artifactPath: artifact,
9212
- author: localReviewAuthor(model)
9213
- }).reply({
9214
- threadId: params.threadId,
9215
- body: params.body,
9216
- resolve: false,
9217
- question
9218
- });
9219
- return result3(`Local ${question ? "answer" : "response"} posted to tuicr and recorded in ${artifact}${awaitUserDeletion ? "; delete the source comment in tuicr to resolve it" : ""}.`, { artifact, question, awaitUserDeletion, session: tuicrSession });
9220
- }
9221
8870
  if (!review.pr || !review.forge)
9222
8871
  return result3("No remote PR/MR for this reply.");
9223
- const remote = createRemoteReviewBackend(review.vcs, review.pr.number);
9224
- const remoteThreads = await remote.listThreads();
9225
- const known = remoteThreads.find((thread) => thread.id === params.threadId);
9226
- const question = known?.question === true || params.question === true || !known && params.question === undefined;
9227
- const resolve = question ? false : params.resolve ?? true;
9228
- await remote.reply({
8872
+ const resolve = params.question ? false : params.resolve ?? false;
8873
+ await createRemoteReviewBackend(review.vcs, review.pr.number).reply({
9229
8874
  threadId: params.threadId,
9230
- body: withRemoteProvenance(params.body, model),
8875
+ body: withRemoteProvenance(params.body, modelRoute(ctx)),
9231
8876
  resolve,
9232
- question
8877
+ question: params.question
9233
8878
  });
9234
8879
  return result3(`Replied to ${params.threadId}${resolve ? " and resolved it" : " and left it open"}.`, {
9235
8880
  pr: review.pr,
@@ -9240,51 +8885,40 @@ ${params.body}`, {
9240
8885
  defineTool6({
9241
8886
  name: "review_publish",
9242
8887
  label: "review publish",
9243
- description: "Publish pending remote draft PR/MR review comments and status, or promote a local tuicr draft before publishing.",
8888
+ description: "Publish pending remote review comments and status.",
9244
8889
  promptSnippet: "Call review_publish to make remote review work public",
9245
- promptGuidelines: ["Statuses are COMMENT, APPROVE, REQUEST_CHANGES, or CLOSE."],
8890
+ promptGuidelines: ["Local reviews use revision dumps and are never published remotely."],
9246
8891
  parameters: parameters2(publishSchema),
9247
8892
  executionMode: "sequential",
9248
- async execute(_id, input, _signal, _onUpdate, ctx) {
8893
+ async execute(_id, input) {
9249
8894
  const params = publishSchema.parse(input);
8895
+ if (params.local)
8896
+ return result3("Local reviews use immutable revision dumps and have no publish step.");
9250
8897
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
9251
8898
  if (!review.pr || !review.forge)
9252
8899
  return result3("No remote PR/MR to publish.");
9253
- return publishResolvedReview(review, params, modelRoute(ctx));
8900
+ return publishResolvedReview(review, params);
9254
8901
  }
9255
8902
  }),
9256
8903
  defineTool6({
9257
8904
  name: "review_complete",
9258
8905
  label: "review complete",
9259
- description: "Approve, reject, or abandon a remote review, or archive a local review artifact.",
9260
- promptSnippet: "Call review_complete to finish a review without merging it.",
9261
- promptGuidelines: ["A local completion archives the Diffpi artifact and deletes the matching tuicr session."],
8906
+ description: "Approve, reject, or abandon a remote review.",
8907
+ promptSnippet: "Call review_complete to finish a remote review without merging it.",
8908
+ promptGuidelines: ["Use review_dump to finish a local review revision."],
9262
8909
  parameters: parameters2(completeSchema),
9263
8910
  executionMode: "sequential",
9264
- async execute(_id, input, _signal, _onUpdate, ctx) {
8911
+ async execute(_id, input) {
9265
8912
  const params = completeSchema.parse(input);
8913
+ if (params.local)
8914
+ return result3("Use review_dump to finish a local review revision.");
9266
8915
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
9267
- if (params.local) {
9268
- const session = await resolveTuicrSession(review, true);
9269
- if (!session)
9270
- return result3("No matching local tuicr session to close.");
9271
- const artifact = await localThreadArtifactPath(review.cwd, session.slug);
9272
- if (!existsSync2(artifact))
9273
- return result3("No local review artifact. Run review_comments with local=true first.");
9274
- const archived = await uniqueRecordPath(await completedReviewsDir(review.cwd), reviewSlug(session.slug));
9275
- await rename3(artifact, archived);
9276
- await unlink2(session.path);
9277
- return result3(`Archived ${archived} and deleted the tuicr session ${session.slug}.`, {
9278
- artifact: archived,
9279
- session
9280
- });
9281
- }
9282
8916
  if (!review.pr || !review.forge)
9283
8917
  return result3("No remote PR/MR to complete.");
9284
8918
  if (!params.action)
9285
8919
  return result3("Choose a complete action: approve, reject, or abandon.");
9286
8920
  const status = { approve: "APPROVE", reject: "REQUEST_CHANGES", abandon: "CLOSE" }[params.action];
9287
- return publishResolvedReview(review, { ...params, status }, modelRoute(ctx));
8921
+ return publishResolvedReview(review, { ...params, status });
9288
8922
  }
9289
8923
  }),
9290
8924
  defineTool6({
@@ -9293,10 +8927,10 @@ ${params.body}`, {
9293
8927
  description: "Squash-merge an open, ready GitHub PR after checking its conventional subject.",
9294
8928
  promptSnippet: "Call review_merge after confirming the PR is open, ready, and CI is settled; approval is optional",
9295
8929
  promptGuidelines: ["This is intentionally GitHub-only until GitLab merge support is added."],
9296
- parameters: parameters2(contextSchema.extend({ subject: z11.string().optional() })),
8930
+ parameters: parameters2(contextSchema.extend({ subject: z9.string().optional() })),
9297
8931
  executionMode: "sequential",
9298
8932
  async execute(_id, input) {
9299
- const params = contextSchema.extend({ subject: z11.string().optional() }).parse(input);
8933
+ const params = contextSchema.extend({ subject: z9.string().optional() }).parse(input);
9300
8934
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
9301
8935
  if (review.vcs.provider !== "github" || !review.forge)
9302
8936
  return result3("review_merge currently supports GitHub only.");
@@ -9329,32 +8963,8 @@ ${params.body}`, {
9329
8963
  })
9330
8964
  ];
9331
8965
  }
9332
- async function syncLocalThreadArtifact(path, title, sessionSlug, threads) {
9333
- let previous = [];
9334
- try {
9335
- previous = parseThreadArtifact(await readFile11(path, "utf8"));
9336
- } catch (error) {
9337
- if (error.code !== "ENOENT")
9338
- throw error;
9339
- }
9340
- const previousById = new Map(previous.map((thread) => [thread.id, thread]));
9341
- const currentIds = new Set(threads.map((thread) => thread.id));
9342
- const current = threads.map((thread) => {
9343
- const existing = previousById.get(thread.id);
9344
- return {
9345
- ...thread,
9346
- resolved: false,
9347
- addressed: existing?.addressed ?? Boolean(existing?.reply),
9348
- reply: existing?.reply
9349
- };
9350
- });
9351
- const removed = previous.filter((thread) => !currentIds.has(thread.id)).map((thread) => ({ ...thread, resolved: true }));
9352
- const merged = [...current, ...removed];
9353
- await writeFile7(path, renderThreadArtifact(title, sessionSlug, merged), "utf8");
9354
- return merged;
9355
- }
9356
8966
  function parameters2(schema) {
9357
- return z11.toJSONSchema(schema, { io: "input" });
8967
+ return z9.toJSONSchema(schema, { io: "input" });
9358
8968
  }
9359
8969
  function resolveWorkingDirectory(params) {
9360
8970
  return params.cwd ?? process.cwd();
@@ -9370,159 +8980,17 @@ function modelRoute(ctx) {
9370
8980
  throw new Error("Cannot record review provenance because Pi did not provide an active model route.");
9371
8981
  return `${ctx.model.provider}/${ctx.model.id}`;
9372
8982
  }
9373
- async function publishResolvedReview(review, params, model) {
8983
+ async function publishResolvedReview(review, params) {
9374
8984
  const status = params.status ?? "COMMENT";
9375
8985
  const event = status === "CLOSE" ? "COMMENT" : status;
9376
8986
  assertReviewEventSupported(review.vcs.provider, event);
9377
- const remote = createRemoteReviewBackend(review.vcs, review.pr.number);
9378
- const promotion = params.local ? await promoteLocalReview(review, remote, model) : undefined;
9379
8987
  if (status !== "CLOSE" && review.pr.isDraft)
9380
8988
  await review.forge.markReady(review.pr.number);
9381
- await remote.publish(event);
9382
- if (promotion) {
9383
- promotion.publication.state.bodies = [
9384
- ...new Set([...promotion.publication.state.bodies, ...promotion.bodyFingerprints])
9385
- ];
9386
- promotion.publication.state.comments = [
9387
- ...new Set([...promotion.publication.state.comments, ...promotion.commentFingerprints])
9388
- ];
9389
- const published = new Set(promotion.commentFingerprints);
9390
- promotion.publication.state.stagedComments = promotion.publication.state.stagedComments.filter((fingerprint) => !published.has(fingerprint));
9391
- await saveReviewPublicationState(promotion.publication.path, promotion.publication.state);
9392
- }
8989
+ await createRemoteReviewBackend(review.vcs, review.pr.number).publish(event);
9393
8990
  if (status === "CLOSE")
9394
8991
  await review.forge.closePr(review.pr.number);
9395
8992
  const finalPr = await review.forge.viewPr(String(review.pr.number));
9396
- const promotedBodies = promotion?.promotedBodies ?? 0;
9397
- const promotedComments = promotion?.promotedComments ?? 0;
9398
- const promotedReplies = promotion?.promotedReplies ?? 0;
9399
- return result3(`Published #${review.pr.number} (${status}); promoted ${promotedBodies} review bodies, ${promotedComments} comments, and ${promotedReplies} replies.`, { pr: finalPr ?? review.pr, status, promotedBodies, promotedComments, promotedReplies });
9400
- }
9401
- async function promoteLocalReview(review, remote, model) {
9402
- const publication = await loadReviewPublicationState(review.cwd, review.vcs, review.pr.number);
9403
- const session = await resolvePublishSession(review.cwd, {
9404
- branch: review.vcs.branch,
9405
- owner: review.vcs.provider === "none" ? undefined : review.vcs.owner,
9406
- repo: review.vcs.provider === "none" ? undefined : review.vcs.repo,
9407
- number: review.pr?.number
9408
- });
9409
- if (!session)
9410
- throw new Error("No matching tuicr session to publish.");
9411
- const sessionIsWorkingTree = session.kind === "local";
9412
- const local = createLocalReviewBackend({
9413
- session: session.path,
9414
- artifactPath: "",
9415
- author: localReviewAuthor(model)
9416
- });
9417
- const draft = await local.readDraft();
9418
- const remoteDraft = await remote.readDraft();
9419
- const knownRootComments = new Set([
9420
- ...publication.state.comments,
9421
- ...publication.state.stagedComments,
9422
- ...remoteDraft.comments.map(reviewCommentFingerprint)
9423
- ]);
9424
- const { checkpointed, candidates } = partitionReviewComments(draft.comments, knownRootComments, model);
9425
- const remoteThreads = await remote.listThreads();
9426
- const threadReplies = sessionIsWorkingTree ? [] : candidates.flatMap(({ source: comment }) => {
9427
- const thread = resolveReviewThread(comment, remoteThreads);
9428
- return thread ? [{ comment, thread }] : [];
9429
- });
9430
- const unmatchedActions = sessionIsWorkingTree ? [] : candidates.map(({ source }) => source).filter((comment) => parseReviewThreadAction(comment.body).action && !threadReplies.some((reply) => reply.comment === comment));
9431
- if (unmatchedActions.length > 0) {
9432
- const locations = unmatchedActions.map(({ file, line }) => `${file}:${line ?? "?"}`).join(", ");
9433
- throw new Error(`Cannot apply review thread action: no matching remote thread at ${locations}.`);
9434
- }
9435
- const comments = candidates.filter(({ source }) => !threadReplies.some((reply) => reply.comment === source)).map(({ published }) => published);
9436
- const body = draft.body.trim();
9437
- const bodyFingerprints = body ? [reviewBodyFingerprint(body)] : [];
9438
- const commentFingerprints = [
9439
- ...checkpointed.map(({ fingerprint }) => fingerprint),
9440
- ...comments.map(reviewCommentFingerprint)
9441
- ];
9442
- const knownBodies = new Set(publication.state.bodies);
9443
- if (remoteDraft.body.trim())
9444
- knownBodies.add(reviewBodyFingerprint(remoteDraft.body));
9445
- const unpublishedBody = body && !knownBodies.has(reviewBodyFingerprint(body)) ? body : "";
9446
- const unpublished = unpublishedReviewComments(comments, knownRootComments);
9447
- if (unpublishedBody)
9448
- await remote.stage({ comments: [], body: unpublishedBody });
9449
- let promotedComments = 0;
9450
- for (const comment of unpublished) {
9451
- await remote.stage({ comments: [comment], body: "" });
9452
- publication.state.stagedComments.push(reviewCommentFingerprint(comment));
9453
- await saveReviewPublicationState(publication.path, publication.state);
9454
- promotedComments += 1;
9455
- }
9456
- const promotedReplies = await promoteLocalReplies(review, remote, publication, model, sessionIsWorkingTree) + await promoteLocationReplies(remote, publication, threadReplies, model);
9457
- return {
9458
- publication,
9459
- bodyFingerprints,
9460
- commentFingerprints,
9461
- promotedBodies: unpublishedBody ? 1 : 0,
9462
- promotedComments,
9463
- promotedReplies
9464
- };
9465
- }
9466
- async function promoteLocationReplies(remote, publication, replies, model) {
9467
- let count = 0;
9468
- for (const { comment, thread } of replies) {
9469
- const parsed = parseReviewThreadAction(comment.body);
9470
- const body = parsed.body.trim();
9471
- const action = parsed.action;
9472
- const fingerprint = reviewReplyFingerprint(thread.id, `${action ?? "reply"}\x00${body}`);
9473
- if (publication.state.replies.includes(fingerprint))
9474
- continue;
9475
- if (action === "delete") {
9476
- if (!remote.deleteThread)
9477
- throw new Error(`The review backend cannot delete thread ${thread.id}.`);
9478
- await remote.deleteThread(thread.id);
9479
- publication.state.replies.push(fingerprint);
9480
- await saveReviewPublicationState(publication.path, publication.state);
9481
- count += 1;
9482
- continue;
9483
- }
9484
- const remoteBody = withCommentProvenance({ ...comment, body }, model);
9485
- if (body && !thread.replies?.some((reply) => reply === body || reply === remoteBody)) {
9486
- await remote.reply({
9487
- threadId: thread.id,
9488
- body: remoteBody,
9489
- resolve: false
9490
- });
9491
- }
9492
- if (action && remote.setResolved)
9493
- await remote.setResolved(thread.id, action === "resolve");
9494
- publication.state.replies.push(fingerprint);
9495
- await saveReviewPublicationState(publication.path, publication.state);
9496
- count += 1;
9497
- }
9498
- return count;
9499
- }
9500
- async function promoteLocalReplies(review, remote, publication, model, workingTree) {
9501
- const target = workingTree ? "uncommitted" : await reviewTargetId(review);
9502
- const artifact = workingTree ? await latestThreadArtifact(review.cwd, review, target) : publication.state.overlayPath ?? await latestThreadArtifact(review.cwd, review, target);
9503
- if (!artifact)
9504
- return 0;
9505
- if (!workingTree)
9506
- publication.state.overlayPath = artifact;
9507
- let count = 0;
9508
- for (const thread of await readThreadArtifact(artifact)) {
9509
- if (!thread.reply)
9510
- continue;
9511
- const body = withRemoteProvenance(thread.reply, model);
9512
- const fingerprint = reviewReplyFingerprint(thread.id, body);
9513
- if (publication.state.replies.includes(fingerprint))
9514
- continue;
9515
- await remote.reply({
9516
- threadId: thread.id,
9517
- body,
9518
- resolve: !thread.question,
9519
- question: thread.question
9520
- });
9521
- publication.state.replies.push(fingerprint);
9522
- await saveReviewPublicationState(publication.path, publication.state);
9523
- count += 1;
9524
- }
9525
- return count;
8993
+ return result3(`Published #${review.pr.number} (${status}).`, { pr: finalPr ?? review.pr, status });
9526
8994
  }
9527
8995
  async function resolveReviewContext(cwd, target) {
9528
8996
  const vcs = await detectVcs(cwd);
@@ -9563,10 +9031,6 @@ async function assertRemoteBranchReady(cwd) {
9563
9031
  if (head.stdout.trim() !== pushed.stdout.trim())
9564
9032
  throw new Error("Push the current branch before opening a remote review.");
9565
9033
  }
9566
- function withCommentProvenance(comment, fallbackModel) {
9567
- const route = comment.author?.match(/^Agent:\s*(.+)$/)?.[1];
9568
- return route ? withRemoteProvenance(comment.body, route || fallbackModel) : comment.body;
9569
- }
9570
9034
  async function reviewTargetId(review) {
9571
9035
  if (!review.pr)
9572
9036
  return "uncommitted";
@@ -9583,60 +9047,9 @@ async function headSha(cwd) {
9583
9047
  }
9584
9048
  async function newReviewArtifactPath(cwd, target) {
9585
9049
  const dir = await reviewsDir(cwd);
9586
- await mkdir7(dir, { recursive: true });
9050
+ await mkdir8(dir, { recursive: true });
9587
9051
  return uniqueRecordPath(dir, reviewRecordName(target));
9588
9052
  }
9589
- async function localThreadArtifactPath(cwd, sessionSlug) {
9590
- const dir = await reviewsDir(cwd);
9591
- return join16(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
9592
- }
9593
- async function readThreadArtifact(path) {
9594
- try {
9595
- return parseThreadArtifact(await readFile11(path, "utf8"));
9596
- } catch (error) {
9597
- if (error.code === "ENOENT") {
9598
- throw new Error(`Expected local reply overlay is missing: ${path}`);
9599
- }
9600
- throw error;
9601
- }
9602
- }
9603
- async function latestThreadArtifact(cwd, review, target) {
9604
- try {
9605
- return await findLatestThreadArtifact(cwd, review, target);
9606
- } catch (error) {
9607
- throw new Error(`Cannot locate the latest review thread artifact for ${target}.`, { cause: error });
9608
- }
9609
- }
9610
- async function findLatestThreadArtifact(cwd, review, target) {
9611
- const dir = await reviewsDir(cwd);
9612
- const suffix = reviewSlug(target) || "uncommitted";
9613
- const names = await listReviewArtifactNames(dir);
9614
- const candidates = await Promise.all(names.flatMap((name) => {
9615
- if (!name.endsWith(".md"))
9616
- return [];
9617
- return [
9618
- (async () => {
9619
- const path = join16(dir, name);
9620
- const info = await stat(path);
9621
- return { path, name, content: await readFile11(path, "utf8"), modified: info.mtimeMs };
9622
- })()
9623
- ];
9624
- }));
9625
- return candidates.filter(({ name, content }) => {
9626
- if (!content.startsWith("<!-- diffpi-threads:"))
9627
- return false;
9628
- if (review.pr && target !== "local" && target !== "uncommitted")
9629
- return content.includes(`- PR/MR: #${review.pr.number}`);
9630
- return artifactNameMatches(name, suffix);
9631
- }).sort((a, b) => b.modified - a.modified)[0]?.path;
9632
- }
9633
- async function listReviewArtifactNames(dir) {
9634
- try {
9635
- return await readdir2(dir);
9636
- } catch (error) {
9637
- throw new Error(`Cannot read review artifacts in ${dir}.`, { cause: error });
9638
- }
9639
- }
9640
9053
  function deriveTitle(branch) {
9641
9054
  return branch.replace(/^(feature|feat|fix|bug|chore)\//, "").replace(/^eng-\d+-/i, "").replace(/[-_]+/g, " ").replace(/^\w/, (char) => char.toUpperCase());
9642
9055
  }
@@ -9647,39 +9060,28 @@ function uniqueRecordPath(dir, base) {
9647
9060
  path = join16(dir, `${base}-${count++}.md`);
9648
9061
  return path;
9649
9062
  }
9650
- function artifactNameMatches(name, suffix) {
9651
- if (!name.endsWith(".md"))
9652
- return false;
9653
- const stem = name.slice(0, -3);
9654
- const marker = `-${suffix}`;
9655
- const markerIndex = stem.lastIndexOf(marker);
9656
- if (markerIndex < 0)
9657
- return false;
9658
- const tail = stem.slice(markerIndex + marker.length);
9659
- return tail === "" || /^-\d+$/.test(tail);
9660
- }
9661
9063
 
9662
9064
  // src/tools/setup.ts
9663
9065
  import { defineTool as defineTool7 } from "@earendil-works/pi-coding-agent";
9664
- import { z as z13 } from "zod";
9066
+ import { z as z11 } from "zod";
9665
9067
 
9666
9068
  // src/setup.ts
9667
9069
  import { parseFrontmatter as parseFrontmatter2 } from "@earendil-works/pi-coding-agent";
9668
- import { readdir as readdir4, readFile as readFile14 } from "node:fs/promises";
9070
+ import { readdir as readdir4, readFile as readFile12 } from "node:fs/promises";
9669
9071
  import { homedir as homedir8 } from "node:os";
9670
9072
  import { basename as basename6, join as join20 } from "node:path";
9671
9073
 
9672
9074
  // src/config.ts
9673
9075
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
9674
- import { z as z12 } from "zod";
9076
+ import { z as z10 } from "zod";
9675
9077
  import { homedir as homedir5 } from "node:os";
9676
9078
  import { join as join17 } from "node:path";
9677
9079
 
9678
9080
  // src/fsx.ts
9679
- import { readdir as readdir3, readFile as readFile12 } from "node:fs/promises";
9081
+ import { readdir as readdir3, readFile as readFile10 } from "node:fs/promises";
9680
9082
  async function readTextIfExists(path) {
9681
9083
  try {
9682
- return await readFile12(path, "utf8");
9084
+ return await readFile10(path, "utf8");
9683
9085
  } catch (error) {
9684
9086
  if (isMissingPath(error))
9685
9087
  return;
@@ -9692,11 +9094,11 @@ function isMissingPath(error) {
9692
9094
 
9693
9095
  // src/config.ts
9694
9096
  var modelReferenceSchema = zx.text;
9695
- var agentConfigSchema = z12.object({
9696
- models: z12.array(modelReferenceSchema).optional()
9097
+ var agentConfigSchema = z10.object({
9098
+ models: z10.array(modelReferenceSchema).optional()
9697
9099
  }).strict();
9698
- var diffpiConfigSchema = z12.object({
9699
- agents: z12.record(z12.string(), agentConfigSchema).optional()
9100
+ var diffpiConfigSchema = z10.object({
9101
+ agents: z10.record(z10.string(), agentConfigSchema).optional()
9700
9102
  }).strict();
9701
9103
  function diffpiConfigPaths(homeDir = homedir5()) {
9702
9104
  const directory = join17(homeDir, ".difflab", "diffpi");
@@ -9758,7 +9160,7 @@ function normalizeModelReference(value) {
9758
9160
  }
9759
9161
 
9760
9162
  // src/mcp.ts
9761
- import { mkdir as mkdir8, readFile as readFile13, writeFile as writeFile8 } from "node:fs/promises";
9163
+ import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile7 } from "node:fs/promises";
9762
9164
  import { homedir as homedir6 } from "node:os";
9763
9165
  import { dirname as dirname8, join as join18 } from "node:path";
9764
9166
  var mcp = {
@@ -9776,8 +9178,8 @@ var mcp = {
9776
9178
  const next = { ...current, mcpServers: nextServers };
9777
9179
  const changed = JSON.stringify(current) !== JSON.stringify(next);
9778
9180
  if (changed && !options.dryRun) {
9779
- await mkdir8(dirname8(path), { recursive: true });
9780
- await writeFile8(path, `${JSON.stringify(next, null, 2)}
9181
+ await mkdir9(dirname8(path), { recursive: true });
9182
+ await writeFile7(path, `${JSON.stringify(next, null, 2)}
9781
9183
  `, "utf8");
9782
9184
  }
9783
9185
  return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
@@ -9806,7 +9208,7 @@ function getParsedConfig(content, path) {
9806
9208
  }
9807
9209
  async function getOptionalFile2(path) {
9808
9210
  try {
9809
- return await readFile13(path, "utf8");
9211
+ return await readFile11(path, "utf8");
9810
9212
  } catch (error) {
9811
9213
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
9812
9214
  return;
@@ -9818,7 +9220,7 @@ function isRecord(value) {
9818
9220
  }
9819
9221
 
9820
9222
  // src/pi.ts
9821
- import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
9223
+ import { mkdir as mkdir10, writeFile as writeFile8 } from "node:fs/promises";
9822
9224
  import { homedir as homedir7 } from "node:os";
9823
9225
  import { dirname as dirname9, join as join19 } from "node:path";
9824
9226
  var pi = {
@@ -9837,8 +9239,8 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
9837
9239
  const currentText = await readTextIfExists(path);
9838
9240
  const changed = currentText !== content;
9839
9241
  if (changed && !dryRun) {
9840
- await mkdir9(dirname9(path), { recursive: true });
9841
- await writeFile9(path, content, "utf8");
9242
+ await mkdir10(dirname9(path), { recursive: true });
9243
+ await writeFile8(path, content, "utf8");
9842
9244
  }
9843
9245
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
9844
9246
  }
@@ -9874,8 +9276,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
9874
9276
  const next = update(current);
9875
9277
  const changed = JSON.stringify(current) !== JSON.stringify(next);
9876
9278
  if (changed && !dryRun) {
9877
- await mkdir9(dirname9(path), { recursive: true });
9878
- await writeFile9(path, `${JSON.stringify(next, null, 2)}
9279
+ await mkdir10(dirname9(path), { recursive: true });
9280
+ await writeFile8(path, `${JSON.stringify(next, null, 2)}
9879
9281
  `, "utf8");
9880
9282
  }
9881
9283
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
@@ -10013,7 +9415,7 @@ async function ensurePiAgents(options = {}) {
10013
9415
  const actions = [];
10014
9416
  for (const entry of entries) {
10015
9417
  const id = basename6(entry.name, ".md").replace(/^diffpi-/, "");
10016
- const source = await readFile14(join20(bundledAgentsDir, entry.name), "utf8");
9418
+ const source = await readFile12(join20(bundledAgentsDir, entry.name), "utf8");
10017
9419
  const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
10018
9420
  const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
10019
9421
  actions.push(getConfigSetupAction(`pi agent ${id}`, result));
@@ -10218,7 +9620,7 @@ function reportProgress(options, message) {
10218
9620
  options.onProgress?.(message);
10219
9621
  }
10220
9622
  async function packageVersion2() {
10221
- const source = await readFile14(join20(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
9623
+ const source = await readFile12(join20(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
10222
9624
  let value;
10223
9625
  try {
10224
9626
  value = JSON.parse(source);
@@ -10234,13 +9636,13 @@ function getRecord(value) {
10234
9636
  }
10235
9637
 
10236
9638
  // src/tools/setup.ts
10237
- var setupParametersSchema = z13.object({
10238
- issueTracker: z13.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira."),
10239
- forges: z13.array(z13.enum(["github", "gitlab"])).default([]).describe("Hosted VCS MCP servers to configure. Installs matching CLIs and registers each MCP server."),
10240
- forge: z13.enum(["none", "github", "gitlab"]).optional().describe("Deprecated single-host alias. Prefer forges for one or more VCS MCP servers."),
10241
- bindZedKey: z13.boolean().default(false).describe("Opt in to a Zed keybinding for the tuicr review task.")
9639
+ var setupParametersSchema = z11.object({
9640
+ issueTracker: z11.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira."),
9641
+ forges: z11.array(z11.enum(["github", "gitlab"])).default([]).describe("Hosted VCS MCP servers to configure. Installs matching CLIs and registers each MCP server."),
9642
+ forge: z11.enum(["none", "github", "gitlab"]).optional().describe("Deprecated single-host alias. Prefer forges for one or more VCS MCP servers."),
9643
+ bindZedKey: z11.boolean().default(false).describe("Opt in to a Zed keybinding for the tuicr review task.")
10242
9644
  });
10243
- var setupParameters = z13.toJSONSchema(setupParametersSchema, { io: "input" });
9645
+ var setupParameters = z11.toJSONSchema(setupParametersSchema, { io: "input" });
10244
9646
  var diffpiSetupTool = defineTool7({
10245
9647
  name: "diffpi_setup",
10246
9648
  label: "diffpi setup",
@@ -10313,11 +9715,11 @@ ${lines.join(`
10313
9715
 
10314
9716
  // src/tools/templates.ts
10315
9717
  import { defineTool as defineTool8 } from "@earendil-works/pi-coding-agent";
10316
- import { z as z14 } from "zod";
10317
- var templateSchema = z14.object({
10318
- name: z14.string().min(1),
10319
- variables: z14.record(z14.string(), z14.string()).optional(),
10320
- homeDir: z14.string().optional()
9718
+ import { z as z12 } from "zod";
9719
+ var templateSchema = z12.object({
9720
+ name: z12.string().min(1),
9721
+ variables: z12.record(z12.string(), z12.string()).optional(),
9722
+ homeDir: z12.string().optional()
10321
9723
  });
10322
9724
  var diffpiTemplateTool = defineTool8({
10323
9725
  name: "diffpi_template",
@@ -10325,7 +9727,7 @@ var diffpiTemplateTool = defineTool8({
10325
9727
  description: "Load a bundled Diffpi template or a user override and render named variables.",
10326
9728
  promptSnippet: "Use diffpi_template for package workflow templates",
10327
9729
  promptGuidelines: ["User overrides live under ~/.difflab/diffpi/templates."],
10328
- parameters: z14.toJSONSchema(templateSchema, { io: "input" }),
9730
+ parameters: z12.toJSONSchema(templateSchema, { io: "input" }),
10329
9731
  executionMode: "parallel",
10330
9732
  async execute(_id, input) {
10331
9733
  const params = templateSchema.parse(input);