@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.
- package/agents/diffpi-reviewer.md +2 -2
- package/dist/extensions/index.js +74 -25
- package/dist/extensions/tuicrx.d.ts.map +1 -1
- package/dist/index.js +51 -20
- package/dist/review/github-review-backend.d.ts.map +1 -1
- package/dist/review/gitlab-review-backend.d.ts.map +1 -1
- package/dist/review/types.d.ts +5 -2
- package/dist/review/types.d.ts.map +1 -1
- package/dist/tools/index.js +74 -25
- package/package.json +1 -1
- package/skills/review/references/review-standards.md +1 -1
- package/skills/review/references/workflows/address.md +3 -3
- package/skills/review/references/workflows/publish.md +3 -3
|
@@ -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`, `
|
|
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.
|
package/dist/extensions/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
22012
|
-
|
|
22013
|
-
"-f",
|
|
22014
|
-
|
|
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
|
|
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
|
|
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
|
|
22391
|
-
|
|
22392
|
-
|
|
22393
|
-
|
|
22394
|
-
|
|
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 &&
|
|
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:
|
|
23157
|
+
body: withCommentProvenance(comment, model)
|
|
23122
23158
|
}));
|
|
23123
|
-
const body = draft.body.trim()
|
|
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
|
-
|
|
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":"AAIA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,
|
|
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
|
-
|
|
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
|
-
|
|
1485
|
-
|
|
1486
|
-
"-f",
|
|
1487
|
-
|
|
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
|
|
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
|
|
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
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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,
|
|
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,
|
|
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"}
|
package/dist/review/types.d.ts
CHANGED
|
@@ -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
|
|
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":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;AAC1C,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,iBAAiB,GAAG,SAAS,CAAC;AAEpE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/review/types.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
8005
|
-
|
|
8006
|
-
"-f",
|
|
8007
|
-
|
|
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
|
|
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
|
|
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
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
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 &&
|
|
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:
|
|
9150
|
+
body: withCommentProvenance(comment, model)
|
|
9115
9151
|
}));
|
|
9116
|
-
const body = draft.body.trim()
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
5
|
-
3.
|
|
6
|
-
4. For a
|
|
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.
|