opencode-magi 0.1.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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/commands.js +18 -0
- package/dist/config/load.js +62 -0
- package/dist/config/output.js +16 -0
- package/dist/config/resolve.js +113 -0
- package/dist/config/validate.js +567 -0
- package/dist/github/commands.js +398 -0
- package/dist/github/retry.js +44 -0
- package/dist/index.js +536 -0
- package/dist/orchestrator/abort.js +9 -0
- package/dist/orchestrator/ci.js +568 -0
- package/dist/orchestrator/findings.js +66 -0
- package/dist/orchestrator/majority.js +48 -0
- package/dist/orchestrator/merge.js +836 -0
- package/dist/orchestrator/model.js +202 -0
- package/dist/orchestrator/pool.js +15 -0
- package/dist/orchestrator/report.js +168 -0
- package/dist/orchestrator/review.js +790 -0
- package/dist/orchestrator/run-manager.js +1663 -0
- package/dist/orchestrator/safety.js +44 -0
- package/dist/permissions/common.json +24 -0
- package/dist/permissions/editor.json +7 -0
- package/dist/prompts/compose.js +298 -0
- package/dist/prompts/contracts.js +189 -0
- package/dist/prompts/output.js +260 -0
- package/dist/prompts/templates/ci-classification-after-edit.md +16 -0
- package/dist/prompts/templates/ci-classification.md +9 -0
- package/dist/prompts/templates/close-reconsideration.md +6 -0
- package/dist/prompts/templates/edit.md +9 -0
- package/dist/prompts/templates/finding-validation.md +7 -0
- package/dist/prompts/templates/rereview-close-reconsideration.md +6 -0
- package/dist/prompts/templates/rereview.md +16 -0
- package/dist/prompts/templates/review.md +7 -0
- package/dist/types.js +1 -0
- package/package.json +69 -0
- package/schema.json +200 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
|
+
import { fetchPullRequestSafetyMeta } from "../github/commands";
|
|
3
|
+
export function evaluateSafetyGate(repository, meta) {
|
|
4
|
+
const reasons = [];
|
|
5
|
+
const labels = new Set(meta.labels);
|
|
6
|
+
const missingLabels = repository.safety.requiredLabels.filter((label) => !labels.has(label));
|
|
7
|
+
if (missingLabels.length) {
|
|
8
|
+
reasons.push(`Missing required labels: ${missingLabels.join(", ")}`);
|
|
9
|
+
}
|
|
10
|
+
if (repository.safety.allowAuthors.length &&
|
|
11
|
+
!repository.safety.allowAuthors.includes(meta.author)) {
|
|
12
|
+
reasons.push(`PR author is not allowed: ${meta.author || "unknown"}`);
|
|
13
|
+
}
|
|
14
|
+
if (repository.safety.maxChangedFiles != null &&
|
|
15
|
+
meta.changedFiles > repository.safety.maxChangedFiles) {
|
|
16
|
+
reasons.push(`Changed files exceed safety limit: ${meta.changedFiles} > ${repository.safety.maxChangedFiles}`);
|
|
17
|
+
}
|
|
18
|
+
if (repository.safety.blockedPaths.length) {
|
|
19
|
+
const isBlocked = picomatch(repository.safety.blockedPaths, { dot: true });
|
|
20
|
+
const blocked = meta.files.filter((file) => isBlocked(file));
|
|
21
|
+
if (blocked.length) {
|
|
22
|
+
reasons.push(`Blocked paths changed: ${blocked.slice(0, 10).join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { meta, ok: reasons.length === 0, reasons };
|
|
26
|
+
}
|
|
27
|
+
export function hasSafetyGate(repository) {
|
|
28
|
+
return Boolean(repository.safety.requiredLabels.length ||
|
|
29
|
+
repository.safety.blockedPaths.length ||
|
|
30
|
+
repository.safety.allowAuthors.length ||
|
|
31
|
+
repository.safety.maxChangedFiles != null);
|
|
32
|
+
}
|
|
33
|
+
export async function checkSafetyGate(input) {
|
|
34
|
+
const meta = await fetchPullRequestSafetyMeta(input.exec, input.repository, input.pr);
|
|
35
|
+
return evaluateSafetyGate(input.repository, meta);
|
|
36
|
+
}
|
|
37
|
+
export function formatSafetyGateReport(result) {
|
|
38
|
+
if (result.ok)
|
|
39
|
+
return "- **Safety**: Passed";
|
|
40
|
+
return [
|
|
41
|
+
"- **Safety**: Blocked",
|
|
42
|
+
...result.reasons.map((reason) => ` - ${reason}`),
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bash": {
|
|
3
|
+
"*": "deny",
|
|
4
|
+
"git status*": "allow",
|
|
5
|
+
"git diff*": "allow",
|
|
6
|
+
"git show*": "allow",
|
|
7
|
+
"git grep*": "allow",
|
|
8
|
+
"git log*": "allow",
|
|
9
|
+
"git blame*": "allow",
|
|
10
|
+
"git ls-files*": "allow",
|
|
11
|
+
"git rev-parse*": "allow",
|
|
12
|
+
"git merge-base*": "allow",
|
|
13
|
+
"rg *": "allow"
|
|
14
|
+
},
|
|
15
|
+
"edit": "deny",
|
|
16
|
+
"glob": "allow",
|
|
17
|
+
"grep": "allow",
|
|
18
|
+
"list": "allow",
|
|
19
|
+
"question": "deny",
|
|
20
|
+
"read": "allow",
|
|
21
|
+
"task": "deny",
|
|
22
|
+
"webfetch": "deny",
|
|
23
|
+
"websearch": "deny"
|
|
24
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { ciClassificationAfterEditOutputContract, ciClassificationOutputContract, closeReconsiderationOutputContract, editOutputContract, findingValidationOutputContract, rereviewCloseReconsiderationOutputContract, rereviewOutputContract, reviewOutputContract, } from "./contracts";
|
|
5
|
+
async function readOptionalPrompt(directory, path, values = {}) {
|
|
6
|
+
if (!path)
|
|
7
|
+
return "";
|
|
8
|
+
const fullPath = promptPath(directory, path);
|
|
9
|
+
return renderTemplate(await readFile(fullPath, "utf8"), values);
|
|
10
|
+
}
|
|
11
|
+
function promptPath(directory, path) {
|
|
12
|
+
if (path === "~")
|
|
13
|
+
return homedir();
|
|
14
|
+
if (path.startsWith("~/"))
|
|
15
|
+
return join(homedir(), path.slice(2));
|
|
16
|
+
return isAbsolute(path) ? path : join(directory, path);
|
|
17
|
+
}
|
|
18
|
+
async function readTemplate(name) {
|
|
19
|
+
return readFile(new URL(`./templates/${name}.md`, import.meta.url), "utf8");
|
|
20
|
+
}
|
|
21
|
+
function renderTemplate(template, values) {
|
|
22
|
+
return template.replaceAll(/\{([A-Za-z0-9_]+)\}/g, (match, key) => values[key] ?? match);
|
|
23
|
+
}
|
|
24
|
+
async function taskBlock(input) {
|
|
25
|
+
const body = input.customPath
|
|
26
|
+
? await readOptionalPrompt(input.directory, input.customPath, input.values)
|
|
27
|
+
: renderTemplate(await readTemplate(input.builtin), input.values);
|
|
28
|
+
return `<task>\n${body.trim()}\n</task>`;
|
|
29
|
+
}
|
|
30
|
+
function repositoryValues(repository) {
|
|
31
|
+
return {
|
|
32
|
+
owner: repository.github.owner,
|
|
33
|
+
repo: repository.github.repo,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function reviewValues(input) {
|
|
37
|
+
const ciFailureContext = input.ciFailureContext?.trim() ?? input.ciFailureLogs?.trim() ?? "";
|
|
38
|
+
return {
|
|
39
|
+
...repositoryValues(input.repository),
|
|
40
|
+
baseSha: input.baseSha,
|
|
41
|
+
ciFailureContext,
|
|
42
|
+
ciFailureContextBlock: ciFailureContext
|
|
43
|
+
? `<ci_failure_context>\n${ciFailureContext}\n</ci_failure_context>`
|
|
44
|
+
: "",
|
|
45
|
+
ciFailureLogs: ciFailureContext,
|
|
46
|
+
ciFailureLogsBlock: ciFailureContext
|
|
47
|
+
? `<ci_failure_context>\n${ciFailureContext}\n</ci_failure_context>`
|
|
48
|
+
: "",
|
|
49
|
+
headSha: input.headSha,
|
|
50
|
+
jsonEncodedWorktreePath: JSON.stringify(input.worktreePath),
|
|
51
|
+
pr: String(input.pr),
|
|
52
|
+
worktreePath: input.worktreePath,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function rereviewValues(input) {
|
|
56
|
+
return {
|
|
57
|
+
...reviewValues(input),
|
|
58
|
+
previousHeadSha: input.previousHeadSha,
|
|
59
|
+
previousReview: input.previousReview ?? "",
|
|
60
|
+
previousReviewBlock: previousReviewBlock(input.previousReview),
|
|
61
|
+
unresolvedThreads: input.unresolvedThreads,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function editValues(input) {
|
|
65
|
+
return {
|
|
66
|
+
...repositoryValues(input.repository),
|
|
67
|
+
pr: String(input.pr),
|
|
68
|
+
unresolvedThreads: input.unresolvedThreads,
|
|
69
|
+
worktreePath: input.worktreePath,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function personaBlock(persona) {
|
|
73
|
+
return persona ? `<persona>\n${persona}\n</persona>` : "";
|
|
74
|
+
}
|
|
75
|
+
function languageBlock(language) {
|
|
76
|
+
return language ? `<language>\n${language}\n</language>` : "";
|
|
77
|
+
}
|
|
78
|
+
function previousReviewBlock(previousReview) {
|
|
79
|
+
return previousReview?.trim()
|
|
80
|
+
? `<previous_review>\n${previousReview.trim()}\n</previous_review>`
|
|
81
|
+
: "";
|
|
82
|
+
}
|
|
83
|
+
async function reviewGuidelinesBlock(input) {
|
|
84
|
+
const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
|
|
85
|
+
return body ? `<review_guidelines>\n${body}\n</review_guidelines>` : "";
|
|
86
|
+
}
|
|
87
|
+
async function editGuidelinesBlock(input) {
|
|
88
|
+
const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
|
|
89
|
+
return body ? `<edit_guidelines>\n${body}\n</edit_guidelines>` : "";
|
|
90
|
+
}
|
|
91
|
+
async function sessionContextBlocks(input) {
|
|
92
|
+
return [
|
|
93
|
+
input.includeSessionContext ? languageBlock(input.repository.language) : "",
|
|
94
|
+
input.includeSessionContext ? personaBlock(input.reviewer.persona) : "",
|
|
95
|
+
input.includeReviewGuidelines
|
|
96
|
+
? await reviewGuidelinesBlock({
|
|
97
|
+
directory: input.directory,
|
|
98
|
+
path: input.repository.prompts.reviewGuidelines,
|
|
99
|
+
values: input.values,
|
|
100
|
+
})
|
|
101
|
+
: "",
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
export async function composeReviewPrompt(input) {
|
|
105
|
+
const values = reviewValues(input);
|
|
106
|
+
const task = await taskBlock({
|
|
107
|
+
builtin: "review",
|
|
108
|
+
customPath: input.repository.prompts.review,
|
|
109
|
+
directory: input.directory,
|
|
110
|
+
values,
|
|
111
|
+
});
|
|
112
|
+
return [
|
|
113
|
+
task,
|
|
114
|
+
languageBlock(input.repository.language),
|
|
115
|
+
personaBlock(input.reviewer.persona),
|
|
116
|
+
await reviewGuidelinesBlock({
|
|
117
|
+
directory: input.directory,
|
|
118
|
+
path: input.repository.prompts.reviewGuidelines,
|
|
119
|
+
values,
|
|
120
|
+
}),
|
|
121
|
+
reviewOutputContract,
|
|
122
|
+
]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join("\n\n");
|
|
125
|
+
}
|
|
126
|
+
export async function composeRereviewPrompt(input) {
|
|
127
|
+
const values = rereviewValues(input);
|
|
128
|
+
const task = await taskBlock({
|
|
129
|
+
builtin: "rereview",
|
|
130
|
+
customPath: input.repository.prompts.rereview,
|
|
131
|
+
directory: input.directory,
|
|
132
|
+
values,
|
|
133
|
+
});
|
|
134
|
+
return [
|
|
135
|
+
task,
|
|
136
|
+
input.includeSessionContext === false
|
|
137
|
+
? ""
|
|
138
|
+
: languageBlock(input.repository.language),
|
|
139
|
+
input.includeSessionContext === false
|
|
140
|
+
? ""
|
|
141
|
+
: personaBlock(input.reviewer.persona),
|
|
142
|
+
input.includeReviewGuidelines === false
|
|
143
|
+
? ""
|
|
144
|
+
: await reviewGuidelinesBlock({
|
|
145
|
+
directory: input.directory,
|
|
146
|
+
path: input.repository.prompts.reviewGuidelines,
|
|
147
|
+
values,
|
|
148
|
+
}),
|
|
149
|
+
rereviewOutputContract,
|
|
150
|
+
]
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.join("\n\n");
|
|
153
|
+
}
|
|
154
|
+
export async function composeEditPrompt(input) {
|
|
155
|
+
const values = editValues(input);
|
|
156
|
+
const task = await taskBlock({
|
|
157
|
+
builtin: "edit",
|
|
158
|
+
customPath: input.repository.prompts.edit,
|
|
159
|
+
directory: input.directory,
|
|
160
|
+
values,
|
|
161
|
+
});
|
|
162
|
+
const persona = input.repository.agents.editor?.persona;
|
|
163
|
+
return [
|
|
164
|
+
task,
|
|
165
|
+
languageBlock(input.repository.language),
|
|
166
|
+
personaBlock(persona),
|
|
167
|
+
await editGuidelinesBlock({
|
|
168
|
+
directory: input.directory,
|
|
169
|
+
path: input.repository.prompts.editGuidelines,
|
|
170
|
+
values,
|
|
171
|
+
}),
|
|
172
|
+
editOutputContract,
|
|
173
|
+
]
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.join("\n\n");
|
|
176
|
+
}
|
|
177
|
+
export async function composeFindingValidationPrompt(input) {
|
|
178
|
+
const values = { ...reviewValues(input), findings: input.findings };
|
|
179
|
+
const task = await taskBlock({
|
|
180
|
+
builtin: "finding-validation",
|
|
181
|
+
customPath: input.repository.prompts.findingValidation,
|
|
182
|
+
directory: input.directory,
|
|
183
|
+
values,
|
|
184
|
+
});
|
|
185
|
+
return [
|
|
186
|
+
task,
|
|
187
|
+
...(await sessionContextBlocks({
|
|
188
|
+
directory: input.directory,
|
|
189
|
+
includeReviewGuidelines: input.includeReviewGuidelines,
|
|
190
|
+
includeSessionContext: input.includeSessionContext,
|
|
191
|
+
repository: input.repository,
|
|
192
|
+
reviewer: input.reviewer,
|
|
193
|
+
values,
|
|
194
|
+
})),
|
|
195
|
+
findingValidationOutputContract,
|
|
196
|
+
]
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.join("\n\n");
|
|
199
|
+
}
|
|
200
|
+
export async function composeCloseReconsiderationPrompt(input) {
|
|
201
|
+
const values = {
|
|
202
|
+
...reviewValues(input),
|
|
203
|
+
closeReason: input.closeReason ?? "",
|
|
204
|
+
};
|
|
205
|
+
const task = await taskBlock({
|
|
206
|
+
builtin: "close-reconsideration",
|
|
207
|
+
customPath: input.repository.prompts.closeReconsideration,
|
|
208
|
+
directory: input.directory,
|
|
209
|
+
values,
|
|
210
|
+
});
|
|
211
|
+
return [
|
|
212
|
+
task,
|
|
213
|
+
...(await sessionContextBlocks({
|
|
214
|
+
directory: input.directory,
|
|
215
|
+
includeReviewGuidelines: input.includeReviewGuidelines,
|
|
216
|
+
includeSessionContext: input.includeSessionContext,
|
|
217
|
+
repository: input.repository,
|
|
218
|
+
reviewer: input.reviewer,
|
|
219
|
+
values,
|
|
220
|
+
})),
|
|
221
|
+
closeReconsiderationOutputContract,
|
|
222
|
+
]
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.join("\n\n");
|
|
225
|
+
}
|
|
226
|
+
export async function composeRereviewCloseReconsiderationPrompt(input) {
|
|
227
|
+
const values = {
|
|
228
|
+
...reviewValues(input),
|
|
229
|
+
closeReason: input.closeReason ?? "",
|
|
230
|
+
previousHeadSha: input.previousHeadSha,
|
|
231
|
+
};
|
|
232
|
+
const task = await taskBlock({
|
|
233
|
+
builtin: "rereview-close-reconsideration",
|
|
234
|
+
customPath: input.repository.prompts.rereviewCloseReconsideration,
|
|
235
|
+
directory: input.directory,
|
|
236
|
+
values,
|
|
237
|
+
});
|
|
238
|
+
return [
|
|
239
|
+
task,
|
|
240
|
+
...(await sessionContextBlocks({
|
|
241
|
+
directory: input.directory,
|
|
242
|
+
includeReviewGuidelines: input.includeReviewGuidelines,
|
|
243
|
+
includeSessionContext: input.includeSessionContext,
|
|
244
|
+
repository: input.repository,
|
|
245
|
+
reviewer: input.reviewer,
|
|
246
|
+
values,
|
|
247
|
+
})),
|
|
248
|
+
rereviewCloseReconsiderationOutputContract,
|
|
249
|
+
]
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.join("\n\n");
|
|
252
|
+
}
|
|
253
|
+
export async function composeCiClassificationPrompt(input) {
|
|
254
|
+
const values = {
|
|
255
|
+
...repositoryValues(input.repository),
|
|
256
|
+
failedChecks: JSON.stringify(input.checks, null, 2),
|
|
257
|
+
pr: String(input.pr),
|
|
258
|
+
};
|
|
259
|
+
const task = await taskBlock({
|
|
260
|
+
builtin: "ci-classification",
|
|
261
|
+
customPath: input.repository.prompts.ciClassification,
|
|
262
|
+
directory: input.directory,
|
|
263
|
+
values,
|
|
264
|
+
});
|
|
265
|
+
return [
|
|
266
|
+
task,
|
|
267
|
+
languageBlock(input.repository.language),
|
|
268
|
+
ciClassificationOutputContract,
|
|
269
|
+
]
|
|
270
|
+
.filter(Boolean)
|
|
271
|
+
.join("\n\n");
|
|
272
|
+
}
|
|
273
|
+
export async function composeCiClassificationAfterEditPrompt(input) {
|
|
274
|
+
const values = {
|
|
275
|
+
...repositoryValues(input.repository),
|
|
276
|
+
cycle: String(input.cycle),
|
|
277
|
+
failedChecks: JSON.stringify(input.checks, null, 2),
|
|
278
|
+
headSha: input.headSha,
|
|
279
|
+
jsonEncodedWorktreePath: JSON.stringify(input.worktreePath),
|
|
280
|
+
previousHeadSha: input.previousHeadSha,
|
|
281
|
+
pr: String(input.pr),
|
|
282
|
+
worktreePath: input.worktreePath,
|
|
283
|
+
};
|
|
284
|
+
const task = await taskBlock({
|
|
285
|
+
builtin: "ci-classification-after-edit",
|
|
286
|
+
customPath: input.repository.prompts.ciClassificationAfterEdit ??
|
|
287
|
+
input.repository.prompts.ciClassification,
|
|
288
|
+
directory: input.directory,
|
|
289
|
+
values,
|
|
290
|
+
});
|
|
291
|
+
return [
|
|
292
|
+
task,
|
|
293
|
+
languageBlock(input.repository.language),
|
|
294
|
+
ciClassificationAfterEditOutputContract,
|
|
295
|
+
]
|
|
296
|
+
.filter(Boolean)
|
|
297
|
+
.join("\n\n");
|
|
298
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export const reviewOutputContract = `
|
|
2
|
+
<output_contract>
|
|
3
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
4
|
+
|
|
5
|
+
The object must match this shape:
|
|
6
|
+
{
|
|
7
|
+
"verdict": "MERGE" | "CHANGES_REQUESTED" | "CLOSE",
|
|
8
|
+
"findings": [
|
|
9
|
+
{
|
|
10
|
+
"path": "relative/path.ext",
|
|
11
|
+
"line": 123,
|
|
12
|
+
"startLine": 120,
|
|
13
|
+
"issue": "What is wrong.",
|
|
14
|
+
"fix": "How to fix it.",
|
|
15
|
+
"perspective": "Optional review perspective."
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"reason": "Required only for CLOSE."
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Rules:
|
|
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.
|
|
25
|
+
- path must be repository-relative.
|
|
26
|
+
- line and startLine must refer to lines inside the PR diff hunk.
|
|
27
|
+
- Omit startLine for single-line findings.
|
|
28
|
+
</output_contract>`.trim();
|
|
29
|
+
export const rereviewOutputContract = `
|
|
30
|
+
<output_contract>
|
|
31
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
32
|
+
|
|
33
|
+
The object must match this shape:
|
|
34
|
+
{
|
|
35
|
+
"verdict": "MERGE" | "CHANGES_REQUESTED" | "CLOSE",
|
|
36
|
+
"resolve": [{ "commentId": 123, "threadId": "..." }],
|
|
37
|
+
"followUps": [{ "commentId": 123, "body": "..." }],
|
|
38
|
+
"newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }],
|
|
39
|
+
"reason": "Required only for CLOSE."
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Rules:
|
|
43
|
+
- MERGE requires empty followUps and newFindings arrays.
|
|
44
|
+
- CHANGES_REQUESTED requires at least one followUp or newFinding.
|
|
45
|
+
- CLOSE requires a reason and empty followUps and newFindings arrays.
|
|
46
|
+
- line and startLine must refer to lines inside the latest PR diff hunk.
|
|
47
|
+
- Omit startLine for single-line findings.
|
|
48
|
+
</output_contract>`.trim();
|
|
49
|
+
export const findingValidationOutputContract = `
|
|
50
|
+
<output_contract>
|
|
51
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
52
|
+
|
|
53
|
+
The object must match this shape:
|
|
54
|
+
{
|
|
55
|
+
"votes": [
|
|
56
|
+
{
|
|
57
|
+
"reviewer": "reviewer-key-that-authored-the-finding",
|
|
58
|
+
"findingIndex": 0,
|
|
59
|
+
"vote": "AGREE" | "DISAGREE",
|
|
60
|
+
"reason": "Optional short rationale."
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Rules:
|
|
66
|
+
- Vote on every finding listed in the task.
|
|
67
|
+
- Do not vote on your own findings.
|
|
68
|
+
- AGREE means the finding should remain posted.
|
|
69
|
+
- DISAGREE means the finding should be discarded.
|
|
70
|
+
</output_contract>`.trim();
|
|
71
|
+
export const closeReconsiderationOutputContract = `
|
|
72
|
+
<output_contract>
|
|
73
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
74
|
+
|
|
75
|
+
The object must match this shape:
|
|
76
|
+
{
|
|
77
|
+
"verdict": "MERGE" | "CHANGES_REQUESTED",
|
|
78
|
+
"findings": [
|
|
79
|
+
{
|
|
80
|
+
"path": "relative/path.ext",
|
|
81
|
+
"line": 123,
|
|
82
|
+
"startLine": 120,
|
|
83
|
+
"issue": "What is wrong.",
|
|
84
|
+
"fix": "How to fix it."
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Rules:
|
|
90
|
+
- MERGE requires an empty findings array.
|
|
91
|
+
- CHANGES_REQUESTED requires at least one finding.
|
|
92
|
+
- CLOSE is not allowed in this reconsideration step.
|
|
93
|
+
- Omit startLine for single-line findings.
|
|
94
|
+
</output_contract>`.trim();
|
|
95
|
+
export const rereviewCloseReconsiderationOutputContract = `
|
|
96
|
+
<output_contract>
|
|
97
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
98
|
+
|
|
99
|
+
The object must match this shape:
|
|
100
|
+
{
|
|
101
|
+
"verdict": "MERGE" | "CHANGES_REQUESTED",
|
|
102
|
+
"resolve": [{ "commentId": 123, "threadId": "..." }],
|
|
103
|
+
"followUps": [{ "commentId": 123, "body": "..." }],
|
|
104
|
+
"newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Rules:
|
|
108
|
+
- MERGE requires empty followUps and newFindings arrays.
|
|
109
|
+
- CHANGES_REQUESTED requires at least one followUp or newFinding.
|
|
110
|
+
- CLOSE is not allowed in this reconsideration step.
|
|
111
|
+
- Omit startLine for single-line findings.
|
|
112
|
+
</output_contract>`.trim();
|
|
113
|
+
export const editOutputContract = `
|
|
114
|
+
<output_contract>
|
|
115
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
116
|
+
|
|
117
|
+
The object must match this shape:
|
|
118
|
+
{
|
|
119
|
+
"mode": "EDITED" | "REPLIED",
|
|
120
|
+
"commitSha": "full sha, required only when mode is EDITED; omit when mode is REPLIED",
|
|
121
|
+
"commitMessage": "fix(scope): short description, required only when mode is EDITED; omit when mode is REPLIED",
|
|
122
|
+
"filesTouched": ["relative/path.ext"],
|
|
123
|
+
"responses": [{ "commentId": 123, "action": "FIXED" | "DISAGREE" | "ASK", "body": "Fixed." }]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Rules:
|
|
127
|
+
- Use EDITED only when you edited files, staged changes, and committed.
|
|
128
|
+
- Use REPLIED when you only replied without code changes.
|
|
129
|
+
- FIXED means you agreed with the reviewer and made a code change.
|
|
130
|
+
- DISAGREE means you did not edit because the requested change is incorrect or unnecessary.
|
|
131
|
+
- ASK means you did not edit because you need clarification.
|
|
132
|
+
- Do not make changes just because a reviewer requested them; edit only when you understand and agree.
|
|
133
|
+
- Do not push. The orchestrator pushes after validating this envelope.
|
|
134
|
+
- filesTouched must include every final changed file.
|
|
135
|
+
- responses must include a reply for each thread you addressed.
|
|
136
|
+
- REPLIED requires filesTouched to be empty and at least one DISAGREE or ASK response.
|
|
137
|
+
</output_contract>`.trim();
|
|
138
|
+
export const ciClassificationOutputContract = `
|
|
139
|
+
<output_contract>
|
|
140
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
141
|
+
{
|
|
142
|
+
"checks": [
|
|
143
|
+
{
|
|
144
|
+
"name": "exact failed check name",
|
|
145
|
+
"classification": "SCOPE_IN" | "SCOPE_OUT",
|
|
146
|
+
"reason": "Short reason."
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
Rules:
|
|
151
|
+
- Return one item for every failed check.
|
|
152
|
+
- SCOPE_IN means the failure should be treated as caused by the PR changes and passed to reviewers/editor.
|
|
153
|
+
- SCOPE_OUT means the failure is likely flaky, external, or infrastructure-related and may be rerun.
|
|
154
|
+
- If uncertain, choose SCOPE_IN.
|
|
155
|
+
</output_contract>`.trim();
|
|
156
|
+
export const ciClassificationAfterEditOutputContract = `
|
|
157
|
+
<output_contract>
|
|
158
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
159
|
+
{
|
|
160
|
+
"checks": [
|
|
161
|
+
{
|
|
162
|
+
"name": "exact failed check name",
|
|
163
|
+
"classification": "SCOPE_IN" | "SCOPE_OUT",
|
|
164
|
+
"reason": "Short reason."
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
Rules:
|
|
169
|
+
- Return one item for every failed check.
|
|
170
|
+
- SCOPE_IN means the failure should be treated as caused by the PR changes or the editor changes and passed to reviewers/editor.
|
|
171
|
+
- SCOPE_OUT means the failure is likely flaky, external, or infrastructure-related and may be rerun.
|
|
172
|
+
- If uncertain, choose SCOPE_IN.
|
|
173
|
+
</output_contract>`.trim();
|
|
174
|
+
const outputContractsBySchemaName = {
|
|
175
|
+
"CI classification": ciClassificationOutputContract,
|
|
176
|
+
"close reconsideration": closeReconsiderationOutputContract,
|
|
177
|
+
edit: editOutputContract,
|
|
178
|
+
"finding validation": findingValidationOutputContract,
|
|
179
|
+
rereview: rereviewOutputContract,
|
|
180
|
+
"rereview close reconsideration": rereviewCloseReconsiderationOutputContract,
|
|
181
|
+
review: reviewOutputContract,
|
|
182
|
+
};
|
|
183
|
+
export function repairPrompt(schemaName) {
|
|
184
|
+
const outputContract = outputContractsBySchemaName[schemaName];
|
|
185
|
+
const instructions = `Your previous ${schemaName} output did not match the required schema. Regenerate the ${schemaName} result.\n\nReturn only a JSON object matching the output contract below. Do not include analysis, explanation, apologies, markdown, or any text before or after the JSON object.`;
|
|
186
|
+
if (!outputContract)
|
|
187
|
+
return instructions;
|
|
188
|
+
return `${instructions}\n\n${outputContract}`;
|
|
189
|
+
}
|