opencode-magi 0.3.0 → 0.4.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/dist/config/validate.js +51 -0
- package/dist/github/commands.js +39 -5
- package/dist/orchestrator/inline-comments.js +6 -0
- package/dist/orchestrator/merge.js +92 -31
- package/dist/orchestrator/report.js +10 -6
- package/dist/orchestrator/review.js +73 -5
- package/dist/prompts/compose.js +1 -0
- package/dist/prompts/contracts.js +14 -8
- package/dist/prompts/output.js +25 -9
- package/dist/prompts/templates/merge/edit.md +12 -5
- package/package.json +1 -1
- package/schema.json +37 -5
package/dist/config/validate.js
CHANGED
|
@@ -157,6 +157,56 @@ function ghHostOption(config) {
|
|
|
157
157
|
function isPlainObject(value) {
|
|
158
158
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
159
159
|
}
|
|
160
|
+
function expandAgentRefUse(value, path, refs, refsInvalid, errors) {
|
|
161
|
+
if (!isPlainObject(value) || !Object.hasOwn(value, "ref"))
|
|
162
|
+
return value;
|
|
163
|
+
const use = { ...value };
|
|
164
|
+
const ref = use.ref;
|
|
165
|
+
delete use.ref;
|
|
166
|
+
if (typeof ref !== "string") {
|
|
167
|
+
errors.push(`${path}.ref must be a string`);
|
|
168
|
+
return use;
|
|
169
|
+
}
|
|
170
|
+
if (refsInvalid) {
|
|
171
|
+
errors.push(`agents.refs must be an object to resolve ${path}.ref`);
|
|
172
|
+
return use;
|
|
173
|
+
}
|
|
174
|
+
const preset = refs?.[ref];
|
|
175
|
+
if (preset == null) {
|
|
176
|
+
errors.push(`${path}.ref references unknown agents.refs preset: ${ref}`);
|
|
177
|
+
return use;
|
|
178
|
+
}
|
|
179
|
+
if (!isPlainObject(preset)) {
|
|
180
|
+
errors.push(`agents.refs.${ref} must be an object when referenced by ${path}.ref`);
|
|
181
|
+
return use;
|
|
182
|
+
}
|
|
183
|
+
const presetFields = { ...preset };
|
|
184
|
+
delete presetFields.ref;
|
|
185
|
+
return { ...presetFields, ...use };
|
|
186
|
+
}
|
|
187
|
+
function expandAgentRefs(config, errors) {
|
|
188
|
+
if (!config || typeof config !== "object")
|
|
189
|
+
return;
|
|
190
|
+
const magiConfig = config;
|
|
191
|
+
const agents = magiConfig.agents;
|
|
192
|
+
const refsValue = isPlainObject(agents) ? agents.refs : undefined;
|
|
193
|
+
const refsInvalid = refsValue != null && !isPlainObject(refsValue);
|
|
194
|
+
const refs = isPlainObject(refsValue) ? refsValue : undefined;
|
|
195
|
+
if (Array.isArray(magiConfig.review?.agents)) {
|
|
196
|
+
magiConfig.review.agents = magiConfig.review.agents.map((agent, index) => expandAgentRefUse(agent, `review.agents[${index}]`, refs, refsInvalid, errors));
|
|
197
|
+
}
|
|
198
|
+
if (isPlainObject(magiConfig.merge?.editor)) {
|
|
199
|
+
magiConfig.merge.editor = expandAgentRefUse(magiConfig.merge.editor, "merge.editor", refs, refsInvalid, errors);
|
|
200
|
+
}
|
|
201
|
+
if (Array.isArray(magiConfig.triage?.agents)) {
|
|
202
|
+
magiConfig.triage.agents = magiConfig.triage.agents.map((agent, index) => expandAgentRefUse(agent, `triage.agents[${index}]`, refs, refsInvalid, errors));
|
|
203
|
+
}
|
|
204
|
+
if (isPlainObject(magiConfig.triage?.creator)) {
|
|
205
|
+
magiConfig.triage.creator = expandAgentRefUse(magiConfig.triage.creator, "triage.creator", refs, refsInvalid, errors);
|
|
206
|
+
}
|
|
207
|
+
if (isPlainObject(magiConfig.agents))
|
|
208
|
+
delete magiConfig.agents.refs;
|
|
209
|
+
}
|
|
160
210
|
function validateKnownKeys(value, path, keys, errors) {
|
|
161
211
|
if (!isPlainObject(value))
|
|
162
212
|
return;
|
|
@@ -792,6 +842,7 @@ export async function validateConfig(config, options = {}) {
|
|
|
792
842
|
const warnings = [];
|
|
793
843
|
if (!config || typeof config !== "object")
|
|
794
844
|
errors.push("config must be an object");
|
|
845
|
+
expandAgentRefs(config, errors);
|
|
795
846
|
if (config && typeof config === "object")
|
|
796
847
|
validateJsonSchema(config, errors);
|
|
797
848
|
validateKnownKeys(config, "config", CONFIG_KEYS, errors);
|
package/dist/github/commands.js
CHANGED
|
@@ -509,6 +509,9 @@ export async function postCloseComment(exec, repository, pr, account, body) {
|
|
|
509
509
|
await rm(payloadPath, { force: true });
|
|
510
510
|
}
|
|
511
511
|
}
|
|
512
|
+
function isInlineFinding(finding) {
|
|
513
|
+
return finding.line != null;
|
|
514
|
+
}
|
|
512
515
|
function findingComment(finding) {
|
|
513
516
|
const comment = {
|
|
514
517
|
body: `**Issue:** ${finding.issue}\n\n**Fix:** ${finding.fix}`,
|
|
@@ -529,16 +532,47 @@ function requirementFindingSummary(finding) {
|
|
|
529
532
|
` Fix: ${finding.fix}`,
|
|
530
533
|
].join("\n");
|
|
531
534
|
}
|
|
535
|
+
function findingLocation(finding) {
|
|
536
|
+
if (finding.line == null)
|
|
537
|
+
return finding.path;
|
|
538
|
+
if (finding.startLine == null)
|
|
539
|
+
return `${finding.path}:${finding.line}`;
|
|
540
|
+
return `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
541
|
+
}
|
|
542
|
+
function findingSummary(finding) {
|
|
543
|
+
return [
|
|
544
|
+
`- ${findingLocation(finding)}: ${finding.issue}`,
|
|
545
|
+
` Fix: ${finding.fix}`,
|
|
546
|
+
]
|
|
547
|
+
.filter(Boolean)
|
|
548
|
+
.join("\n");
|
|
549
|
+
}
|
|
550
|
+
function changesRequestedBody(findings, requirementFindings) {
|
|
551
|
+
const inlineFindings = findings.filter(isInlineFinding);
|
|
552
|
+
const fileLevelFindings = findings.filter((finding) => !isInlineFinding(finding));
|
|
553
|
+
const sections = [];
|
|
554
|
+
if (inlineFindings.length) {
|
|
555
|
+
sections.push(["Inline findings:", ...inlineFindings.map(findingSummary)].join("\n"));
|
|
556
|
+
}
|
|
557
|
+
if (fileLevelFindings.length) {
|
|
558
|
+
sections.push(["File-level findings:", ...fileLevelFindings.map(findingSummary)].join("\n"));
|
|
559
|
+
}
|
|
560
|
+
if (requirementFindings.length) {
|
|
561
|
+
sections.push([
|
|
562
|
+
"Requirement findings:",
|
|
563
|
+
...requirementFindings.map(requirementFindingSummary),
|
|
564
|
+
].join("\n"));
|
|
565
|
+
}
|
|
566
|
+
return sections.join("\n\n");
|
|
567
|
+
}
|
|
532
568
|
export async function postChangesRequested(exec, repository, pr, account, findings, requirementFindings = []) {
|
|
533
569
|
const token = await ghToken(exec, repository, account);
|
|
534
570
|
const payloadPath = join(tmpdir(), `magi-review-${process.pid}-${Date.now()}.json`);
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
.concat(requirementFindings.map(requirementFindingSummary))
|
|
538
|
-
.join("\n");
|
|
571
|
+
const inlineFindings = findings.filter(isInlineFinding);
|
|
572
|
+
const body = changesRequestedBody(findings, requirementFindings);
|
|
539
573
|
await writeFile(payloadPath, JSON.stringify({
|
|
540
574
|
body,
|
|
541
|
-
comments:
|
|
575
|
+
comments: inlineFindings.map(findingComment),
|
|
542
576
|
event: "REQUEST_CHANGES",
|
|
543
577
|
}));
|
|
544
578
|
try {
|
|
@@ -52,6 +52,12 @@ function assertPositiveInteger(value, name) {
|
|
|
52
52
|
export function validateInlineCommentTargets(findings, targets, label = "findings") {
|
|
53
53
|
for (const [index, finding] of findings.entries()) {
|
|
54
54
|
const name = `${label}[${index}]`;
|
|
55
|
+
if (finding.line == null) {
|
|
56
|
+
if (finding.startLine != null) {
|
|
57
|
+
throw new Error(`${name}.startLine requires line`);
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
55
61
|
assertPositiveInteger(finding.line, `${name}.line`);
|
|
56
62
|
if (finding.startLine != null) {
|
|
57
63
|
assertPositiveInteger(finding.startLine, `${name}.startLine`);
|
|
@@ -50,7 +50,7 @@ async function withReviewerFailureProgress(input) {
|
|
|
50
50
|
throw error;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
async function runEditor(input, worktreePath, cycle, unresolvedThreads) {
|
|
53
|
+
async function runEditor(input, worktreePath, cycle, reviewFindings, unresolvedThreads) {
|
|
54
54
|
const editor = input.repository.agents.editor;
|
|
55
55
|
if (!editor)
|
|
56
56
|
throw new Error("agents.editor is required for magi_merge");
|
|
@@ -64,6 +64,7 @@ async function runEditor(input, worktreePath, cycle, unresolvedThreads) {
|
|
|
64
64
|
directory: input.directory,
|
|
65
65
|
pr: input.pr,
|
|
66
66
|
repository: input.repository,
|
|
67
|
+
reviewFindings: JSON.stringify(reviewFindings, null, 2),
|
|
67
68
|
unresolvedThreads: JSON.stringify(unresolvedThreads, null, 2),
|
|
68
69
|
worktreePath,
|
|
69
70
|
});
|
|
@@ -145,14 +146,14 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
145
146
|
if (output.verdict === "CLOSE") {
|
|
146
147
|
return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
|
|
147
148
|
}
|
|
148
|
-
if (output.newFindings.length) {
|
|
149
|
+
if (output.newFindings.length || output.requirementFindings.length) {
|
|
149
150
|
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
|
|
150
151
|
fix: "Please address this before merging.",
|
|
151
152
|
issue: finding.body,
|
|
152
|
-
line: finding.line,
|
|
153
153
|
path: finding.path,
|
|
154
|
+
...(finding.line == null ? {} : { line: finding.line }),
|
|
154
155
|
startLine: finding.startLine,
|
|
155
|
-
})));
|
|
156
|
+
})), output.requirementFindings);
|
|
156
157
|
}
|
|
157
158
|
return replies[0] ?? "";
|
|
158
159
|
}
|
|
@@ -161,6 +162,51 @@ function parseRereviewOutputWithInlineTargets(text, targets) {
|
|
|
161
162
|
validateInlineCommentTargets(output.newFindings, targets, "newFindings");
|
|
162
163
|
return output;
|
|
163
164
|
}
|
|
165
|
+
function newFindingToEditorFinding(reviewer, finding) {
|
|
166
|
+
return {
|
|
167
|
+
body: finding.body,
|
|
168
|
+
fix: "Please address this before merging.",
|
|
169
|
+
path: finding.path,
|
|
170
|
+
reviewer,
|
|
171
|
+
...(finding.line == null ? {} : { line: finding.line }),
|
|
172
|
+
...(finding.startLine == null ? {} : { startLine: finding.startLine }),
|
|
173
|
+
type: finding.line == null ? "file" : "inline",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function blockingReviewFindings(outputs) {
|
|
177
|
+
return Object.entries(outputs).flatMap(([reviewer, output]) => {
|
|
178
|
+
if (output.verdict !== "CHANGES_REQUESTED")
|
|
179
|
+
return [];
|
|
180
|
+
const requirementFindings = output.requirementFindings.map((finding) => ({
|
|
181
|
+
evidence: finding.evidence,
|
|
182
|
+
fix: finding.fix,
|
|
183
|
+
issueNumber: finding.issueNumber,
|
|
184
|
+
requirement: finding.requirement,
|
|
185
|
+
reviewer,
|
|
186
|
+
type: "requirement",
|
|
187
|
+
}));
|
|
188
|
+
if ("findings" in output) {
|
|
189
|
+
return [
|
|
190
|
+
...output.findings.map((finding) => ({
|
|
191
|
+
fix: finding.fix,
|
|
192
|
+
issue: finding.issue,
|
|
193
|
+
path: finding.path,
|
|
194
|
+
reviewer,
|
|
195
|
+
...(finding.line == null ? {} : { line: finding.line }),
|
|
196
|
+
...(finding.startLine == null
|
|
197
|
+
? {}
|
|
198
|
+
: { startLine: finding.startLine }),
|
|
199
|
+
type: finding.line == null ? "file" : "inline",
|
|
200
|
+
})),
|
|
201
|
+
...requirementFindings,
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
return [
|
|
205
|
+
...output.newFindings.map((finding) => newFindingToEditorFinding(reviewer, finding)),
|
|
206
|
+
...requirementFindings,
|
|
207
|
+
];
|
|
208
|
+
});
|
|
209
|
+
}
|
|
164
210
|
async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionIds, ciFailureContext, options = {}) {
|
|
165
211
|
throwIfAborted(input.signal);
|
|
166
212
|
const meta = await fetchPullRequest(input.exec, input.repository, input.pr);
|
|
@@ -460,15 +506,42 @@ function syntheticReviewThreads(outputs) {
|
|
|
460
506
|
const threads = {};
|
|
461
507
|
for (const [reviewer, output] of Object.entries(outputs)) {
|
|
462
508
|
if ("findings" in output) {
|
|
463
|
-
threads[reviewer] = output.findings.
|
|
509
|
+
threads[reviewer] = output.findings.flatMap((finding) => {
|
|
510
|
+
if (finding.line == null)
|
|
511
|
+
return [];
|
|
464
512
|
const commentId = nextCommentId--;
|
|
465
|
-
return
|
|
466
|
-
|
|
513
|
+
return [
|
|
514
|
+
{
|
|
515
|
+
body: `Issue: ${finding.issue}\n\nFix: ${finding.fix}`,
|
|
516
|
+
commentId,
|
|
517
|
+
comments: [
|
|
518
|
+
{
|
|
519
|
+
author: reviewer,
|
|
520
|
+
body: `Issue: ${finding.issue}\n\nFix: ${finding.fix}`,
|
|
521
|
+
commentId,
|
|
522
|
+
createdAt: new Date(0).toISOString(),
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
line: finding.line,
|
|
526
|
+
path: finding.path,
|
|
527
|
+
threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
|
|
528
|
+
},
|
|
529
|
+
];
|
|
530
|
+
});
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
threads[reviewer] = output.newFindings.flatMap((finding) => {
|
|
534
|
+
if (finding.line == null)
|
|
535
|
+
return [];
|
|
536
|
+
const commentId = nextCommentId--;
|
|
537
|
+
return [
|
|
538
|
+
{
|
|
539
|
+
body: finding.body,
|
|
467
540
|
commentId,
|
|
468
541
|
comments: [
|
|
469
542
|
{
|
|
470
543
|
author: reviewer,
|
|
471
|
-
body:
|
|
544
|
+
body: finding.body,
|
|
472
545
|
commentId,
|
|
473
546
|
createdAt: new Date(0).toISOString(),
|
|
474
547
|
},
|
|
@@ -476,27 +549,8 @@ function syntheticReviewThreads(outputs) {
|
|
|
476
549
|
line: finding.line,
|
|
477
550
|
path: finding.path,
|
|
478
551
|
threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
threads[reviewer] = output.newFindings.map((finding) => {
|
|
484
|
-
const commentId = nextCommentId--;
|
|
485
|
-
return {
|
|
486
|
-
body: finding.body,
|
|
487
|
-
commentId,
|
|
488
|
-
comments: [
|
|
489
|
-
{
|
|
490
|
-
author: reviewer,
|
|
491
|
-
body: finding.body,
|
|
492
|
-
commentId,
|
|
493
|
-
createdAt: new Date(0).toISOString(),
|
|
494
|
-
},
|
|
495
|
-
],
|
|
496
|
-
line: finding.line,
|
|
497
|
-
path: finding.path,
|
|
498
|
-
threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
|
|
499
|
-
};
|
|
552
|
+
},
|
|
553
|
+
];
|
|
500
554
|
});
|
|
501
555
|
}
|
|
502
556
|
return threads;
|
|
@@ -666,7 +720,14 @@ export async function runMerge(input) {
|
|
|
666
720
|
maxThreadResolutionCycles: input.repository.merge.maxThreadResolutionCycles,
|
|
667
721
|
threads: unresolvedThreads,
|
|
668
722
|
});
|
|
669
|
-
|
|
723
|
+
const editorFindings = blockingReviewFindings(reportOutputs);
|
|
724
|
+
const editableFindings = editableThreads.length
|
|
725
|
+
? editorFindings
|
|
726
|
+
: editorFindings.filter((finding) => finding.type !== "inline");
|
|
727
|
+
const findingAttemptsExhausted = input.repository.merge.maxThreadResolutionCycles !== 0 &&
|
|
728
|
+
cycle > input.repository.merge.maxThreadResolutionCycles;
|
|
729
|
+
if (!editableThreads.length &&
|
|
730
|
+
(!editableFindings.length || findingAttemptsExhausted)) {
|
|
670
731
|
await input.onProgress?.({
|
|
671
732
|
status: "changes_unresolved",
|
|
672
733
|
type: "merge_completed",
|
|
@@ -693,7 +754,7 @@ export async function runMerge(input) {
|
|
|
693
754
|
});
|
|
694
755
|
if (!review.worktreePath)
|
|
695
756
|
throw new Error("Review worktree is missing");
|
|
696
|
-
const editorOutput = await runEditor(abortableInput, review.worktreePath, cycle, editableThreads);
|
|
757
|
+
const editorOutput = await runEditor(abortableInput, review.worktreePath, cycle, editableFindings, editableThreads);
|
|
697
758
|
editorOutputs.push(editorOutput);
|
|
698
759
|
dryRunThreads = input.dryRun
|
|
699
760
|
? appendDryRunEditorResponses({
|
|
@@ -16,15 +16,19 @@ function pullRequestLine(input) {
|
|
|
16
16
|
return `- **Pull Request**: [#${input.pr}](${url})`;
|
|
17
17
|
}
|
|
18
18
|
function formatFinding(finding) {
|
|
19
|
-
const line = finding.
|
|
20
|
-
?
|
|
21
|
-
:
|
|
19
|
+
const line = finding.line == null
|
|
20
|
+
? finding.path
|
|
21
|
+
: finding.startLine == null
|
|
22
|
+
? `${finding.path}:${finding.line}`
|
|
23
|
+
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
22
24
|
return `\`${line}\`: ${finding.issue}`;
|
|
23
25
|
}
|
|
24
26
|
function formatRereviewFinding(finding) {
|
|
25
|
-
const line = finding.
|
|
26
|
-
?
|
|
27
|
-
:
|
|
27
|
+
const line = finding.line == null
|
|
28
|
+
? finding.path
|
|
29
|
+
: finding.startLine == null
|
|
30
|
+
? `${finding.path}:${finding.line}`
|
|
31
|
+
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
28
32
|
return `\`${line}\`: ${finding.body}`;
|
|
29
33
|
}
|
|
30
34
|
function formatRequirementFinding(finding) {
|
|
@@ -135,8 +135,69 @@ 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 { path: location };
|
|
151
|
+
}
|
|
152
|
+
function reviewFindingsFromBody(body) {
|
|
153
|
+
const findings = [];
|
|
154
|
+
const requirementFindings = [];
|
|
155
|
+
const lines = (body ?? "").split(/\r?\n/);
|
|
156
|
+
let section;
|
|
157
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
158
|
+
const line = lines[index];
|
|
159
|
+
if (line === "Inline findings:" || line === "File-level findings:") {
|
|
160
|
+
section = "finding";
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (line === "Requirement findings:") {
|
|
164
|
+
section = "requirement";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (section === "finding") {
|
|
168
|
+
const match = /^- (.*): (.+)$/.exec(line ?? "");
|
|
169
|
+
const fix = /^\s+Fix: (.+)$/.exec(lines[index + 1] ?? "");
|
|
170
|
+
if (!match || !fix)
|
|
171
|
+
continue;
|
|
172
|
+
findings.push({
|
|
173
|
+
...parsePostedFindingLocation(match[1] ?? ""),
|
|
174
|
+
fix: fix[1] ?? "Please address this before merging.",
|
|
175
|
+
issue: match[2] ?? "Review finding.",
|
|
176
|
+
});
|
|
177
|
+
index += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (section !== "requirement")
|
|
181
|
+
continue;
|
|
182
|
+
const match = /^- Missing issue #(\d+) requirement: (.+)$/.exec(line ?? "");
|
|
183
|
+
const evidence = /^\s+Evidence: (.+)$/.exec(lines[index + 1] ?? "");
|
|
184
|
+
const fix = /^\s+Fix: (.+)$/.exec(lines[index + 2] ?? "");
|
|
185
|
+
if (!match || !evidence || !fix)
|
|
186
|
+
continue;
|
|
187
|
+
requirementFindings.push({
|
|
188
|
+
evidence: evidence[1] ?? "See review body.",
|
|
189
|
+
fix: fix[1] ?? "Please address this before merging.",
|
|
190
|
+
issueNumber: Number(match[1]),
|
|
191
|
+
requirement: match[2] ?? "Review requirement.",
|
|
192
|
+
});
|
|
193
|
+
index += 2;
|
|
194
|
+
}
|
|
195
|
+
return { findings, requirementFindings };
|
|
196
|
+
}
|
|
197
|
+
export function reviewOutputFromState(review) {
|
|
139
198
|
const verdict = reviewStateToVerdict(review.state);
|
|
199
|
+
if (verdict === "CHANGES_REQUESTED")
|
|
200
|
+
return { ...reviewFindingsFromBody(review.body), verdict };
|
|
140
201
|
return verdict === "CLOSE"
|
|
141
202
|
? {
|
|
142
203
|
findings: [],
|
|
@@ -173,8 +234,8 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
173
234
|
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
|
|
174
235
|
fix: "Please address this before merging.",
|
|
175
236
|
issue: finding.body,
|
|
176
|
-
line: finding.line,
|
|
177
237
|
path: finding.path,
|
|
238
|
+
...(finding.line == null ? {} : { line: finding.line }),
|
|
178
239
|
startLine: finding.startLine,
|
|
179
240
|
})), output.requirementFindings);
|
|
180
241
|
}
|
|
@@ -761,7 +822,14 @@ export async function runReview(input) {
|
|
|
761
822
|
sessionIds,
|
|
762
823
|
worktreePath,
|
|
763
824
|
});
|
|
764
|
-
const
|
|
825
|
+
const activeOutputs = validation.outputs;
|
|
826
|
+
const skippedOutputs = Object.fromEntries(input.repository.agents.reviewers.flatMap((reviewer) => {
|
|
827
|
+
const assignment = mode.assignments.get(reviewer.account);
|
|
828
|
+
return assignment?.type === "skip"
|
|
829
|
+
? [[reviewer.key, reviewOutputFromState(assignment.review)]]
|
|
830
|
+
: [];
|
|
831
|
+
}));
|
|
832
|
+
const outputs = { ...skippedOutputs, ...activeOutputs };
|
|
765
833
|
const remainingSkippedVerdicts = input.repository.agents.reviewers.flatMap((reviewer) => {
|
|
766
834
|
const assignment = mode.assignments.get(reviewer.account);
|
|
767
835
|
if (assignment?.type !== "skip" || closeTargets.includes(reviewer.key))
|
|
@@ -773,7 +841,7 @@ export async function runReview(input) {
|
|
|
773
841
|
},
|
|
774
842
|
];
|
|
775
843
|
});
|
|
776
|
-
const activeVerdicts = Object.entries(
|
|
844
|
+
const activeVerdicts = Object.entries(activeOutputs).map(([reviewer, output]) => ({
|
|
777
845
|
reviewer,
|
|
778
846
|
verdict: output.verdict,
|
|
779
847
|
}));
|
|
@@ -786,7 +854,7 @@ export async function runReview(input) {
|
|
|
786
854
|
? [[reviewer.key, "skipped: already reviewed current head"]]
|
|
787
855
|
: [];
|
|
788
856
|
})),
|
|
789
|
-
...Object.fromEntries(await Promise.all(Object.entries(
|
|
857
|
+
...Object.fromEntries(await Promise.all(Object.entries(activeOutputs).map(async ([key, output]) => [
|
|
790
858
|
key,
|
|
791
859
|
input.dryRun
|
|
792
860
|
? dryRunReviewPost(key, output)
|
package/dist/prompts/compose.js
CHANGED
|
@@ -31,9 +31,10 @@ Rules:
|
|
|
31
31
|
- CHANGES_REQUESTED requires at least one finding or requirementFinding.
|
|
32
32
|
- CLOSE requires a reason and empty findings and requirementFindings arrays.
|
|
33
33
|
- path must be repository-relative.
|
|
34
|
-
- line
|
|
35
|
-
-
|
|
36
|
-
-
|
|
34
|
+
- line is optional. Include line only when the finding targets a valid line inside the PR diff hunk.
|
|
35
|
+
- startLine is allowed only when line is present and must also refer to a line inside the PR diff hunk.
|
|
36
|
+
- Omit startLine for single-line findings and omit line for file-level or body-only findings.
|
|
37
|
+
- Use requirementFindings only for missing closing-issue requirements; use findings for ordinary file-level issues that do not map cleanly to a diff line.
|
|
37
38
|
</output_contract>`.trim();
|
|
38
39
|
export const rereviewOutputContract = `
|
|
39
40
|
<output_contract>
|
|
@@ -53,9 +54,10 @@ Rules:
|
|
|
53
54
|
- MERGE requires empty followUps, newFindings, and requirementFindings arrays.
|
|
54
55
|
- CHANGES_REQUESTED requires at least one followUp, newFinding, or requirementFinding.
|
|
55
56
|
- CLOSE requires a reason and empty followUps, newFindings, and requirementFindings arrays.
|
|
56
|
-
- line
|
|
57
|
-
-
|
|
58
|
-
-
|
|
57
|
+
- line is optional. Include line only when the newFinding targets a valid line inside the latest PR diff hunk.
|
|
58
|
+
- startLine is allowed only when line is present and must also refer to a line inside the latest PR diff hunk.
|
|
59
|
+
- Omit startLine for single-line findings and omit line for file-level or body-only findings.
|
|
60
|
+
- Use requirementFindings only for missing closing-issue requirements; use newFindings for ordinary file-level issues that do not map cleanly to a diff line.
|
|
59
61
|
</output_contract>`.trim();
|
|
60
62
|
export const findingValidationOutputContract = `
|
|
61
63
|
<output_contract>
|
|
@@ -102,7 +104,9 @@ Rules:
|
|
|
102
104
|
- MERGE requires empty findings and requirementFindings arrays.
|
|
103
105
|
- CHANGES_REQUESTED requires at least one finding or requirementFinding.
|
|
104
106
|
- CLOSE is not allowed in this reconsideration step.
|
|
105
|
-
-
|
|
107
|
+
- line is optional. Include line only when the finding targets a valid line inside the PR diff hunk.
|
|
108
|
+
- startLine is allowed only when line is present.
|
|
109
|
+
- Omit startLine for single-line findings and omit line for file-level or body-only findings.
|
|
106
110
|
</output_contract>`.trim();
|
|
107
111
|
export const rereviewCloseReconsiderationOutputContract = `
|
|
108
112
|
<output_contract>
|
|
@@ -121,7 +125,9 @@ Rules:
|
|
|
121
125
|
- MERGE requires empty followUps, newFindings, and requirementFindings arrays.
|
|
122
126
|
- CHANGES_REQUESTED requires at least one followUp, newFinding, or requirementFinding.
|
|
123
127
|
- CLOSE is not allowed in this reconsideration step.
|
|
124
|
-
-
|
|
128
|
+
- line is optional. Include line only when the newFinding targets a valid line inside the latest PR diff hunk.
|
|
129
|
+
- startLine is allowed only when line is present.
|
|
130
|
+
- Omit startLine for single-line findings and omit line for file-level or body-only findings.
|
|
125
131
|
</output_contract>`.trim();
|
|
126
132
|
export const editOutputContract = `
|
|
127
133
|
<output_contract>
|
package/dist/prompts/output.js
CHANGED
|
@@ -71,6 +71,16 @@ function requireNumber(value, path) {
|
|
|
71
71
|
throw new Error(`${path} must be an integer`);
|
|
72
72
|
return value;
|
|
73
73
|
}
|
|
74
|
+
function optionalLine(value, path) {
|
|
75
|
+
return value == null ? undefined : requireNumber(value, path);
|
|
76
|
+
}
|
|
77
|
+
function optionalStartLine(input) {
|
|
78
|
+
if (input.value == null)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (input.line == null)
|
|
81
|
+
throw new Error(`${input.path} requires line`);
|
|
82
|
+
return requireNumber(input.value, input.path);
|
|
83
|
+
}
|
|
74
84
|
function parseRequirementFindings(value) {
|
|
75
85
|
return (value == null ? [] : requireArray(value, "requirementFindings")).map((finding, index) => {
|
|
76
86
|
const item = finding;
|
|
@@ -173,17 +183,20 @@ export function parseReviewOutput(text) {
|
|
|
173
183
|
throw new Error("verdict must be MERGE, CHANGES_REQUESTED, or CLOSE");
|
|
174
184
|
const findings = requireArray(data.findings, "findings").map((finding, index) => {
|
|
175
185
|
const item = finding;
|
|
186
|
+
const line = optionalLine(item.line, `findings[${index}].line`);
|
|
176
187
|
return {
|
|
177
188
|
fix: requireString(item.fix, `findings[${index}].fix`),
|
|
178
189
|
issue: requireString(item.issue, `findings[${index}].issue`),
|
|
179
|
-
line
|
|
190
|
+
line,
|
|
180
191
|
path: requireString(item.path, `findings[${index}].path`),
|
|
181
192
|
perspective: item.perspective == null
|
|
182
193
|
? undefined
|
|
183
194
|
: requireString(item.perspective, `findings[${index}].perspective`),
|
|
184
|
-
startLine:
|
|
185
|
-
|
|
186
|
-
:
|
|
195
|
+
startLine: optionalStartLine({
|
|
196
|
+
line,
|
|
197
|
+
path: `findings[${index}].startLine`,
|
|
198
|
+
value: item.startLine,
|
|
199
|
+
}),
|
|
187
200
|
};
|
|
188
201
|
});
|
|
189
202
|
const requirementFindings = parseRequirementFindings(data.requirementFindings);
|
|
@@ -229,13 +242,16 @@ export function parseRereviewOutput(text) {
|
|
|
229
242
|
});
|
|
230
243
|
const newFindings = requireArray(data.newFindings, "newFindings").map((item, index) => {
|
|
231
244
|
const value = item;
|
|
245
|
+
const line = optionalLine(value.line, `newFindings[${index}].line`);
|
|
232
246
|
return {
|
|
233
247
|
body: requireString(value.body, `newFindings[${index}].body`),
|
|
234
|
-
line
|
|
248
|
+
line,
|
|
235
249
|
path: requireString(value.path, `newFindings[${index}].path`),
|
|
236
|
-
startLine:
|
|
237
|
-
|
|
238
|
-
:
|
|
250
|
+
startLine: optionalStartLine({
|
|
251
|
+
line,
|
|
252
|
+
path: `newFindings[${index}].startLine`,
|
|
253
|
+
value: value.startLine,
|
|
254
|
+
}),
|
|
239
255
|
};
|
|
240
256
|
});
|
|
241
257
|
const requirementFindings = parseRequirementFindings(data.requirementFindings);
|
|
@@ -349,7 +365,7 @@ function parseEditOutputWithOptions(text, options) {
|
|
|
349
365
|
commentId: requireNumber(value.commentId, `responses[${index}].commentId`),
|
|
350
366
|
};
|
|
351
367
|
});
|
|
352
|
-
if (options.requireResponses && !responses.length)
|
|
368
|
+
if (options.requireResponses && data.mode === "REPLIED" && !responses.length)
|
|
353
369
|
throw new Error("responses must not be empty");
|
|
354
370
|
if (data.mode === "EDITED") {
|
|
355
371
|
if (!filesTouched.length)
|
|
@@ -1,9 +1,16 @@
|
|
|
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. Inline findings target a PR diff line; file-level findings may not have a GitHub thread; requirement findings describe missing closing-issue requirements.
|
|
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.
|
|
15
|
+
File-level and requirement findings may not have a thread to reply to, but they are still blocking and must be addressed.
|
|
9
16
|
Do not make changes just because a reviewer requested them. Do not push.
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
"type": "object",
|
|
10
10
|
"additionalProperties": false,
|
|
11
11
|
"properties": {
|
|
12
|
-
"permissions": { "$ref": "#/$defs/permissions" }
|
|
12
|
+
"permissions": { "$ref": "#/$defs/permissions" },
|
|
13
|
+
"refs": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": { "$ref": "#/$defs/agentRef" }
|
|
16
|
+
}
|
|
13
17
|
}
|
|
14
18
|
},
|
|
15
19
|
"clear": {
|
|
@@ -51,11 +55,33 @@
|
|
|
51
55
|
"triage": { "$ref": "#/$defs/triage" }
|
|
52
56
|
},
|
|
53
57
|
"$defs": {
|
|
58
|
+
"agentRef": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"properties": {
|
|
62
|
+
"id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
|
|
63
|
+
"model": { "type": "string", "minLength": 1 },
|
|
64
|
+
"options": { "type": "object", "additionalProperties": true },
|
|
65
|
+
"account": { "type": "string", "minLength": 1 },
|
|
66
|
+
"author": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"additionalProperties": false,
|
|
69
|
+
"properties": {
|
|
70
|
+
"name": { "type": "string", "minLength": 1 },
|
|
71
|
+
"email": { "type": "string", "minLength": 1 }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"permissions": { "$ref": "#/$defs/permissions" },
|
|
75
|
+
"persona": { "type": "string" }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
54
78
|
"reviewer": {
|
|
55
79
|
"type": "object",
|
|
56
|
-
"
|
|
80
|
+
"if": { "not": { "required": ["ref"] } },
|
|
81
|
+
"then": { "required": ["model", "account"] },
|
|
57
82
|
"additionalProperties": false,
|
|
58
83
|
"properties": {
|
|
84
|
+
"ref": { "type": "string", "minLength": 1 },
|
|
59
85
|
"id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
|
|
60
86
|
"model": { "type": "string", "minLength": 1 },
|
|
61
87
|
"options": { "type": "object", "additionalProperties": true },
|
|
@@ -66,9 +92,11 @@
|
|
|
66
92
|
},
|
|
67
93
|
"editor": {
|
|
68
94
|
"type": "object",
|
|
69
|
-
"
|
|
95
|
+
"if": { "not": { "required": ["ref"] } },
|
|
96
|
+
"then": { "required": ["model", "account", "author"] },
|
|
70
97
|
"additionalProperties": false,
|
|
71
98
|
"properties": {
|
|
99
|
+
"ref": { "type": "string", "minLength": 1 },
|
|
72
100
|
"model": { "type": "string", "minLength": 1 },
|
|
73
101
|
"options": { "type": "object", "additionalProperties": true },
|
|
74
102
|
"account": { "type": "string", "minLength": 1 },
|
|
@@ -87,9 +115,11 @@
|
|
|
87
115
|
},
|
|
88
116
|
"triageAgent": {
|
|
89
117
|
"type": "object",
|
|
90
|
-
"required": ["
|
|
118
|
+
"if": { "not": { "required": ["ref"] } },
|
|
119
|
+
"then": { "required": ["model"] },
|
|
91
120
|
"additionalProperties": false,
|
|
92
121
|
"properties": {
|
|
122
|
+
"ref": { "type": "string", "minLength": 1 },
|
|
93
123
|
"id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
|
|
94
124
|
"model": { "type": "string", "minLength": 1 },
|
|
95
125
|
"options": { "type": "object", "additionalProperties": true },
|
|
@@ -99,9 +129,11 @@
|
|
|
99
129
|
},
|
|
100
130
|
"triageCreator": {
|
|
101
131
|
"type": "object",
|
|
102
|
-
"
|
|
132
|
+
"if": { "not": { "required": ["ref"] } },
|
|
133
|
+
"then": { "required": ["model", "author"] },
|
|
103
134
|
"additionalProperties": false,
|
|
104
135
|
"properties": {
|
|
136
|
+
"ref": { "type": "string", "minLength": 1 },
|
|
105
137
|
"account": { "type": "string", "minLength": 1 },
|
|
106
138
|
"model": { "type": "string", "minLength": 1 },
|
|
107
139
|
"options": { "type": "object", "additionalProperties": true },
|