opencode-magi 0.3.0 → 0.5.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.
- package/README.md +53 -45
- package/dist/config/validate.js +51 -0
- package/dist/github/commands.js +13 -12
- package/dist/index.js +41 -2
- package/dist/orchestrator/findings.js +3 -4
- package/dist/orchestrator/merge.js +68 -29
- package/dist/orchestrator/report.js +1 -8
- package/dist/orchestrator/review-context.js +37 -4
- package/dist/orchestrator/review.js +65 -10
- package/dist/orchestrator/run-manager.js +54 -8
- package/dist/prompts/compose.js +1 -0
- package/dist/prompts/contracts.js +28 -29
- package/dist/prompts/output.js +40 -42
- package/dist/prompts/templates/merge/edit.md +11 -5
- package/dist/prompts/templates/review/close-reconsideration.md +1 -0
- package/dist/prompts/templates/review/rereview.md +3 -0
- package/dist/prompts/templates/review/review.md +4 -0
- package/package.json +1 -1
- package/schema.json +37 -5
|
@@ -37,6 +37,23 @@ function quoteEvidence(value) {
|
|
|
37
37
|
const compact = value.replaceAll(/\s+/g, " ").trim();
|
|
38
38
|
return compact.length > 120 ? `${compact.slice(0, 117)}...` : compact;
|
|
39
39
|
}
|
|
40
|
+
function errorText(error) {
|
|
41
|
+
if (!error || typeof error !== "object")
|
|
42
|
+
return String(error);
|
|
43
|
+
const value = error;
|
|
44
|
+
return [value.message, value.stderr, value.stdout]
|
|
45
|
+
.filter((item) => typeof item === "string")
|
|
46
|
+
.join("\n");
|
|
47
|
+
}
|
|
48
|
+
function isIssueLookupFailure(error) {
|
|
49
|
+
const text = errorText(error);
|
|
50
|
+
return (/could not resolve to an issue/i.test(text) ||
|
|
51
|
+
/could not fetch issue #\d+/i.test(text) ||
|
|
52
|
+
/not an issue/i.test(text));
|
|
53
|
+
}
|
|
54
|
+
function isIssueUrl(url) {
|
|
55
|
+
return /\/issues\/\d+(?:$|[/?#])/i.test(url);
|
|
56
|
+
}
|
|
40
57
|
function issueReferencePattern(repository) {
|
|
41
58
|
const host = escapeRegExp(repository.github.host || "github.com");
|
|
42
59
|
const owner = escapeRegExp(repository.github.owner);
|
|
@@ -120,6 +137,9 @@ export function collectIssueRelationships(input) {
|
|
|
120
137
|
async function contextIssue(input) {
|
|
121
138
|
const issue = input.issue ??
|
|
122
139
|
(await fetchIssue(input.exec, input.repository, input.relationship.number));
|
|
140
|
+
if (!isIssueUrl(issue.url)) {
|
|
141
|
+
throw new Error(`Reference #${issue.number} resolved to ${issue.url}, not an Issue`);
|
|
142
|
+
}
|
|
123
143
|
const commentPage = await fetchIssueCommentPage(input.exec, input.repository, issue.number, input.limit);
|
|
124
144
|
return {
|
|
125
145
|
author: issue.author,
|
|
@@ -138,6 +158,19 @@ async function contextIssue(input) {
|
|
|
138
158
|
url: issue.url,
|
|
139
159
|
};
|
|
140
160
|
}
|
|
161
|
+
async function contextIssueIfIssue(input) {
|
|
162
|
+
try {
|
|
163
|
+
return await contextIssue(input);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (isIssueLookupFailure(error))
|
|
167
|
+
return undefined;
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function presentIssue(issue) {
|
|
172
|
+
return Boolean(issue);
|
|
173
|
+
}
|
|
141
174
|
function orderReviewThreads(threads) {
|
|
142
175
|
return [...threads]
|
|
143
176
|
.sort((a, b) => {
|
|
@@ -183,13 +216,13 @@ export async function buildReviewContextSnapshot(input) {
|
|
|
183
216
|
const closingRelationships = relationships.filter((relationship) => relationship.relationship === "closing");
|
|
184
217
|
const referencedRelationships = relationships.filter((relationship) => relationship.relationship === "referenced");
|
|
185
218
|
return {
|
|
186
|
-
closingIssues: await Promise.all(closingRelationships.map((relationship) =>
|
|
219
|
+
closingIssues: (await Promise.all(closingRelationships.map((relationship) => contextIssueIfIssue({
|
|
187
220
|
exec: input.exec,
|
|
188
221
|
issue: closingIssueMap.get(relationship.number),
|
|
189
222
|
limit: LIMITS.closingIssueComments,
|
|
190
223
|
relationship,
|
|
191
224
|
repository: input.repository,
|
|
192
|
-
}))),
|
|
225
|
+
})))).filter(presentIssue),
|
|
193
226
|
pullRequest: {
|
|
194
227
|
author: input.pr.author?.login ?? safetyMeta.author,
|
|
195
228
|
baseRef: input.pr.baseRefName,
|
|
@@ -207,12 +240,12 @@ export async function buildReviewContextSnapshot(input) {
|
|
|
207
240
|
title: input.pr.title,
|
|
208
241
|
url: input.pr.url,
|
|
209
242
|
},
|
|
210
|
-
referencedIssues: await Promise.all(referencedRelationships.map((relationship) =>
|
|
243
|
+
referencedIssues: (await Promise.all(referencedRelationships.map((relationship) => contextIssueIfIssue({
|
|
211
244
|
exec: input.exec,
|
|
212
245
|
limit: LIMITS.referencedIssueComments,
|
|
213
246
|
relationship,
|
|
214
247
|
repository: input.repository,
|
|
215
|
-
}))),
|
|
248
|
+
})))).filter(presentIssue),
|
|
216
249
|
reviewDiscussion: {
|
|
217
250
|
prComments: boundedComments(prComments, LIMITS.prComments),
|
|
218
251
|
prCommentsOmitted,
|
|
@@ -39,7 +39,7 @@ async function postReviewOutput(input, reviewerKey, output) {
|
|
|
39
39
|
return postApproval(input.exec, input.repository, input.pr, reviewer.account);
|
|
40
40
|
if (output.verdict === "CLOSE")
|
|
41
41
|
return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
|
|
42
|
-
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.findings
|
|
42
|
+
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.findings);
|
|
43
43
|
}
|
|
44
44
|
function dryRunReviewPost(key, output) {
|
|
45
45
|
if (output.verdict === "MERGE")
|
|
@@ -135,16 +135,64 @@ function parseRereviewOutputWithInlineTargets(text, targets) {
|
|
|
135
135
|
validateInlineCommentTargets(output.newFindings, targets, "newFindings");
|
|
136
136
|
return output;
|
|
137
137
|
}
|
|
138
|
-
function
|
|
138
|
+
function parsePostedFindingLocation(location) {
|
|
139
|
+
const range = /^(.*):(\d+)-(\d+)$/.exec(location);
|
|
140
|
+
if (range) {
|
|
141
|
+
return {
|
|
142
|
+
line: Number(range[3]),
|
|
143
|
+
path: range[1] ?? location,
|
|
144
|
+
startLine: Number(range[2]),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const line = /^(.*):(\d+)$/.exec(location);
|
|
148
|
+
if (line)
|
|
149
|
+
return { line: Number(line[2]), path: line[1] ?? location };
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
function reviewFindingsFromBody(body) {
|
|
153
|
+
const findings = [];
|
|
154
|
+
const lines = (body ?? "").split(/\r?\n/);
|
|
155
|
+
let section;
|
|
156
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
157
|
+
const line = lines[index];
|
|
158
|
+
if (line === "Inline findings:" || line === "File-level findings:") {
|
|
159
|
+
section = "finding";
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (line === "Requirement findings:") {
|
|
163
|
+
section = undefined;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (section === "finding") {
|
|
167
|
+
const match = /^- (.*): (.+)$/.exec(line ?? "");
|
|
168
|
+
const fix = /^\s+Fix: (.+)$/.exec(lines[index + 1] ?? "");
|
|
169
|
+
if (!match || !fix)
|
|
170
|
+
continue;
|
|
171
|
+
const location = parsePostedFindingLocation(match[1] ?? "");
|
|
172
|
+
if (!location)
|
|
173
|
+
continue;
|
|
174
|
+
findings.push({
|
|
175
|
+
...location,
|
|
176
|
+
fix: fix[1] ?? "Please address this before merging.",
|
|
177
|
+
issue: match[2] ?? "Review finding.",
|
|
178
|
+
});
|
|
179
|
+
index += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { findings };
|
|
184
|
+
}
|
|
185
|
+
export function reviewOutputFromState(review) {
|
|
139
186
|
const verdict = reviewStateToVerdict(review.state);
|
|
187
|
+
if (verdict === "CHANGES_REQUESTED")
|
|
188
|
+
return { ...reviewFindingsFromBody(review.body), verdict };
|
|
140
189
|
return verdict === "CLOSE"
|
|
141
190
|
? {
|
|
142
191
|
findings: [],
|
|
143
192
|
reason: review.body || "Close requested.",
|
|
144
|
-
requirementFindings: [],
|
|
145
193
|
verdict,
|
|
146
194
|
}
|
|
147
|
-
: { findings: [],
|
|
195
|
+
: { findings: [], verdict };
|
|
148
196
|
}
|
|
149
197
|
export function hasPendingThreadReply(threads, reviewerAccount) {
|
|
150
198
|
return threads.some((thread) => {
|
|
@@ -168,15 +216,15 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
168
216
|
return postApproval(input.exec, input.repository, input.pr, reviewer.account);
|
|
169
217
|
if (output.verdict === "CLOSE")
|
|
170
218
|
return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
|
|
171
|
-
if (!output.newFindings.length
|
|
219
|
+
if (!output.newFindings.length)
|
|
172
220
|
return "";
|
|
173
221
|
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
|
|
174
222
|
fix: "Please address this before merging.",
|
|
175
223
|
issue: finding.body,
|
|
176
|
-
line: finding.line,
|
|
177
224
|
path: finding.path,
|
|
225
|
+
line: finding.line,
|
|
178
226
|
startLine: finding.startLine,
|
|
179
|
-
}))
|
|
227
|
+
})));
|
|
180
228
|
}
|
|
181
229
|
function isReviewOutput(output) {
|
|
182
230
|
return "findings" in output;
|
|
@@ -761,7 +809,14 @@ export async function runReview(input) {
|
|
|
761
809
|
sessionIds,
|
|
762
810
|
worktreePath,
|
|
763
811
|
});
|
|
764
|
-
const
|
|
812
|
+
const activeOutputs = validation.outputs;
|
|
813
|
+
const skippedOutputs = Object.fromEntries(input.repository.agents.reviewers.flatMap((reviewer) => {
|
|
814
|
+
const assignment = mode.assignments.get(reviewer.account);
|
|
815
|
+
return assignment?.type === "skip"
|
|
816
|
+
? [[reviewer.key, reviewOutputFromState(assignment.review)]]
|
|
817
|
+
: [];
|
|
818
|
+
}));
|
|
819
|
+
const outputs = { ...skippedOutputs, ...activeOutputs };
|
|
765
820
|
const remainingSkippedVerdicts = input.repository.agents.reviewers.flatMap((reviewer) => {
|
|
766
821
|
const assignment = mode.assignments.get(reviewer.account);
|
|
767
822
|
if (assignment?.type !== "skip" || closeTargets.includes(reviewer.key))
|
|
@@ -773,7 +828,7 @@ export async function runReview(input) {
|
|
|
773
828
|
},
|
|
774
829
|
];
|
|
775
830
|
});
|
|
776
|
-
const activeVerdicts = Object.entries(
|
|
831
|
+
const activeVerdicts = Object.entries(activeOutputs).map(([reviewer, output]) => ({
|
|
777
832
|
reviewer,
|
|
778
833
|
verdict: output.verdict,
|
|
779
834
|
}));
|
|
@@ -786,7 +841,7 @@ export async function runReview(input) {
|
|
|
786
841
|
? [[reviewer.key, "skipped: already reviewed current head"]]
|
|
787
842
|
: [];
|
|
788
843
|
})),
|
|
789
|
-
...Object.fromEntries(await Promise.all(Object.entries(
|
|
844
|
+
...Object.fromEntries(await Promise.all(Object.entries(activeOutputs).map(async ([key, output]) => [
|
|
790
845
|
key,
|
|
791
846
|
input.dryRun
|
|
792
847
|
? dryRunReviewPost(key, output)
|
|
@@ -15,6 +15,7 @@ const DEFAULT_CLEAR_OPTIONS = {
|
|
|
15
15
|
session: true,
|
|
16
16
|
worktree: true,
|
|
17
17
|
};
|
|
18
|
+
const SYNC_RUN_TIMEOUT_MS = 600_000;
|
|
18
19
|
function createRunId() {
|
|
19
20
|
return `run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
20
21
|
}
|
|
@@ -453,11 +454,14 @@ export class MagiRunManager {
|
|
|
453
454
|
await this.notify(state, `Started Magi review for ${prMarkdownLink(state)}.`);
|
|
454
455
|
const controller = new AbortController();
|
|
455
456
|
this.controllers.set(runId, controller);
|
|
456
|
-
|
|
457
|
+
const execute = () => this.executeReview({
|
|
457
458
|
...input,
|
|
458
459
|
runId,
|
|
459
460
|
signal: controller.signal,
|
|
460
|
-
})
|
|
461
|
+
});
|
|
462
|
+
if (input.sync)
|
|
463
|
+
return this.executeSync(state, controller, execute);
|
|
464
|
+
void execute().catch(async (error) => {
|
|
461
465
|
await this.failRun(runId, error);
|
|
462
466
|
});
|
|
463
467
|
return state;
|
|
@@ -511,11 +515,14 @@ export class MagiRunManager {
|
|
|
511
515
|
await this.notify(state, `Started Magi merge for ${prMarkdownLink(state)}.`);
|
|
512
516
|
const controller = new AbortController();
|
|
513
517
|
this.controllers.set(runId, controller);
|
|
514
|
-
|
|
518
|
+
const execute = () => this.executeMerge({
|
|
515
519
|
...input,
|
|
516
520
|
runId,
|
|
517
521
|
signal: controller.signal,
|
|
518
|
-
})
|
|
522
|
+
});
|
|
523
|
+
if (input.sync)
|
|
524
|
+
return this.executeSync(state, controller, execute);
|
|
525
|
+
void execute().catch(async (error) => {
|
|
519
526
|
await this.failRun(runId, error);
|
|
520
527
|
});
|
|
521
528
|
return state;
|
|
@@ -568,11 +575,14 @@ export class MagiRunManager {
|
|
|
568
575
|
await this.notify(state, `Started Magi triage for ${issueMarkdownLink(state)}.`);
|
|
569
576
|
const controller = new AbortController();
|
|
570
577
|
this.controllers.set(runId, controller);
|
|
571
|
-
|
|
578
|
+
const execute = () => this.executeTriage({
|
|
572
579
|
...input,
|
|
573
580
|
runId,
|
|
574
581
|
signal: controller.signal,
|
|
575
|
-
})
|
|
582
|
+
});
|
|
583
|
+
if (input.sync)
|
|
584
|
+
return this.executeSync(state, controller, execute);
|
|
585
|
+
void execute().catch(async (error) => {
|
|
576
586
|
await this.failRun(runId, error);
|
|
577
587
|
});
|
|
578
588
|
return state;
|
|
@@ -1210,6 +1220,36 @@ export class MagiRunManager {
|
|
|
1210
1220
|
hasBlockedAgents(state) {
|
|
1211
1221
|
return this.agentEntries(state).some(([, agent]) => agent.status === "blocked");
|
|
1212
1222
|
}
|
|
1223
|
+
async executeSync(state, controller, execute) {
|
|
1224
|
+
let timeout;
|
|
1225
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1226
|
+
timeout = setTimeout(() => resolve("timeout"), SYNC_RUN_TIMEOUT_MS);
|
|
1227
|
+
});
|
|
1228
|
+
try {
|
|
1229
|
+
const result = await Promise.race([
|
|
1230
|
+
execute().then(() => "completed"),
|
|
1231
|
+
timeoutPromise,
|
|
1232
|
+
]);
|
|
1233
|
+
if (result === "timeout") {
|
|
1234
|
+
controller.abort();
|
|
1235
|
+
await this.failRun(state.runId, new Error("Magi sync run timed out after 600 seconds."));
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
catch (error) {
|
|
1239
|
+
controller.abort();
|
|
1240
|
+
await this.failRun(state.runId, error);
|
|
1241
|
+
}
|
|
1242
|
+
finally {
|
|
1243
|
+
if (timeout)
|
|
1244
|
+
clearTimeout(timeout);
|
|
1245
|
+
}
|
|
1246
|
+
return (await this.readStateByRunId(state.runId)) ?? state;
|
|
1247
|
+
}
|
|
1248
|
+
assertSuccessfulSyncFollowUp(state) {
|
|
1249
|
+
if (state.status === "completed")
|
|
1250
|
+
return;
|
|
1251
|
+
throw new Error(`Synchronous follow-up ${state.command} run ${state.runId} finished with status ${state.status}.`);
|
|
1252
|
+
}
|
|
1213
1253
|
async executeReview(input) {
|
|
1214
1254
|
const result = await runReview({
|
|
1215
1255
|
approvalPolicy: input.repository.merge.approvalPolicy,
|
|
@@ -1347,24 +1387,30 @@ export class MagiRunManager {
|
|
|
1347
1387
|
: undefined;
|
|
1348
1388
|
const triageAutomation = input.repository.triage?.automation;
|
|
1349
1389
|
if (followUpPr != null && triageAutomation?.merge) {
|
|
1350
|
-
await this.startMerge({
|
|
1390
|
+
const followUp = await this.startMerge({
|
|
1351
1391
|
config: input.config,
|
|
1352
1392
|
dryRun: input.dryRun,
|
|
1353
1393
|
parentSessionId: input.parentSessionId,
|
|
1354
1394
|
pr: followUpPr,
|
|
1355
1395
|
repository: input.repository,
|
|
1356
1396
|
signal: input.signal,
|
|
1397
|
+
sync: input.sync,
|
|
1357
1398
|
});
|
|
1399
|
+
if (input.sync)
|
|
1400
|
+
this.assertSuccessfulSyncFollowUp(followUp);
|
|
1358
1401
|
}
|
|
1359
1402
|
else if (followUpPr != null && triageAutomation?.review) {
|
|
1360
|
-
await this.startReview({
|
|
1403
|
+
const followUp = await this.startReview({
|
|
1361
1404
|
config: input.config,
|
|
1362
1405
|
dryRun: input.dryRun,
|
|
1363
1406
|
parentSessionId: input.parentSessionId,
|
|
1364
1407
|
pr: followUpPr,
|
|
1365
1408
|
repository: input.repository,
|
|
1366
1409
|
signal: input.signal,
|
|
1410
|
+
sync: input.sync,
|
|
1367
1411
|
});
|
|
1412
|
+
if (input.sync)
|
|
1413
|
+
this.assertSuccessfulSyncFollowUp(followUp);
|
|
1368
1414
|
}
|
|
1369
1415
|
this.active.delete(input.runId);
|
|
1370
1416
|
this.controllers.delete(input.runId);
|
package/dist/prompts/compose.js
CHANGED
|
@@ -15,25 +15,20 @@ The object must match this shape:
|
|
|
15
15
|
"perspective": "Optional review perspective."
|
|
16
16
|
}
|
|
17
17
|
],
|
|
18
|
-
"requirementFindings": [
|
|
19
|
-
{
|
|
20
|
-
"issueNumber": 47,
|
|
21
|
-
"requirement": "Required closing-issue behavior that is missing.",
|
|
22
|
-
"evidence": "Why the PR does not satisfy the requirement.",
|
|
23
|
-
"fix": "How to satisfy the requirement."
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
18
|
"reason": "Required only for CLOSE."
|
|
27
19
|
}
|
|
28
20
|
|
|
29
21
|
Rules:
|
|
30
|
-
- MERGE requires empty findings
|
|
31
|
-
- CHANGES_REQUESTED requires at least one finding
|
|
32
|
-
- CLOSE requires a reason and empty findings
|
|
22
|
+
- MERGE requires an empty findings array.
|
|
23
|
+
- CHANGES_REQUESTED requires at least one finding.
|
|
24
|
+
- CLOSE requires a reason and an empty findings array.
|
|
33
25
|
- path must be repository-relative.
|
|
34
|
-
- line and
|
|
26
|
+
- line is required and must target a valid right-side line inside the PR diff hunk.
|
|
27
|
+
- startLine is optional and must also target a valid right-side line inside the same PR diff hunk range.
|
|
35
28
|
- Omit startLine for single-line findings.
|
|
36
|
-
-
|
|
29
|
+
- Do not omit line. Do not create file-level or body-only findings.
|
|
30
|
+
- Missing closing-issue requirements must be normal findings anchored to the nearest responsible changed line.
|
|
31
|
+
- If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior. This includes but is not limited to missing validation, missing wiring, missing requirements, missing tests, missing documentation, affected configuration, or relevant call sites.
|
|
37
32
|
</output_contract>`.trim();
|
|
38
33
|
export const rereviewOutputContract = `
|
|
39
34
|
<output_contract>
|
|
@@ -45,17 +40,19 @@ The object must match this shape:
|
|
|
45
40
|
"resolve": [{ "commentId": 123, "threadId": "..." }],
|
|
46
41
|
"followUps": [{ "commentId": 123, "body": "..." }],
|
|
47
42
|
"newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }],
|
|
48
|
-
"requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }],
|
|
49
43
|
"reason": "Required only for CLOSE."
|
|
50
44
|
}
|
|
51
45
|
|
|
52
46
|
Rules:
|
|
53
|
-
- MERGE requires empty followUps
|
|
54
|
-
- CHANGES_REQUESTED requires at least one followUp
|
|
55
|
-
- CLOSE requires a reason and empty followUps
|
|
56
|
-
- line and
|
|
47
|
+
- MERGE requires empty followUps and newFindings arrays.
|
|
48
|
+
- CHANGES_REQUESTED requires at least one followUp or newFinding.
|
|
49
|
+
- CLOSE requires a reason and empty followUps and newFindings arrays.
|
|
50
|
+
- line is required and must target a valid right-side line inside the latest PR diff hunk.
|
|
51
|
+
- startLine is optional and must also target a valid right-side line inside the same latest PR diff hunk range.
|
|
57
52
|
- Omit startLine for single-line findings.
|
|
58
|
-
-
|
|
53
|
+
- Do not omit line. Do not create file-level or body-only findings.
|
|
54
|
+
- Missing closing-issue requirements must be normal newFindings anchored to the nearest responsible changed line.
|
|
55
|
+
- If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior. This includes but is not limited to missing validation, missing wiring, missing requirements, missing tests, missing documentation, affected configuration, or relevant call sites.
|
|
59
56
|
</output_contract>`.trim();
|
|
60
57
|
export const findingValidationOutputContract = `
|
|
61
58
|
<output_contract>
|
|
@@ -94,15 +91,16 @@ The object must match this shape:
|
|
|
94
91
|
"issue": "What is wrong.",
|
|
95
92
|
"fix": "How to fix it."
|
|
96
93
|
}
|
|
97
|
-
]
|
|
98
|
-
"requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }]
|
|
94
|
+
]
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
Rules:
|
|
102
|
-
- MERGE requires empty findings
|
|
103
|
-
- CHANGES_REQUESTED requires at least one finding
|
|
98
|
+
- MERGE requires an empty findings array.
|
|
99
|
+
- CHANGES_REQUESTED requires at least one finding.
|
|
104
100
|
- CLOSE is not allowed in this reconsideration step.
|
|
105
|
-
-
|
|
101
|
+
- line is required and must target a valid right-side line inside the PR diff hunk.
|
|
102
|
+
- startLine is optional and must also target a valid right-side line inside the same PR diff hunk range.
|
|
103
|
+
- Do not omit line. Do not create file-level or body-only findings.
|
|
106
104
|
</output_contract>`.trim();
|
|
107
105
|
export const rereviewCloseReconsiderationOutputContract = `
|
|
108
106
|
<output_contract>
|
|
@@ -113,15 +111,16 @@ The object must match this shape:
|
|
|
113
111
|
"verdict": "MERGE" | "CHANGES_REQUESTED",
|
|
114
112
|
"resolve": [{ "commentId": 123, "threadId": "..." }],
|
|
115
113
|
"followUps": [{ "commentId": 123, "body": "..." }],
|
|
116
|
-
"newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }]
|
|
117
|
-
"requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }]
|
|
114
|
+
"newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }]
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
Rules:
|
|
121
|
-
- MERGE requires empty followUps
|
|
122
|
-
- CHANGES_REQUESTED requires at least one followUp
|
|
118
|
+
- MERGE requires empty followUps and newFindings arrays.
|
|
119
|
+
- CHANGES_REQUESTED requires at least one followUp or newFinding.
|
|
123
120
|
- CLOSE is not allowed in this reconsideration step.
|
|
124
|
-
-
|
|
121
|
+
- line is required and must target a valid right-side line inside the latest PR diff hunk.
|
|
122
|
+
- startLine is optional and must also target a valid right-side line inside the same latest PR diff hunk range.
|
|
123
|
+
- Do not omit line. Do not create file-level or body-only findings.
|
|
125
124
|
</output_contract>`.trim();
|
|
126
125
|
export const editOutputContract = `
|
|
127
126
|
<output_contract>
|
package/dist/prompts/output.js
CHANGED
|
@@ -71,16 +71,15 @@ function requireNumber(value, path) {
|
|
|
71
71
|
throw new Error(`${path} must be an integer`);
|
|
72
72
|
return value;
|
|
73
73
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
});
|
|
74
|
+
function requireLine(value, path) {
|
|
75
|
+
if (value == null)
|
|
76
|
+
throw new Error(`${path} is required`);
|
|
77
|
+
return requireNumber(value, path);
|
|
78
|
+
}
|
|
79
|
+
function optionalStartLine(input) {
|
|
80
|
+
if (input.value == null)
|
|
81
|
+
return undefined;
|
|
82
|
+
return requireNumber(input.value, input.path);
|
|
84
83
|
}
|
|
85
84
|
function requireOneOf(value, path, values) {
|
|
86
85
|
const text = requireString(value, path);
|
|
@@ -169,34 +168,34 @@ export function parseReviewOutput(text) {
|
|
|
169
168
|
const data = extractJson(text);
|
|
170
169
|
if (!data || typeof data !== "object")
|
|
171
170
|
throw new Error("review output must be an object");
|
|
171
|
+
if (data.requirementFindings != null)
|
|
172
|
+
throw new Error("requirementFindings is not accepted");
|
|
172
173
|
if (!isVerdict(data.verdict))
|
|
173
174
|
throw new Error("verdict must be MERGE, CHANGES_REQUESTED, or CLOSE");
|
|
174
175
|
const findings = requireArray(data.findings, "findings").map((finding, index) => {
|
|
175
176
|
const item = finding;
|
|
177
|
+
const line = requireLine(item.line, `findings[${index}].line`);
|
|
176
178
|
return {
|
|
177
179
|
fix: requireString(item.fix, `findings[${index}].fix`),
|
|
178
180
|
issue: requireString(item.issue, `findings[${index}].issue`),
|
|
179
|
-
line
|
|
181
|
+
line,
|
|
180
182
|
path: requireString(item.path, `findings[${index}].path`),
|
|
181
183
|
perspective: item.perspective == null
|
|
182
184
|
? undefined
|
|
183
185
|
: requireString(item.perspective, `findings[${index}].perspective`),
|
|
184
|
-
startLine:
|
|
185
|
-
|
|
186
|
-
:
|
|
186
|
+
startLine: optionalStartLine({
|
|
187
|
+
line,
|
|
188
|
+
path: `findings[${index}].startLine`,
|
|
189
|
+
value: item.startLine,
|
|
190
|
+
}),
|
|
187
191
|
};
|
|
188
192
|
});
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
throw new Error("
|
|
193
|
-
if (data.verdict === "
|
|
194
|
-
|
|
195
|
-
!requirementFindings.length)
|
|
196
|
-
throw new Error("CHANGES_REQUESTED requires findings or requirementFindings");
|
|
197
|
-
if (data.verdict === "CLOSE" &&
|
|
198
|
-
(findings.length || requirementFindings.length))
|
|
199
|
-
throw new Error("CLOSE requires no findings or requirementFindings");
|
|
193
|
+
if (data.verdict === "MERGE" && findings.length)
|
|
194
|
+
throw new Error("MERGE requires no findings");
|
|
195
|
+
if (data.verdict === "CHANGES_REQUESTED" && !findings.length)
|
|
196
|
+
throw new Error("CHANGES_REQUESTED requires findings");
|
|
197
|
+
if (data.verdict === "CLOSE" && findings.length)
|
|
198
|
+
throw new Error("CLOSE requires no findings");
|
|
200
199
|
const reason = typeof data.reason === "string" && data.reason.trim()
|
|
201
200
|
? data.reason
|
|
202
201
|
: undefined;
|
|
@@ -205,7 +204,6 @@ export function parseReviewOutput(text) {
|
|
|
205
204
|
return {
|
|
206
205
|
findings,
|
|
207
206
|
reason,
|
|
208
|
-
requirementFindings,
|
|
209
207
|
verdict: data.verdict,
|
|
210
208
|
};
|
|
211
209
|
}
|
|
@@ -213,6 +211,8 @@ export function parseRereviewOutput(text) {
|
|
|
213
211
|
const data = extractJson(text);
|
|
214
212
|
if (!isVerdict(data.verdict))
|
|
215
213
|
throw new Error("rereview verdict must be MERGE, CHANGES_REQUESTED, or CLOSE");
|
|
214
|
+
if (data.requirementFindings != null)
|
|
215
|
+
throw new Error("requirementFindings is not accepted");
|
|
216
216
|
const resolve = requireArray(data.resolve, "resolve").map((item, index) => {
|
|
217
217
|
const value = item;
|
|
218
218
|
return {
|
|
@@ -229,38 +229,36 @@ export function parseRereviewOutput(text) {
|
|
|
229
229
|
});
|
|
230
230
|
const newFindings = requireArray(data.newFindings, "newFindings").map((item, index) => {
|
|
231
231
|
const value = item;
|
|
232
|
+
const line = requireLine(value.line, `newFindings[${index}].line`);
|
|
232
233
|
return {
|
|
233
234
|
body: requireString(value.body, `newFindings[${index}].body`),
|
|
234
|
-
line
|
|
235
|
+
line,
|
|
235
236
|
path: requireString(value.path, `newFindings[${index}].path`),
|
|
236
|
-
startLine:
|
|
237
|
-
|
|
238
|
-
:
|
|
237
|
+
startLine: optionalStartLine({
|
|
238
|
+
line,
|
|
239
|
+
path: `newFindings[${index}].startLine`,
|
|
240
|
+
value: value.startLine,
|
|
241
|
+
}),
|
|
239
242
|
};
|
|
240
243
|
});
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
(followUps.length || newFindings.length || requirementFindings.length)) {
|
|
244
|
-
throw new Error("MERGE requires no followUps, newFindings, or requirementFindings");
|
|
244
|
+
if (data.verdict === "MERGE" && (followUps.length || newFindings.length)) {
|
|
245
|
+
throw new Error("MERGE requires no followUps or newFindings");
|
|
245
246
|
}
|
|
246
|
-
if (data.verdict === "CLOSE" &&
|
|
247
|
-
(followUps
|
|
248
|
-
throw new Error("CLOSE requires no followUps, newFindings, or requirementFindings");
|
|
247
|
+
if (data.verdict === "CLOSE" && (followUps.length || newFindings.length)) {
|
|
248
|
+
throw new Error("CLOSE requires no followUps or newFindings");
|
|
249
249
|
}
|
|
250
250
|
if (data.verdict === "CLOSE" && !data.reason) {
|
|
251
251
|
throw new Error("CLOSE requires reason");
|
|
252
252
|
}
|
|
253
253
|
if (data.verdict === "CHANGES_REQUESTED" &&
|
|
254
254
|
!followUps.length &&
|
|
255
|
-
!newFindings.length
|
|
256
|
-
|
|
257
|
-
throw new Error("CHANGES_REQUESTED requires followUps, newFindings, or requirementFindings");
|
|
255
|
+
!newFindings.length) {
|
|
256
|
+
throw new Error("CHANGES_REQUESTED requires followUps or newFindings");
|
|
258
257
|
}
|
|
259
258
|
return {
|
|
260
259
|
followUps,
|
|
261
260
|
newFindings,
|
|
262
261
|
reason: data.reason == null ? undefined : requireString(data.reason, "reason"),
|
|
263
|
-
requirementFindings,
|
|
264
262
|
resolve,
|
|
265
263
|
verdict: data.verdict,
|
|
266
264
|
};
|
|
@@ -349,7 +347,7 @@ function parseEditOutputWithOptions(text, options) {
|
|
|
349
347
|
commentId: requireNumber(value.commentId, `responses[${index}].commentId`),
|
|
350
348
|
};
|
|
351
349
|
});
|
|
352
|
-
if (options.requireResponses && !responses.length)
|
|
350
|
+
if (options.requireResponses && data.mode === "REPLIED" && !responses.length)
|
|
353
351
|
throw new Error("responses must not be empty");
|
|
354
352
|
if (data.mode === "EDITED") {
|
|
355
353
|
if (!filesTouched.length)
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
Fix pull request #{pr} for {owner}/{repo}.
|
|
2
2
|
The PR worktree is {worktreePath}.
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
Act as the PR author and address every blocking review finding listed below.
|
|
5
|
+
Review findings are the complete set of requested changes. Each finding targets a PR diff line and should have a corresponding GitHub review thread unless it comes from legacy read-side state.
|
|
6
|
+
{reviewFindings}
|
|
7
|
+
|
|
8
|
+
Unresolved GitHub review threads are conversations that may need replies or resolution.
|
|
4
9
|
{unresolvedThreads}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
If
|
|
8
|
-
If
|
|
10
|
+
|
|
11
|
+
For each review finding and thread, decide whether you agree with the reviewer.
|
|
12
|
+
If you understand and agree with the requested change, edit the code, stage changes, commit, and reply with action FIXED for each related thread.
|
|
13
|
+
If a requested change in a thread is incorrect or unnecessary and you have a clear reason, do not edit for that thread; reply with action DISAGREE and explain why.
|
|
14
|
+
If you cannot determine whether a threaded request is correct or what change is expected, do not blindly edit; reply with action ASK and ask a concrete question.
|
|
9
15
|
Do not make changes just because a reviewer requested them. Do not push.
|
|
@@ -2,4 +2,5 @@ You requested CLOSE for pull request #{pr} in {owner}/{repo}, but the other revi
|
|
|
2
2
|
Reconsider your decision using the existing session context and choose MERGE or CHANGES_REQUESTED instead.
|
|
3
3
|
Original close reason:
|
|
4
4
|
{closeReason}
|
|
5
|
+
Every finding must target a valid right-side line in the PR diff. If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior.
|
|
5
6
|
Do not edit files or perform write operations.
|
|
@@ -9,6 +9,9 @@ If there is no new commit, still reconsider the thread when a user replied after
|
|
|
9
9
|
If you agree with the user's explanation or the code is fixed, resolve the thread.
|
|
10
10
|
If you do not agree, reply in the same thread with a followUp explaining why the issue still needs changes and keep CHANGES_REQUESTED.
|
|
11
11
|
Do not duplicate an existing unresolved thread as a newFinding. Use newFindings only for separate new issues.
|
|
12
|
+
Every newFinding must target a valid right-side line in the PR diff.
|
|
13
|
+
If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior. This includes but is not limited to missing validation, missing wiring, missing requirements, missing tests, missing documentation, affected configuration, or relevant call sites.
|
|
14
|
+
Do not omit line. Do not create file-level or body-only newFindings.
|
|
12
15
|
|
|
13
16
|
{ciFailureContextBlock}
|
|
14
17
|
Do not edit files or perform write operations.
|