opencode-magi 0.0.0-dev-20260519011027
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 +161 -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 +580 -0
- package/dist/config/worktree.js +13 -0
- package/dist/github/commands.js +398 -0
- package/dist/github/retry.js +44 -0
- package/dist/index.js +540 -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 +791 -0
- package/dist/orchestrator/run-manager.js +1670 -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 +67 -0
- package/schema.json +206 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { repairPrompt } from "../prompts/contracts";
|
|
2
|
+
import { throwIfAborted } from "./abort";
|
|
3
|
+
const OPENCODE_PERMISSION_NAMES = [
|
|
4
|
+
"read",
|
|
5
|
+
"edit",
|
|
6
|
+
"glob",
|
|
7
|
+
"grep",
|
|
8
|
+
"list",
|
|
9
|
+
"bash",
|
|
10
|
+
"task",
|
|
11
|
+
"external_directory",
|
|
12
|
+
"todowrite",
|
|
13
|
+
"question",
|
|
14
|
+
"webfetch",
|
|
15
|
+
"websearch",
|
|
16
|
+
"repo_clone",
|
|
17
|
+
"repo_overview",
|
|
18
|
+
"lsp",
|
|
19
|
+
"doom_loop",
|
|
20
|
+
"skill",
|
|
21
|
+
];
|
|
22
|
+
function formatError(value) {
|
|
23
|
+
if (value instanceof Error)
|
|
24
|
+
return value.message;
|
|
25
|
+
if (typeof value === "string")
|
|
26
|
+
return value;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.stringify(value);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return String(value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function toOpenCodePermissionRules(permission) {
|
|
35
|
+
if (!permission)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (typeof permission === "string") {
|
|
38
|
+
return OPENCODE_PERMISSION_NAMES.map((name) => ({
|
|
39
|
+
action: permission,
|
|
40
|
+
pattern: "*",
|
|
41
|
+
permission: name,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
const rules = [];
|
|
45
|
+
for (const [name, value] of Object.entries(permission)) {
|
|
46
|
+
if (typeof value === "string") {
|
|
47
|
+
rules.push({ action: value, pattern: "*", permission: name });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const [pattern, action] of Object.entries(value)) {
|
|
51
|
+
rules.push({ action, pattern, permission: name });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return rules;
|
|
55
|
+
}
|
|
56
|
+
function modelBody(model) {
|
|
57
|
+
const [providerID, ...modelParts] = model.split("/");
|
|
58
|
+
if (!providerID || !modelParts.length)
|
|
59
|
+
return model;
|
|
60
|
+
return { modelID: modelParts.join("/"), providerID };
|
|
61
|
+
}
|
|
62
|
+
function extractSessionId(result) {
|
|
63
|
+
const data = result;
|
|
64
|
+
if (data.error) {
|
|
65
|
+
const status = data.response?.status
|
|
66
|
+
? ` (${data.response.status}${data.response.statusText ? ` ${data.response.statusText}` : ""})`
|
|
67
|
+
: "";
|
|
68
|
+
throw new Error(`OpenCode session.create failed${status}: ${formatError(data.error)}`);
|
|
69
|
+
}
|
|
70
|
+
const id = data.data?.id ?? data.id;
|
|
71
|
+
if (!id)
|
|
72
|
+
throw new Error("OpenCode session.create did not return a session id");
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
function extractText(result, allowEmpty = false) {
|
|
76
|
+
const data = result;
|
|
77
|
+
const parts = data.data?.parts ?? data.parts;
|
|
78
|
+
const text = data.data?.info?.text ??
|
|
79
|
+
data.info?.text ??
|
|
80
|
+
parts
|
|
81
|
+
?.filter((part) => part.type === "text")
|
|
82
|
+
.map((part) => part.text)
|
|
83
|
+
.filter((value) => value != null)
|
|
84
|
+
.join("\n");
|
|
85
|
+
if (text == null || (!allowEmpty && !text))
|
|
86
|
+
throw new Error("OpenCode session.prompt did not return text output");
|
|
87
|
+
return text;
|
|
88
|
+
}
|
|
89
|
+
export async function createModelSession(input) {
|
|
90
|
+
return extractSessionId(await input.client.session.create({
|
|
91
|
+
body: {
|
|
92
|
+
permission: toOpenCodePermissionRules(input.permission),
|
|
93
|
+
title: input.title,
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
export async function promptModelText(input) {
|
|
98
|
+
throwIfAborted(input.signal);
|
|
99
|
+
const result = await input.client.session.prompt({
|
|
100
|
+
body: {
|
|
101
|
+
model: modelBody(input.model),
|
|
102
|
+
parts: [{ type: "text", text: input.prompt }],
|
|
103
|
+
},
|
|
104
|
+
path: { id: input.sessionId },
|
|
105
|
+
});
|
|
106
|
+
throwIfAborted(input.signal);
|
|
107
|
+
return extractText(result, input.allowEmpty);
|
|
108
|
+
}
|
|
109
|
+
async function sendPrompt(client, sessionId, model, prompt, signal) {
|
|
110
|
+
return promptModelText({ client, model, prompt, sessionId, signal });
|
|
111
|
+
}
|
|
112
|
+
export async function runModelText(input) {
|
|
113
|
+
throwIfAborted(input.signal);
|
|
114
|
+
const sessionId = await createModelSession({
|
|
115
|
+
client: input.client,
|
|
116
|
+
permission: input.permission,
|
|
117
|
+
title: input.title,
|
|
118
|
+
});
|
|
119
|
+
await input.onProgress?.({
|
|
120
|
+
options: input.options,
|
|
121
|
+
runAttempt: 1,
|
|
122
|
+
sessionId,
|
|
123
|
+
type: "session_created",
|
|
124
|
+
});
|
|
125
|
+
if (input.initialPrompt?.trim()) {
|
|
126
|
+
await promptModelText({
|
|
127
|
+
client: input.client,
|
|
128
|
+
model: input.model,
|
|
129
|
+
prompt: input.initialPrompt,
|
|
130
|
+
sessionId,
|
|
131
|
+
signal: input.signal,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const raw = await promptModelText({
|
|
135
|
+
allowEmpty: input.allowEmpty,
|
|
136
|
+
client: input.client,
|
|
137
|
+
model: input.model,
|
|
138
|
+
prompt: input.prompt,
|
|
139
|
+
sessionId,
|
|
140
|
+
signal: input.signal,
|
|
141
|
+
});
|
|
142
|
+
await input.onProgress?.({ raw, runAttempt: 1, sessionId, type: "response" });
|
|
143
|
+
return { raw, sessionId };
|
|
144
|
+
}
|
|
145
|
+
export async function runModelWithRepair(input) {
|
|
146
|
+
throwIfAborted(input.signal);
|
|
147
|
+
const runAttempts = Math.max(1, Math.floor(input.runAttempts ?? 2));
|
|
148
|
+
let lastError;
|
|
149
|
+
for (let runAttempt = 1; runAttempt <= runAttempts; runAttempt += 1) {
|
|
150
|
+
throwIfAborted(input.signal);
|
|
151
|
+
const sessionId = runAttempt === 1 && input.sessionId
|
|
152
|
+
? input.sessionId
|
|
153
|
+
: extractSessionId(await input.client.session.create({
|
|
154
|
+
body: {
|
|
155
|
+
permission: toOpenCodePermissionRules(input.permission),
|
|
156
|
+
title: input.title,
|
|
157
|
+
},
|
|
158
|
+
}));
|
|
159
|
+
await input.onProgress?.({
|
|
160
|
+
options: input.options,
|
|
161
|
+
runAttempt,
|
|
162
|
+
sessionId,
|
|
163
|
+
type: "session_created",
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
let raw = await sendPrompt(input.client, sessionId, input.model, input.prompt, input.signal);
|
|
167
|
+
await input.onProgress?.({ raw, runAttempt, sessionId, type: "response" });
|
|
168
|
+
for (let attempt = 0; attempt <= input.repairAttempts; attempt += 1) {
|
|
169
|
+
throwIfAborted(input.signal);
|
|
170
|
+
try {
|
|
171
|
+
return { raw, sessionId, value: input.parse(raw) };
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
lastError = error;
|
|
175
|
+
if (attempt === input.repairAttempts)
|
|
176
|
+
throw error;
|
|
177
|
+
await input.onProgress?.({
|
|
178
|
+
attempt: attempt + 1,
|
|
179
|
+
runAttempt,
|
|
180
|
+
sessionId,
|
|
181
|
+
type: "repair",
|
|
182
|
+
});
|
|
183
|
+
raw = await sendPrompt(input.client, sessionId, input.model, repairPrompt(input.schemaName), input.signal);
|
|
184
|
+
await input.onProgress?.({
|
|
185
|
+
raw,
|
|
186
|
+
runAttempt,
|
|
187
|
+
sessionId,
|
|
188
|
+
type: "response",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
lastError = error;
|
|
195
|
+
if (runAttempt === runAttempts)
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
throw lastError instanceof Error
|
|
200
|
+
? lastError
|
|
201
|
+
: new Error("unreachable model retry state");
|
|
202
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export async function mapPool(items, limit, worker, options = {}) {
|
|
2
|
+
const concurrency = Math.max(1, Math.floor(limit));
|
|
3
|
+
const results = Array.from({ length: items.length });
|
|
4
|
+
let nextIndex = 0;
|
|
5
|
+
async function runWorker() {
|
|
6
|
+
while (nextIndex < items.length) {
|
|
7
|
+
options.signal?.throwIfAborted();
|
|
8
|
+
const index = nextIndex;
|
|
9
|
+
nextIndex += 1;
|
|
10
|
+
results[index] = await worker(items[index], index);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => runWorker()));
|
|
14
|
+
return results;
|
|
15
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { formatSafetyGateReport } from "./safety";
|
|
2
|
+
function reportUrl(value) {
|
|
3
|
+
const trimmed = value?.trim();
|
|
4
|
+
if (!trimmed || trimmed.startsWith("skipped:"))
|
|
5
|
+
return undefined;
|
|
6
|
+
if (!/^https?:\/\//.test(trimmed))
|
|
7
|
+
return undefined;
|
|
8
|
+
return trimmed;
|
|
9
|
+
}
|
|
10
|
+
function linkOrText(text, url) {
|
|
11
|
+
return url ? `[${text}](${url})` : text;
|
|
12
|
+
}
|
|
13
|
+
function formatFinding(finding) {
|
|
14
|
+
const line = finding.startLine == null
|
|
15
|
+
? `${finding.path}:${finding.line}`
|
|
16
|
+
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
17
|
+
return `\`${line}\`: ${finding.issue}`;
|
|
18
|
+
}
|
|
19
|
+
function formatRereviewFinding(finding) {
|
|
20
|
+
const line = finding.startLine == null
|
|
21
|
+
? `${finding.path}:${finding.line}`
|
|
22
|
+
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
23
|
+
return `\`${line}\`: ${finding.body}`;
|
|
24
|
+
}
|
|
25
|
+
function isReviewOutput(output) {
|
|
26
|
+
return "findings" in output;
|
|
27
|
+
}
|
|
28
|
+
function discardedByReviewer(discarded) {
|
|
29
|
+
const grouped = {};
|
|
30
|
+
for (const item of discarded ?? []) {
|
|
31
|
+
grouped[item.reviewer] = [...(grouped[item.reviewer] ?? []), item.finding];
|
|
32
|
+
}
|
|
33
|
+
return grouped;
|
|
34
|
+
}
|
|
35
|
+
function checkLines(reports) {
|
|
36
|
+
const failures = reports.flatMap((report) => [
|
|
37
|
+
...report.scopeInside.map((item) => ({
|
|
38
|
+
name: item.check.name,
|
|
39
|
+
reason: item.reason,
|
|
40
|
+
})),
|
|
41
|
+
...report.scopeOutsideUnresolved.map((item) => ({
|
|
42
|
+
name: item.check.name,
|
|
43
|
+
reason: item.reason,
|
|
44
|
+
})),
|
|
45
|
+
...(report.dryRunRerun ?? []).map((item) => ({
|
|
46
|
+
name: item.check.name,
|
|
47
|
+
reason: `Dry run would rerun scope-out job: ${item.reason}`,
|
|
48
|
+
})),
|
|
49
|
+
]);
|
|
50
|
+
if (!failures.length)
|
|
51
|
+
return ["- **Check**: Pass"];
|
|
52
|
+
return [
|
|
53
|
+
"- **Check**: Failure",
|
|
54
|
+
...failures.map((failure) => ` - **${failure.name}**: ${failure.reason}`),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
function dryRunLines(dryRun) {
|
|
58
|
+
return dryRun ? ["- **Dry run**: GitHub write operations were skipped"] : [];
|
|
59
|
+
}
|
|
60
|
+
function safetyLines(safety) {
|
|
61
|
+
return safety ? formatSafetyGateReport(safety).split("\n") : [];
|
|
62
|
+
}
|
|
63
|
+
function reviewerStatus(output, url) {
|
|
64
|
+
if (output.verdict === "MERGE")
|
|
65
|
+
return "Approved";
|
|
66
|
+
if (output.verdict === "CLOSE")
|
|
67
|
+
return linkOrText("Closed", url);
|
|
68
|
+
return linkOrText("Changes requested", url);
|
|
69
|
+
}
|
|
70
|
+
function reviewerDetailLines(output) {
|
|
71
|
+
if (isReviewOutput(output)) {
|
|
72
|
+
if (output.verdict === "CLOSE")
|
|
73
|
+
return output.reason ? [output.reason] : [];
|
|
74
|
+
if (output.verdict !== "CHANGES_REQUESTED")
|
|
75
|
+
return [];
|
|
76
|
+
return output.findings.map(formatFinding);
|
|
77
|
+
}
|
|
78
|
+
if (output.verdict === "CLOSE")
|
|
79
|
+
return output.reason ? [output.reason] : [];
|
|
80
|
+
if (output.verdict !== "CHANGES_REQUESTED")
|
|
81
|
+
return [];
|
|
82
|
+
return [
|
|
83
|
+
...output.newFindings.map(formatRereviewFinding),
|
|
84
|
+
...output.followUps.map((item) => `Comment #${item.commentId}: ${item.body}`),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
function reviewerLines(input) {
|
|
88
|
+
const discarded = discardedByReviewer(input.discardedFindings);
|
|
89
|
+
const lines = ["- **Reviewer**:"];
|
|
90
|
+
for (const reviewer of input.repository.agents.reviewers) {
|
|
91
|
+
const output = input.outputs[reviewer.key];
|
|
92
|
+
const url = reportUrl(input.posted[reviewer.key]);
|
|
93
|
+
if (!output) {
|
|
94
|
+
lines.push(` - **${reviewer.key}**: Skipped`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
lines.push(` - **${reviewer.key}**: ${reviewerStatus(output, url)}`);
|
|
98
|
+
for (const detail of reviewerDetailLines(output)) {
|
|
99
|
+
lines.push(` - ${detail}`);
|
|
100
|
+
}
|
|
101
|
+
for (const finding of discarded[reviewer.key] ?? []) {
|
|
102
|
+
lines.push(` - ~~${formatFinding(finding)}~~`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
function mergeStatusLines(status) {
|
|
108
|
+
switch (status) {
|
|
109
|
+
case "merged":
|
|
110
|
+
return ["- **Status**: Merged"];
|
|
111
|
+
case "closed":
|
|
112
|
+
return ["- **Status**: Closed"];
|
|
113
|
+
case "approved":
|
|
114
|
+
return ["- **Status**: Approved"];
|
|
115
|
+
case "close_requested":
|
|
116
|
+
return ["- **Status**: Close requested"];
|
|
117
|
+
case "dequeued":
|
|
118
|
+
return ["- **Status**: Failed", " - Removed from GitHub merge queue."];
|
|
119
|
+
case "ci_unresolved":
|
|
120
|
+
return ["- **Status**: Failed", " - CI remained unresolved."];
|
|
121
|
+
case "safety_blocked":
|
|
122
|
+
return ["- **Status**: Safety blocked"];
|
|
123
|
+
case "changes_unresolved":
|
|
124
|
+
return [
|
|
125
|
+
"- **Status**: Failed",
|
|
126
|
+
" - Changes remained unresolved after the per-thread resolution attempt limit.",
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function editorLines(outputs) {
|
|
131
|
+
if (!outputs?.length)
|
|
132
|
+
return [];
|
|
133
|
+
return [
|
|
134
|
+
"- **Editor**:",
|
|
135
|
+
...outputs.flatMap((output, index) => {
|
|
136
|
+
const label = ` - Cycle ${index + 1}`;
|
|
137
|
+
if (output.mode === "REPLIED") {
|
|
138
|
+
return [
|
|
139
|
+
`${label}: replied without code changes`,
|
|
140
|
+
...output.responses.map((response) => ` - ${response.action} comment #${response.commentId}: ${response.body}`),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
return [
|
|
144
|
+
`${label}: ${output.commitMessage} (${output.commitSha?.slice(0, 7)})`,
|
|
145
|
+
...output.filesTouched.map((file) => ` - ${file}`),
|
|
146
|
+
...output.responses.map((response) => ` - ${response.action} comment #${response.commentId}: ${response.body}`),
|
|
147
|
+
];
|
|
148
|
+
}),
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
export function formatReviewReport(input) {
|
|
152
|
+
return [
|
|
153
|
+
...dryRunLines(input.dryRun),
|
|
154
|
+
...safetyLines(input.safety),
|
|
155
|
+
...checkLines(input.ciReports),
|
|
156
|
+
...reviewerLines(input),
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
|
159
|
+
export function formatMergeReport(input) {
|
|
160
|
+
return [
|
|
161
|
+
...mergeStatusLines(input.status),
|
|
162
|
+
...dryRunLines(input.dryRun),
|
|
163
|
+
...safetyLines(input.safety),
|
|
164
|
+
...checkLines(input.ciReports),
|
|
165
|
+
...reviewerLines(input),
|
|
166
|
+
...editorLines(input.editorOutputs),
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|