@difflab/pi 0.3.0-rc.202609201751.b819639 → 0.3.0-rc.202609201843.9cb5906

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.
@@ -14,7 +14,7 @@ metadata:
14
14
 
15
15
  You review code changes. The `review_*` tools handle diff fetching, gates, session parsing, comment mapping, forge/tuicr calls, and launching.
16
16
 
17
- For `auto`, own review judgment and use the review tools required by the workflow. For `address`, act as the review coordinator: classify threads, form non-overlapping bounded implementation tasks, delegate routine edits to `worker`, collect verification and outcomes, then call `review_respond` for every thread. Do not publish, complete, or merge. Read `skills/review/references/review-standards.md` before classifying threads.
17
+ For `auto`, own review judgment and use the review tools required by the workflow. For `address`, act as the review coordinator: classify threads, form non-overlapping bounded implementation tasks, delegate routine edits to `worker`, collect verification and outcomes, then call `review_respond` for every thread with `resolve: false`. Never resolve or delete a thread; the user owns resolution. Do not publish, complete, or merge. Read `skills/review/references/review-standards.md` before classifying threads.
18
18
 
19
19
  ## Rules
20
20
 
@@ -34,4 +34,4 @@ For `auto`, own review judgment and use the review tools required by the workflo
34
34
 
35
35
  For `auto`, return findings for `review_submit` as `{ file, line, severity, body, reference }`, with severity `BLOCKING`, `CONSIDER`, or `NOTE`. Put un-anchorable BLOCKING issues in `overallIssues`. Return `[]` when clean.
36
36
 
37
- For `address`, return one outcome per source thread: `fixed`, `answered`, `resolved`, `deferred`, or `unresolved`; include its exact response text and verification evidence. Questions are answered and stay open. Only trivial accepted requests use `Resolved` and may close; substantive fixes stay open for reviewer confirmation.
37
+ For `address`, return one outcome per source thread: `fixed`, `answered`, `unresolved`, or `deferred`; include its exact response text and verification evidence. Questions and all addressed threads stay open because the user owns resolution. Never report a thread as resolved during address.
@@ -21712,7 +21712,7 @@ function isLocalResponse(body) {
21712
21712
  return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
21713
21713
  }
21714
21714
  function parseReviewThreadAction(body) {
21715
- const match = body.match(/^\[(REOPEN|RESOLVE)\]\s*/i);
21715
+ const match = body.match(/^\[(REOPEN|RESOLVE|DELETE)\]\s*/i);
21716
21716
  if (!match)
21717
21717
  return { body };
21718
21718
  return {
@@ -21961,6 +21961,8 @@ function GitHubReviewBackend(vcs, number) {
21961
21961
  id: thread.id,
21962
21962
  file: thread.path,
21963
21963
  line: thread.line,
21964
+ rootCommentId: comment?.id,
21965
+ commentIds: nodes.flatMap((node) => node.id ? [node.id] : []),
21964
21966
  body,
21965
21967
  author: comment?.author?.login,
21966
21968
  resolved: thread.isResolved,
@@ -21976,12 +21978,12 @@ function GitHubReviewBackend(vcs, number) {
21976
21978
  if (!pending) {
21977
21979
  const payload = {
21978
21980
  body: draft.body,
21979
- comments: draft.comments.map((comment) => ({
21981
+ comments: draft.comments.map((comment) => comment.line === undefined ? { path: comment.file, subject_type: "file", body: comment.body } : {
21980
21982
  path: comment.file,
21981
21983
  line: comment.line,
21982
21984
  side: comment.side ?? "RIGHT",
21983
21985
  body: comment.body
21984
- }))
21986
+ })
21985
21987
  };
21986
21988
  await ghChecked(["api", "--method", "POST", `/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews`, "--input", "-"], { input: JSON.stringify(payload) });
21987
21989
  return;
@@ -21997,22 +21999,22 @@ function GitHubReviewBackend(vcs, number) {
21997
21999
  ]);
21998
22000
  }
21999
22001
  for (const comment of draft.comments) {
22000
- await ghChecked([
22002
+ const args = [
22001
22003
  "api",
22002
22004
  "graphql",
22003
22005
  "-f",
22004
- `query=${GITHUB_ADD_THREAD_MUTATION}`,
22006
+ `query=${comment.line === undefined ? GITHUB_ADD_FILE_THREAD_MUTATION : GITHUB_ADD_THREAD_MUTATION}`,
22005
22007
  "-f",
22006
22008
  `reviewId=${pending.nodeId}`,
22007
22009
  "-f",
22008
22010
  `body=${comment.body}`,
22009
22011
  "-f",
22010
- `path=${comment.file}`,
22011
- "-F",
22012
- `line=${comment.line}`,
22013
- "-f",
22014
- `side=${comment.side ?? "RIGHT"}`
22015
- ]);
22012
+ `path=${comment.file}`
22013
+ ];
22014
+ if (comment.line !== undefined) {
22015
+ args.push("-F", `line=${comment.line}`, "-f", `side=${comment.side ?? "RIGHT"}`);
22016
+ }
22017
+ await ghChecked(args);
22016
22018
  }
22017
22019
  },
22018
22020
  async readDraft() {
@@ -22036,7 +22038,7 @@ function GitHubReviewBackend(vcs, number) {
22036
22038
  body: reviewData.body ?? "",
22037
22039
  comments: commentData.map((comment) => ({
22038
22040
  file: comment.path,
22039
- line: comment.line ?? comment.original_line ?? 1,
22041
+ line: comment.line ?? comment.original_line,
22040
22042
  side: comment.side,
22041
22043
  body: comment.body
22042
22044
  }))
@@ -22071,6 +22073,22 @@ function GitHubReviewBackend(vcs, number) {
22071
22073
  `threadId=${threadId}`
22072
22074
  ]);
22073
22075
  },
22076
+ async deleteThread(threadId) {
22077
+ const thread = (await listThreads()).find((candidate) => candidate.id === threadId);
22078
+ const commentIds = thread?.commentIds ?? (thread?.rootCommentId ? [thread.rootCommentId] : []);
22079
+ if (!commentIds.length)
22080
+ throw new Error(`Cannot delete GitHub thread ${threadId}: comment IDs are missing.`);
22081
+ for (const commentId of commentIds) {
22082
+ await ghChecked([
22083
+ "api",
22084
+ "graphql",
22085
+ "-f",
22086
+ `query=${GITHUB_DELETE_COMMENT_MUTATION}`,
22087
+ "-f",
22088
+ `commentId=${commentId}`
22089
+ ]);
22090
+ }
22091
+ },
22074
22092
  async publish(event) {
22075
22093
  const pending = await pendingReview();
22076
22094
  if (!pending && event === "COMMENT")
@@ -22086,11 +22104,13 @@ function GitHubReviewBackend(vcs, number) {
22086
22104
  function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
22087
22105
  return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
22088
22106
  }
22089
- 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}}}}}`;
22107
+ 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}}}}}`;
22090
22108
  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}}}`;
22109
+ 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}}}`;
22091
22110
  var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
22092
22111
  var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
22093
22112
  var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
22113
+ var GITHUB_DELETE_COMMENT_MUTATION = `mutation($commentId:ID!){deletePullRequestReviewComment(input:{id:$commentId}){clientMutationId}}`;
22094
22114
 
22095
22115
  // src/review/gitlab-review-backend.ts
22096
22116
  function GitLabReviewBackend(vcs, number) {
@@ -22203,6 +22223,10 @@ function GitLabReviewBackend(vcs, number) {
22203
22223
  const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
22204
22224
  await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
22205
22225
  },
22226
+ async deleteThread(threadId) {
22227
+ const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
22228
+ await glabChecked(["api", "--method", "DELETE", endpoint]);
22229
+ },
22206
22230
  async publish(event) {
22207
22231
  assertReviewEventSupported(vcs.provider, event);
22208
22232
  const drafts = await glabChecked(["api", `${mergeRequestEndpoint()}/draft_notes`]);
@@ -22385,15 +22409,22 @@ function toLocalReviewThreads(session) {
22385
22409
  function toFindings(session, options = {}) {
22386
22410
  const comments = [];
22387
22411
  const include = (comment) => (!options.agentOnly || commentAuthor(comment)?.startsWith("Agent: ")) && (!options.excludeLocalResponses || !isLocalResponse(comment.content));
22388
- const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [comment.content] : []);
22412
+ const publishableBody = (comment) => {
22413
+ const author = commentAuthor(comment);
22414
+ const route = author?.match(/^Agent:\s*(.+)$/)?.[1];
22415
+ return route ? withRemoteProvenance(comment.content, route) : comment.content;
22416
+ };
22417
+ const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [publishableBody(comment)] : []);
22389
22418
  for (const [file, entry] of Object.entries(session.files ?? {})) {
22390
- const fileComments = (entry.file_comments ?? []).flatMap((comment) => include(comment) ? [comment.content] : []);
22391
- if (fileComments.length > 0)
22392
- bodyParts.push(`File: ${file}
22393
-
22394
- ${fileComments.join(`
22395
-
22396
- `)}`);
22419
+ for (const fileComment of entry.file_comments ?? []) {
22420
+ if (!include(fileComment))
22421
+ continue;
22422
+ const comment = { file, body: fileComment.content };
22423
+ const author = commentAuthor(fileComment);
22424
+ if (author)
22425
+ comment.author = author;
22426
+ comments.push(comment);
22427
+ }
22397
22428
  for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
22398
22429
  const line = Number.parseInt(lineKey, 10);
22399
22430
  if (!Number.isFinite(line))
@@ -23113,14 +23144,19 @@ async function promoteLocalReview(review, remote, model) {
23113
23144
  const draft = await local.readDraft();
23114
23145
  const remoteThreads = await remote.listThreads();
23115
23146
  const threadReplies = sessionIsWorkingTree ? [] : draft.comments.flatMap((comment) => {
23116
- const thread = remoteThreads.find((candidate) => candidate.file === comment.file && candidate.line === comment.line && candidate.line !== undefined && !candidate.body.includes("Generated review by Diffpi using"));
23147
+ const thread = remoteThreads.find((candidate) => candidate.file === comment.file && candidate.line === comment.line && !candidate.body.includes("Generated review by Diffpi using"));
23117
23148
  return thread ? [{ comment, thread }] : [];
23118
23149
  });
23150
+ const unmatchedActions = sessionIsWorkingTree ? [] : draft.comments.filter((comment) => parseReviewThreadAction(comment.body).action && !threadReplies.some((reply) => reply.comment === comment));
23151
+ if (unmatchedActions.length > 0) {
23152
+ const locations = unmatchedActions.map(({ file, line }) => `${file}:${line ?? "?"}`).join(", ");
23153
+ throw new Error(`Cannot apply review thread action: no matching remote thread at ${locations}.`);
23154
+ }
23119
23155
  const comments = draft.comments.filter(({ file, line }) => !threadReplies.some((reply) => reply.comment.file === file && reply.comment.line === line)).map((comment) => ({
23120
23156
  ...comment,
23121
- body: withRemoteProvenance(comment.body, comment.author?.replace(/^Agent:\s*/, "") || model)
23157
+ body: withCommentProvenance(comment, model)
23122
23158
  }));
23123
- const body = draft.body.trim() ? withRemoteProvenance(draft.body, model) : "";
23159
+ const body = draft.body.trim();
23124
23160
  const bodyFingerprints = body ? [reviewBodyFingerprint(body)] : [];
23125
23161
  const commentFingerprints = comments.map(reviewCommentFingerprint);
23126
23162
  const remoteDraft = await remote.readDraft();
@@ -23151,7 +23187,16 @@ async function promoteLocationReplies(remote, publication, replies, model) {
23151
23187
  const fingerprint = reviewReplyFingerprint(thread.id, `${action ?? "reply"}\x00${body}`);
23152
23188
  if (publication.state.replies.includes(fingerprint))
23153
23189
  continue;
23154
- const remoteBody = withRemoteProvenance(body, comment.author?.replace(/^Agent:\s*/, "") || model);
23190
+ if (action === "delete") {
23191
+ if (!remote.deleteThread)
23192
+ throw new Error(`The review backend cannot delete thread ${thread.id}.`);
23193
+ await remote.deleteThread(thread.id);
23194
+ publication.state.replies.push(fingerprint);
23195
+ await saveReviewPublicationState(publication.path, publication.state);
23196
+ count += 1;
23197
+ continue;
23198
+ }
23199
+ const remoteBody = withCommentProvenance({ ...comment, body }, model);
23155
23200
  if (body && !thread.replies?.some((reply) => reply === body || reply === remoteBody)) {
23156
23201
  await remote.reply({
23157
23202
  threadId: thread.id,
@@ -23233,6 +23278,10 @@ async function assertRemoteBranchReady(cwd) {
23233
23278
  if (head.stdout.trim() !== pushed.stdout.trim())
23234
23279
  throw new Error("Push the current branch before opening a remote review.");
23235
23280
  }
23281
+ function withCommentProvenance(comment, fallbackModel) {
23282
+ const route = comment.author?.match(/^Agent:\s*(.+)$/)?.[1];
23283
+ return route ? withRemoteProvenance(comment.body, route || fallbackModel) : comment.body;
23284
+ }
23236
23285
  async function reviewTargetId(review) {
23237
23286
  if (!review.pr)
23238
23287
  return "uncommitted";
@@ -1 +1 @@
1
- {"version":3,"file":"tuicrx.d.ts","sourceRoot":"","sources":["../../src/extensions/tuicrx.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tuicrx.d.ts","sourceRoot":"","sources":["../../src/extensions/tuicrx.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1434,6 +1434,8 @@ function GitHubReviewBackend(vcs, number) {
1434
1434
  id: thread.id,
1435
1435
  file: thread.path,
1436
1436
  line: thread.line,
1437
+ rootCommentId: comment?.id,
1438
+ commentIds: nodes.flatMap((node) => node.id ? [node.id] : []),
1437
1439
  body,
1438
1440
  author: comment?.author?.login,
1439
1441
  resolved: thread.isResolved,
@@ -1449,12 +1451,12 @@ function GitHubReviewBackend(vcs, number) {
1449
1451
  if (!pending) {
1450
1452
  const payload = {
1451
1453
  body: draft.body,
1452
- comments: draft.comments.map((comment) => ({
1454
+ comments: draft.comments.map((comment) => comment.line === undefined ? { path: comment.file, subject_type: "file", body: comment.body } : {
1453
1455
  path: comment.file,
1454
1456
  line: comment.line,
1455
1457
  side: comment.side ?? "RIGHT",
1456
1458
  body: comment.body
1457
- }))
1459
+ })
1458
1460
  };
1459
1461
  await ghChecked(["api", "--method", "POST", `/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews`, "--input", "-"], { input: JSON.stringify(payload) });
1460
1462
  return;
@@ -1470,22 +1472,22 @@ function GitHubReviewBackend(vcs, number) {
1470
1472
  ]);
1471
1473
  }
1472
1474
  for (const comment of draft.comments) {
1473
- await ghChecked([
1475
+ const args = [
1474
1476
  "api",
1475
1477
  "graphql",
1476
1478
  "-f",
1477
- `query=${GITHUB_ADD_THREAD_MUTATION}`,
1479
+ `query=${comment.line === undefined ? GITHUB_ADD_FILE_THREAD_MUTATION : GITHUB_ADD_THREAD_MUTATION}`,
1478
1480
  "-f",
1479
1481
  `reviewId=${pending.nodeId}`,
1480
1482
  "-f",
1481
1483
  `body=${comment.body}`,
1482
1484
  "-f",
1483
- `path=${comment.file}`,
1484
- "-F",
1485
- `line=${comment.line}`,
1486
- "-f",
1487
- `side=${comment.side ?? "RIGHT"}`
1488
- ]);
1485
+ `path=${comment.file}`
1486
+ ];
1487
+ if (comment.line !== undefined) {
1488
+ args.push("-F", `line=${comment.line}`, "-f", `side=${comment.side ?? "RIGHT"}`);
1489
+ }
1490
+ await ghChecked(args);
1489
1491
  }
1490
1492
  },
1491
1493
  async readDraft() {
@@ -1509,7 +1511,7 @@ function GitHubReviewBackend(vcs, number) {
1509
1511
  body: reviewData.body ?? "",
1510
1512
  comments: commentData.map((comment) => ({
1511
1513
  file: comment.path,
1512
- line: comment.line ?? comment.original_line ?? 1,
1514
+ line: comment.line ?? comment.original_line,
1513
1515
  side: comment.side,
1514
1516
  body: comment.body
1515
1517
  }))
@@ -1544,6 +1546,22 @@ function GitHubReviewBackend(vcs, number) {
1544
1546
  `threadId=${threadId}`
1545
1547
  ]);
1546
1548
  },
1549
+ async deleteThread(threadId) {
1550
+ const thread = (await listThreads()).find((candidate) => candidate.id === threadId);
1551
+ const commentIds = thread?.commentIds ?? (thread?.rootCommentId ? [thread.rootCommentId] : []);
1552
+ if (!commentIds.length)
1553
+ throw new Error(`Cannot delete GitHub thread ${threadId}: comment IDs are missing.`);
1554
+ for (const commentId of commentIds) {
1555
+ await ghChecked([
1556
+ "api",
1557
+ "graphql",
1558
+ "-f",
1559
+ `query=${GITHUB_DELETE_COMMENT_MUTATION}`,
1560
+ "-f",
1561
+ `commentId=${commentId}`
1562
+ ]);
1563
+ }
1564
+ },
1547
1565
  async publish(event) {
1548
1566
  const pending = await pendingReview();
1549
1567
  if (!pending && event === "COMMENT")
@@ -1559,11 +1577,13 @@ function GitHubReviewBackend(vcs, number) {
1559
1577
  function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
1560
1578
  return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
1561
1579
  }
1562
- 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}}}}}`;
1580
+ 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}}}}}`;
1563
1581
  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}}}`;
1582
+ 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}}}`;
1564
1583
  var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
1565
1584
  var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
1566
1585
  var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
1586
+ var GITHUB_DELETE_COMMENT_MUTATION = `mutation($commentId:ID!){deletePullRequestReviewComment(input:{id:$commentId}){clientMutationId}}`;
1567
1587
 
1568
1588
  // src/review/gitlab-review-backend.ts
1569
1589
  function GitLabReviewBackend(vcs, number) {
@@ -1676,6 +1696,10 @@ function GitLabReviewBackend(vcs, number) {
1676
1696
  const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
1677
1697
  await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
1678
1698
  },
1699
+ async deleteThread(threadId) {
1700
+ const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
1701
+ await glabChecked(["api", "--method", "DELETE", endpoint]);
1702
+ },
1679
1703
  async publish(event) {
1680
1704
  assertReviewEventSupported(vcs.provider, event);
1681
1705
  const drafts = await glabChecked(["api", `${mergeRequestEndpoint()}/draft_notes`]);
@@ -1820,15 +1844,22 @@ async function readSession(path) {
1820
1844
  function toFindings(session, options = {}) {
1821
1845
  const comments = [];
1822
1846
  const include = (comment) => (!options.agentOnly || commentAuthor(comment)?.startsWith("Agent: ")) && (!options.excludeLocalResponses || !isLocalResponse(comment.content));
1823
- const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [comment.content] : []);
1847
+ const publishableBody = (comment) => {
1848
+ const author = commentAuthor(comment);
1849
+ const route = author?.match(/^Agent:\s*(.+)$/)?.[1];
1850
+ return route ? withRemoteProvenance(comment.content, route) : comment.content;
1851
+ };
1852
+ const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [publishableBody(comment)] : []);
1824
1853
  for (const [file, entry] of Object.entries(session.files ?? {})) {
1825
- const fileComments = (entry.file_comments ?? []).flatMap((comment) => include(comment) ? [comment.content] : []);
1826
- if (fileComments.length > 0)
1827
- bodyParts.push(`File: ${file}
1828
-
1829
- ${fileComments.join(`
1830
-
1831
- `)}`);
1854
+ for (const fileComment of entry.file_comments ?? []) {
1855
+ if (!include(fileComment))
1856
+ continue;
1857
+ const comment = { file, body: fileComment.content };
1858
+ const author = commentAuthor(fileComment);
1859
+ if (author)
1860
+ comment.author = author;
1861
+ comments.push(comment);
1862
+ }
1832
1863
  for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
1833
1864
  const line = Number.parseInt(lineKey, 10);
1834
1865
  if (!Number.isFinite(line))
@@ -1 +1 @@
1
- {"version":3,"file":"github-review-backend.d.ts","sourceRoot":"","sources":["../../src/review/github-review-backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,SAAS,CAAC;AAEjE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CA4M/E;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,eAAe,EAAE,MAAM,GACtB,MAAM,CAIR"}
1
+ {"version":3,"file":"github-review-backend.d.ts","sourceRoot":"","sources":["../../src/review/github-review-backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,SAAS,CAAC;AAEjE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAiO/E;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,eAAe,EAAE,MAAM,GACtB,MAAM,CAIR"}
@@ -1 +1 @@
1
- {"version":3,"file":"gitlab-review-backend.d.ts","sourceRoot":"","sources":["../../src/review/gitlab-review-backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE7D,OAAO,KAAK,EAAE,aAAa,EAAiB,WAAW,EAAsB,MAAM,SAAS,CAAC;AAE7F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CA+H/E;AAED,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAM5F;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAYjE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAO1D"}
1
+ {"version":3,"file":"gitlab-review-backend.d.ts","sourceRoot":"","sources":["../../src/review/gitlab-review-backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE7D,OAAO,KAAK,EAAE,aAAa,EAAiB,WAAW,EAAsB,MAAM,SAAS,CAAC;AAE7F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAmI/E;AAED,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAM5F;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAYjE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAO1D"}
@@ -4,7 +4,7 @@ export type ReviewSide = 'LEFT' | 'RIGHT';
4
4
  export type ReviewEvent = 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT';
5
5
  export interface ReviewComment {
6
6
  file: string;
7
- line: number;
7
+ line?: number;
8
8
  side?: ReviewSide;
9
9
  body: string;
10
10
  author?: string;
@@ -17,6 +17,8 @@ export interface ReviewThreadRecord {
17
17
  id: string;
18
18
  file?: string;
19
19
  line?: number;
20
+ rootCommentId?: string;
21
+ commentIds?: string[];
20
22
  body: string;
21
23
  author?: string;
22
24
  resolved: boolean;
@@ -38,6 +40,7 @@ export interface ReviewBackend {
38
40
  listThreads(): Promise<ReviewThreadRecord[]>;
39
41
  reply(input: ReviewReply): Promise<void>;
40
42
  setResolved?(threadId: string, resolved: boolean): Promise<void>;
43
+ deleteThread?(threadId: string): Promise<void>;
41
44
  publish(event: ReviewEvent): Promise<void>;
42
45
  }
43
46
  export interface LocalReviewBackendOptions {
@@ -101,7 +104,7 @@ export declare function yymmdd(date?: Date): string;
101
104
  export declare function reviewRecordName(target: string, date?: Date): string;
102
105
  export declare function localResponseMarker(threadId: string): string;
103
106
  export declare function isLocalResponse(body: string): boolean;
104
- export type ReviewThreadAction = 'reopen' | 'resolve';
107
+ export type ReviewThreadAction = 'reopen' | 'resolve' | 'delete';
105
108
  export declare function parseReviewThreadAction(body: string): {
106
109
  action?: ReviewThreadAction;
107
110
  body: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/review/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/review/types.ts"],"names":[],"mappings":"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"}
@@ -7705,7 +7705,7 @@ function isLocalResponse(body) {
7705
7705
  return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
7706
7706
  }
7707
7707
  function parseReviewThreadAction(body) {
7708
- const match = body.match(/^\[(REOPEN|RESOLVE)\]\s*/i);
7708
+ const match = body.match(/^\[(REOPEN|RESOLVE|DELETE)\]\s*/i);
7709
7709
  if (!match)
7710
7710
  return { body };
7711
7711
  return {
@@ -7954,6 +7954,8 @@ function GitHubReviewBackend(vcs, number) {
7954
7954
  id: thread.id,
7955
7955
  file: thread.path,
7956
7956
  line: thread.line,
7957
+ rootCommentId: comment?.id,
7958
+ commentIds: nodes.flatMap((node) => node.id ? [node.id] : []),
7957
7959
  body,
7958
7960
  author: comment?.author?.login,
7959
7961
  resolved: thread.isResolved,
@@ -7969,12 +7971,12 @@ function GitHubReviewBackend(vcs, number) {
7969
7971
  if (!pending) {
7970
7972
  const payload = {
7971
7973
  body: draft.body,
7972
- comments: draft.comments.map((comment) => ({
7974
+ comments: draft.comments.map((comment) => comment.line === undefined ? { path: comment.file, subject_type: "file", body: comment.body } : {
7973
7975
  path: comment.file,
7974
7976
  line: comment.line,
7975
7977
  side: comment.side ?? "RIGHT",
7976
7978
  body: comment.body
7977
- }))
7979
+ })
7978
7980
  };
7979
7981
  await ghChecked(["api", "--method", "POST", `/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews`, "--input", "-"], { input: JSON.stringify(payload) });
7980
7982
  return;
@@ -7990,22 +7992,22 @@ function GitHubReviewBackend(vcs, number) {
7990
7992
  ]);
7991
7993
  }
7992
7994
  for (const comment of draft.comments) {
7993
- await ghChecked([
7995
+ const args = [
7994
7996
  "api",
7995
7997
  "graphql",
7996
7998
  "-f",
7997
- `query=${GITHUB_ADD_THREAD_MUTATION}`,
7999
+ `query=${comment.line === undefined ? GITHUB_ADD_FILE_THREAD_MUTATION : GITHUB_ADD_THREAD_MUTATION}`,
7998
8000
  "-f",
7999
8001
  `reviewId=${pending.nodeId}`,
8000
8002
  "-f",
8001
8003
  `body=${comment.body}`,
8002
8004
  "-f",
8003
- `path=${comment.file}`,
8004
- "-F",
8005
- `line=${comment.line}`,
8006
- "-f",
8007
- `side=${comment.side ?? "RIGHT"}`
8008
- ]);
8005
+ `path=${comment.file}`
8006
+ ];
8007
+ if (comment.line !== undefined) {
8008
+ args.push("-F", `line=${comment.line}`, "-f", `side=${comment.side ?? "RIGHT"}`);
8009
+ }
8010
+ await ghChecked(args);
8009
8011
  }
8010
8012
  },
8011
8013
  async readDraft() {
@@ -8029,7 +8031,7 @@ function GitHubReviewBackend(vcs, number) {
8029
8031
  body: reviewData.body ?? "",
8030
8032
  comments: commentData.map((comment) => ({
8031
8033
  file: comment.path,
8032
- line: comment.line ?? comment.original_line ?? 1,
8034
+ line: comment.line ?? comment.original_line,
8033
8035
  side: comment.side,
8034
8036
  body: comment.body
8035
8037
  }))
@@ -8064,6 +8066,22 @@ function GitHubReviewBackend(vcs, number) {
8064
8066
  `threadId=${threadId}`
8065
8067
  ]);
8066
8068
  },
8069
+ async deleteThread(threadId) {
8070
+ const thread = (await listThreads()).find((candidate) => candidate.id === threadId);
8071
+ const commentIds = thread?.commentIds ?? (thread?.rootCommentId ? [thread.rootCommentId] : []);
8072
+ if (!commentIds.length)
8073
+ throw new Error(`Cannot delete GitHub thread ${threadId}: comment IDs are missing.`);
8074
+ for (const commentId of commentIds) {
8075
+ await ghChecked([
8076
+ "api",
8077
+ "graphql",
8078
+ "-f",
8079
+ `query=${GITHUB_DELETE_COMMENT_MUTATION}`,
8080
+ "-f",
8081
+ `commentId=${commentId}`
8082
+ ]);
8083
+ }
8084
+ },
8067
8085
  async publish(event) {
8068
8086
  const pending = await pendingReview();
8069
8087
  if (!pending && event === "COMMENT")
@@ -8079,11 +8097,13 @@ function GitHubReviewBackend(vcs, number) {
8079
8097
  function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
8080
8098
  return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
8081
8099
  }
8082
- 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}}}}}`;
8100
+ 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}}}}}`;
8083
8101
  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}}}`;
8102
+ 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}}}`;
8084
8103
  var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
8085
8104
  var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
8086
8105
  var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
8106
+ var GITHUB_DELETE_COMMENT_MUTATION = `mutation($commentId:ID!){deletePullRequestReviewComment(input:{id:$commentId}){clientMutationId}}`;
8087
8107
 
8088
8108
  // src/review/gitlab-review-backend.ts
8089
8109
  function GitLabReviewBackend(vcs, number) {
@@ -8196,6 +8216,10 @@ function GitLabReviewBackend(vcs, number) {
8196
8216
  const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
8197
8217
  await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
8198
8218
  },
8219
+ async deleteThread(threadId) {
8220
+ const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
8221
+ await glabChecked(["api", "--method", "DELETE", endpoint]);
8222
+ },
8199
8223
  async publish(event) {
8200
8224
  assertReviewEventSupported(vcs.provider, event);
8201
8225
  const drafts = await glabChecked(["api", `${mergeRequestEndpoint()}/draft_notes`]);
@@ -8378,15 +8402,22 @@ function toLocalReviewThreads(session) {
8378
8402
  function toFindings(session, options = {}) {
8379
8403
  const comments = [];
8380
8404
  const include = (comment) => (!options.agentOnly || commentAuthor(comment)?.startsWith("Agent: ")) && (!options.excludeLocalResponses || !isLocalResponse(comment.content));
8381
- const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [comment.content] : []);
8405
+ const publishableBody = (comment) => {
8406
+ const author = commentAuthor(comment);
8407
+ const route = author?.match(/^Agent:\s*(.+)$/)?.[1];
8408
+ return route ? withRemoteProvenance(comment.content, route) : comment.content;
8409
+ };
8410
+ const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [publishableBody(comment)] : []);
8382
8411
  for (const [file, entry] of Object.entries(session.files ?? {})) {
8383
- const fileComments = (entry.file_comments ?? []).flatMap((comment) => include(comment) ? [comment.content] : []);
8384
- if (fileComments.length > 0)
8385
- bodyParts.push(`File: ${file}
8386
-
8387
- ${fileComments.join(`
8388
-
8389
- `)}`);
8412
+ for (const fileComment of entry.file_comments ?? []) {
8413
+ if (!include(fileComment))
8414
+ continue;
8415
+ const comment = { file, body: fileComment.content };
8416
+ const author = commentAuthor(fileComment);
8417
+ if (author)
8418
+ comment.author = author;
8419
+ comments.push(comment);
8420
+ }
8390
8421
  for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
8391
8422
  const line = Number.parseInt(lineKey, 10);
8392
8423
  if (!Number.isFinite(line))
@@ -9106,14 +9137,19 @@ async function promoteLocalReview(review, remote, model) {
9106
9137
  const draft = await local.readDraft();
9107
9138
  const remoteThreads = await remote.listThreads();
9108
9139
  const threadReplies = sessionIsWorkingTree ? [] : draft.comments.flatMap((comment) => {
9109
- const thread = remoteThreads.find((candidate) => candidate.file === comment.file && candidate.line === comment.line && candidate.line !== undefined && !candidate.body.includes("Generated review by Diffpi using"));
9140
+ const thread = remoteThreads.find((candidate) => candidate.file === comment.file && candidate.line === comment.line && !candidate.body.includes("Generated review by Diffpi using"));
9110
9141
  return thread ? [{ comment, thread }] : [];
9111
9142
  });
9143
+ const unmatchedActions = sessionIsWorkingTree ? [] : draft.comments.filter((comment) => parseReviewThreadAction(comment.body).action && !threadReplies.some((reply) => reply.comment === comment));
9144
+ if (unmatchedActions.length > 0) {
9145
+ const locations = unmatchedActions.map(({ file, line }) => `${file}:${line ?? "?"}`).join(", ");
9146
+ throw new Error(`Cannot apply review thread action: no matching remote thread at ${locations}.`);
9147
+ }
9112
9148
  const comments = draft.comments.filter(({ file, line }) => !threadReplies.some((reply) => reply.comment.file === file && reply.comment.line === line)).map((comment) => ({
9113
9149
  ...comment,
9114
- body: withRemoteProvenance(comment.body, comment.author?.replace(/^Agent:\s*/, "") || model)
9150
+ body: withCommentProvenance(comment, model)
9115
9151
  }));
9116
- const body = draft.body.trim() ? withRemoteProvenance(draft.body, model) : "";
9152
+ const body = draft.body.trim();
9117
9153
  const bodyFingerprints = body ? [reviewBodyFingerprint(body)] : [];
9118
9154
  const commentFingerprints = comments.map(reviewCommentFingerprint);
9119
9155
  const remoteDraft = await remote.readDraft();
@@ -9144,7 +9180,16 @@ async function promoteLocationReplies(remote, publication, replies, model) {
9144
9180
  const fingerprint = reviewReplyFingerprint(thread.id, `${action ?? "reply"}\x00${body}`);
9145
9181
  if (publication.state.replies.includes(fingerprint))
9146
9182
  continue;
9147
- const remoteBody = withRemoteProvenance(body, comment.author?.replace(/^Agent:\s*/, "") || model);
9183
+ if (action === "delete") {
9184
+ if (!remote.deleteThread)
9185
+ throw new Error(`The review backend cannot delete thread ${thread.id}.`);
9186
+ await remote.deleteThread(thread.id);
9187
+ publication.state.replies.push(fingerprint);
9188
+ await saveReviewPublicationState(publication.path, publication.state);
9189
+ count += 1;
9190
+ continue;
9191
+ }
9192
+ const remoteBody = withCommentProvenance({ ...comment, body }, model);
9148
9193
  if (body && !thread.replies?.some((reply) => reply === body || reply === remoteBody)) {
9149
9194
  await remote.reply({
9150
9195
  threadId: thread.id,
@@ -9226,6 +9271,10 @@ async function assertRemoteBranchReady(cwd) {
9226
9271
  if (head.stdout.trim() !== pushed.stdout.trim())
9227
9272
  throw new Error("Push the current branch before opening a remote review.");
9228
9273
  }
9274
+ function withCommentProvenance(comment, fallbackModel) {
9275
+ const route = comment.author?.match(/^Agent:\s*(.+)$/)?.[1];
9276
+ return route ? withRemoteProvenance(comment.body, route || fallbackModel) : comment.body;
9277
+ }
9229
9278
  async function reviewTargetId(review) {
9230
9279
  if (!review.pr)
9231
9280
  return "uncommitted";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@difflab/pi",
3
- "version": "0.3.0-rc.202609201751.b819639",
3
+ "version": "0.3.0-rc.202609201843.9cb5906",
4
4
  "description": "Tools and skills for the pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,4 +20,4 @@ Prefer existing package APIs and small public surfaces. Add a dependency when it
20
20
 
21
21
  ## Responses
22
22
 
23
- Respond to every addressed thread. State what changed and include verification evidence. Keep substantive threads open for reviewer confirmation. Resolve only when the reviewer explicitly requests resolution or the change is trivial and complete.
23
+ Respond to every addressed thread. State what changed and include verification evidence. Keep every thread open for the user to resolve. Address workflows must never resolve or delete threads.
@@ -4,8 +4,8 @@
4
4
  2. Run this workflow as the reviewer coordinator. Call `review_context` with the target and backend selection.
5
5
  3. Call `review_comments`. With `--local`, it synchronizes every comment in the selected tuicr session to `.diffpi/review/<session-slug>.md`. Existing replies and statuses remain; source comments removed by the user become resolved ledger entries.
6
6
  4. Classify every thread, group justified code changes into bounded non-overlapping tasks, and delegate those tasks to lightweight worker agents. Keep questions, outcome decisions, integration, and response text in the reviewer coordinator. With `--local`, workers modify the current working tree without committing. Without `--local`, apply a fix only when the comment requests a change.
7
- 5. Respond to every thread with its outcome. Answer questions and leave them open. For substantive requests, state what changed and whether it is resolved, but leave the thread open for reviewer confirmation. For a trivial request, reply `Resolved`; remote threads may close, while the user must delete the local tuicr source comment before the next sync marks it resolved.
7
+ 5. Respond to every thread with its outcome. Answer questions and leave them open. For substantive requests, state what changed and whether it is resolved, but leave every thread open for the user to resolve. Do not resolve remote threads or delete local source comments in this workflow, including for trivial requests.
8
8
  6. When a remote address flow changes code, invoke the upstream `/git commit --no-push` workflow after checks pass and before drafting responses. Use `--atomic` when the fixes form separate logical commits.
9
- 7. Call `review_respond` for each thread. With `--local`, every response is posted to tuicr and recorded in the session ledger. Without `--local`, replies post to the forge immediately.
9
+ 7. Call `review_respond` for each thread with `resolve: false`. With `--local`, every response is posted to tuicr and recorded in the session ledger. Without `--local`, replies post to the forge immediately. The user owns thread resolution.
10
10
  8. With `--local`, call `review_launch_ui` with `local: true` after replies are recorded so the user can inspect the working-tree review in tuicr.
11
- 9. Report fixed, committed, answered, addressed, resolved, and unresolved counts. Do not publish, complete, or merge in this workflow.
11
+ 9. Report fixed, committed, answered, addressed, unresolved, and skipped counts. Resolved must always be zero in this workflow. Do not publish, complete, or merge.
@@ -1,8 +1,8 @@
1
1
  # publish
2
2
 
3
3
  1. Parse an optional PR/MR id or URL, `--local`, and one status: `--comment`, `--approve`, `--request-changes`, or `--close`. The default is comment.
4
- 2. Call `review_context` with the target and backend selection.
5
- 3. For `--local`, call `review_publish` with `local: true`. The tool reads the exact tuicr session. A local line comment on the same file and line as an existing remote thread is treated as a response, not a new comment. A leading `[REOPEN]` or `[RESOLVE]` changes the thread state; the marker is removed from the response body. Other unpublished line comments become new review comments. The tool follows the stored local overlay path, posts unpublished replies to their remote threads, and then publishes the selected status. Question replies remain open. The tool does not post an overall review comment.
6
- 4. For a remote review, call `review_publish` without `local`. The existing pending forge comments already contain provenance, so the tool only publishes them with the selected status and no overall comment.
4
+ 2. Call `review_context` with the target and `local: true` so the exact tuicr PR session can be inspected. If that session exists and has drafts, publish through the local promotion path even when the user omitted `--local`. Use `local: false` only when no matching tuicr session exists or when the target has forge-native pending review work.
5
+ 3. The local promotion path reads the exact tuicr session. A local line comment on the same file and line as an existing remote thread is treated as a response, not a new comment. A leading `[REOPEN]`, `[RESOLVE]`, or `[DELETE]` changes the thread state; the marker is removed from the response body. `[DELETE]` deletes the matched remote thread when the forge supports it. Other unpublished line comments become new review comments. The tool follows the stored local overlay path, posts unpublished replies to their remote threads, and then publishes the selected status. Question replies remain open. The tool does not post an overall review comment.
6
+ 4. For a forge-native review without a matching tuicr session, call `review_publish` without `local`. Existing pending forge comments already contain provenance, so the tool only publishes them with the selected status and no overall comment.
7
7
  5. Comment, approve, and request-changes mark a draft PR/MR ready before publication. Close publishes pending work as comment status and then closes the PR/MR. GitLab rejects request-changes because it has no equivalent review state.
8
8
  6. Report promoted comment and reply counts, the public status, and the final PR/MR state. Publishing never merges. If a response cannot be matched by file and line, report it as a new comment rather than silently attaching it to the wrong thread.