opencode-magi 0.10.0 → 0.11.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.ja.md +254 -0
- package/README.md +31 -26
- package/dist/commands.js +11 -9
- package/dist/config/index.js +2 -0
- package/dist/config/resolve.js +120 -246
- package/dist/config/validate.js +103 -1110
- package/dist/constant.js +135 -0
- package/dist/graphql/index.generated.js +125 -0
- package/dist/graphql/index.js +49 -0
- package/dist/index.js +12 -800
- package/dist/magi.js +357 -0
- package/dist/permissions/common.json +22 -22
- package/dist/permissions/editor.json +12 -12
- package/dist/prompts/index.js +98 -0
- package/dist/prompts/merge/ci-classification/output-contract.md +22 -0
- package/dist/prompts/merge/ci-classification/task.md +10 -0
- package/dist/prompts/merge/ci-classification/validate.json +20 -0
- package/dist/prompts/merge/conflict/output-contract.md +12 -0
- package/dist/prompts/merge/conflict/task.md +9 -0
- package/dist/prompts/merge/conflict/validate.json +4 -0
- package/dist/prompts/merge/edit/output-contract.md +26 -0
- package/dist/prompts/merge/edit/task.md +10 -0
- package/dist/prompts/merge/edit/validate.json +60 -0
- package/dist/prompts/review/ci-classification/output-contract.md +21 -0
- package/dist/prompts/review/ci-classification/task.md +7 -0
- package/dist/prompts/review/ci-classification/validate.json +20 -0
- package/dist/prompts/review/close-reconsideration/output-contract.md +21 -0
- package/dist/prompts/review/close-reconsideration/task.md +6 -0
- package/dist/prompts/review/close-reconsideration/validate.json +44 -0
- package/dist/prompts/review/comment/output-contract.md +11 -0
- package/dist/prompts/review/comment/task.md +3 -0
- package/dist/prompts/review/comment/validate.json +8 -0
- package/dist/prompts/review/finding-validation/output-contract.md +25 -0
- package/dist/prompts/{templates/review/finding-validation.md → review/finding-validation/task.md} +2 -3
- package/dist/prompts/review/finding-validation/validate.json +21 -0
- package/dist/prompts/review/rereview/output-contract.md +30 -0
- package/dist/prompts/{templates/review/rereview.md → review/rereview/task.md} +8 -11
- package/dist/prompts/review/rereview/validate.json +87 -0
- package/dist/prompts/review/review/output-contract.md +25 -0
- package/dist/prompts/{templates/review/review.md → review/review/task.md} +2 -5
- package/dist/prompts/review/review/validate.json +54 -0
- package/dist/prompts/triage/acceptance/output-contract.md +16 -0
- package/dist/prompts/triage/acceptance/validate.json +23 -0
- package/dist/prompts/triage/category/output-contract.md +16 -0
- package/dist/prompts/triage/category/validate.json +23 -0
- package/dist/prompts/triage/comment-classification/output-contract.md +15 -0
- package/dist/prompts/triage/comment-classification/validate.json +28 -0
- package/dist/prompts/triage/create/output-contract.md +28 -0
- package/dist/prompts/triage/create/validate.json +73 -0
- package/dist/prompts/triage/deplicate/output-contract.md +16 -0
- package/dist/prompts/triage/deplicate/validate.json +20 -0
- package/dist/prompts/triage/existing/output-contract.md +16 -0
- package/dist/prompts/triage/existing/validate.json +13 -0
- package/dist/prompts/triage/reconsider/output-contract.md +16 -0
- package/dist/prompts/triage/reconsider/validate.json +23 -0
- package/dist/prompts/triage/signal/output-contract.md +19 -0
- package/dist/prompts/triage/signal/validate.json +18 -0
- package/dist/tools/clear/index.js +21 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/merge/action.js +47 -0
- package/dist/tools/merge/context.js +118 -0
- package/dist/tools/merge/editor.js +264 -0
- package/dist/tools/merge/index.js +151 -0
- package/dist/tools/merge/index.type.js +1 -0
- package/dist/tools/merge/merge.js +42 -0
- package/dist/tools/merge/report.js +73 -0
- package/dist/tools/review/action.js +429 -0
- package/dist/tools/review/check.js +295 -0
- package/dist/tools/review/context.js +258 -0
- package/dist/tools/review/index.js +104 -0
- package/dist/tools/review/index.type.js +1 -0
- package/dist/tools/review/report.js +143 -0
- package/dist/tools/review/review.js +145 -0
- package/dist/tools/review/reviewer.js +409 -0
- package/dist/tools/triage/index.js +16 -0
- package/dist/tools/validate/index.js +28 -0
- package/dist/utils/array.js +6 -0
- package/dist/utils/assertion.js +38 -0
- package/dist/utils/exec.js +14 -0
- package/dist/utils/fs.js +24 -0
- package/dist/utils/function.js +37 -0
- package/dist/utils/github.js +63 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.type.js +1 -0
- package/dist/utils/object.js +21 -0
- package/dist/utils/opencode.js +25 -0
- package/dist/utils/string.js +38 -0
- package/dist/utils/worker.js +30 -0
- package/package.json +20 -8
- package/schema.json +6 -4
- package/dist/config/load.js +0 -62
- package/dist/config/output.js +0 -25
- package/dist/config/worktree.js +0 -25
- package/dist/github/commands.js +0 -905
- package/dist/github/retry.js +0 -44
- package/dist/orchestrator/abort.js +0 -9
- package/dist/orchestrator/ci.js +0 -579
- package/dist/orchestrator/findings.js +0 -88
- package/dist/orchestrator/inline-comments.js +0 -73
- package/dist/orchestrator/majority.js +0 -62
- package/dist/orchestrator/merge.js +0 -1260
- package/dist/orchestrator/model.js +0 -216
- package/dist/orchestrator/pool.js +0 -15
- package/dist/orchestrator/report.js +0 -175
- package/dist/orchestrator/review-context.js +0 -342
- package/dist/orchestrator/review.js +0 -1480
- package/dist/orchestrator/run-manager.js +0 -2132
- package/dist/orchestrator/safety.js +0 -44
- package/dist/orchestrator/triage.js +0 -1370
- package/dist/prompts/compose.js +0 -473
- package/dist/prompts/contracts.js +0 -300
- package/dist/prompts/output.js +0 -401
- package/dist/prompts/templates/merge/ci-classification.md +0 -16
- package/dist/prompts/templates/merge/conflict.md +0 -10
- package/dist/prompts/templates/merge/edit.md +0 -15
- package/dist/prompts/templates/review/ci-classification.md +0 -9
- package/dist/prompts/templates/review/close-reconsideration.md +0 -6
- /package/dist/{types.js → config/index.type.js} +0 -0
- /package/dist/prompts/{templates/triage/acceptance.md → triage/acceptance/task.md} +0 -0
- /package/dist/prompts/{templates/triage/category.md → triage/category/task.md} +0 -0
- /package/dist/prompts/{templates/triage/comment-classification.md → triage/comment-classification/task.md} +0 -0
- /package/dist/prompts/{templates/triage/create.md → triage/create/task.md} +0 -0
- /package/dist/prompts/{templates/triage/duplicate.md → triage/deplicate/task.md} +0 -0
- /package/dist/prompts/{templates/triage/existing-pr.md → triage/existing/task.md} +0 -0
- /package/dist/prompts/{templates/triage/reconsider.md → triage/reconsider/task.md} +0 -0
- /package/dist/prompts/{templates/triage/signal.md → triage/signal/task.md} +0 -0
package/dist/magi.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { createOpencodeClient } from "@opencode-ai/sdk/v2";
|
|
2
|
+
import { print } from "graphql";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { appendFile, mkdir, readdir, readFile, writeFile, } from "node:fs/promises";
|
|
5
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
6
|
+
import { Octokit } from "octokit";
|
|
7
|
+
import { getConfig, resolvePermissions, validateConfig } from "@/config";
|
|
8
|
+
import { graphql } from "@/graphql";
|
|
9
|
+
import { command, createExec, filterEmpty, isArray, isNumber, isObject, isUndefined, merge, quote, rm, } from "@/utils";
|
|
10
|
+
const active = new Set(["preparing", "running"]);
|
|
11
|
+
export class MagiError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
constructor(status, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.name = "MagiError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class Magi {
|
|
20
|
+
input;
|
|
21
|
+
options;
|
|
22
|
+
exec;
|
|
23
|
+
constructor(input, options) {
|
|
24
|
+
const client = createOpencodeClient({
|
|
25
|
+
baseUrl: input.serverUrl.toString(),
|
|
26
|
+
directory: input.directory,
|
|
27
|
+
fetch: input.client.session._client.getConfig().fetch,
|
|
28
|
+
});
|
|
29
|
+
this.input = { ...input, client };
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.exec = createExec(input.directory);
|
|
32
|
+
}
|
|
33
|
+
getGhToken(account, signal) {
|
|
34
|
+
return this.exec(command("gh", "auth", "token", account && "--user", account && quote(account)), { signal });
|
|
35
|
+
}
|
|
36
|
+
async createOctokit(config, signal, account) {
|
|
37
|
+
const auth = await this.getGhToken(account, signal);
|
|
38
|
+
const retries = config.github.retryApiAttempts;
|
|
39
|
+
return new Octokit({
|
|
40
|
+
auth,
|
|
41
|
+
request: { signal },
|
|
42
|
+
retry: { retries },
|
|
43
|
+
throttle: {
|
|
44
|
+
onRateLimit(_, options) {
|
|
45
|
+
if (options.request.retryCount < retries)
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
onSecondaryRateLimit(_, options) {
|
|
49
|
+
if (options.request.retryCount < retries)
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
createGraphql(octokit) {
|
|
56
|
+
return graphql((document, variables) => octokit.graphql(print(document), variables));
|
|
57
|
+
}
|
|
58
|
+
async clear(config) {
|
|
59
|
+
const summary = {
|
|
60
|
+
branch: 0,
|
|
61
|
+
output: 0,
|
|
62
|
+
run: 0,
|
|
63
|
+
session: 0,
|
|
64
|
+
skipped: 0,
|
|
65
|
+
worktree: 0,
|
|
66
|
+
};
|
|
67
|
+
const states = await this.getStates(config);
|
|
68
|
+
for (const state of states) {
|
|
69
|
+
if (active.has(state.status)) {
|
|
70
|
+
summary.skipped += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (config.clear.session)
|
|
74
|
+
summary.session += await this.deleteSessions(state);
|
|
75
|
+
if (config.clear.worktree && state.worktree?.path)
|
|
76
|
+
summary.worktree += await this.deleteWorktree(state.worktree.path);
|
|
77
|
+
if (config.clear.branch && state.worktree?.branch)
|
|
78
|
+
summary.branch += await this.deleteBranch(state.worktree.branch);
|
|
79
|
+
if (config.clear.output && state.output)
|
|
80
|
+
summary.output += await this.deleteOutput(state.output);
|
|
81
|
+
summary.run += 1;
|
|
82
|
+
}
|
|
83
|
+
return summary;
|
|
84
|
+
}
|
|
85
|
+
getPath(value) {
|
|
86
|
+
return isAbsolute(value) ? value : join(this.input.directory, value);
|
|
87
|
+
}
|
|
88
|
+
async getConfig(require) {
|
|
89
|
+
const config = await getConfig(this.input);
|
|
90
|
+
const errors = await validateConfig(config, { exec: this.exec, require });
|
|
91
|
+
if (errors.length)
|
|
92
|
+
throw new Error(errors.join("\n"));
|
|
93
|
+
return config;
|
|
94
|
+
}
|
|
95
|
+
async getStates(config) {
|
|
96
|
+
const files = await Promise.all([
|
|
97
|
+
this.getStateFiles(config.review.output),
|
|
98
|
+
this.getStateFiles(config.triage.output),
|
|
99
|
+
]);
|
|
100
|
+
return Promise.all(files
|
|
101
|
+
.flat()
|
|
102
|
+
.map(async (file) => JSON.parse(await readFile(file, "utf8"))));
|
|
103
|
+
}
|
|
104
|
+
async getState(dir) {
|
|
105
|
+
return JSON.parse(await readFile(join(dir, "state.json"), "utf8"));
|
|
106
|
+
}
|
|
107
|
+
async getStateFiles(dir) {
|
|
108
|
+
try {
|
|
109
|
+
const entries = await readdir(this.getPath(dir), {
|
|
110
|
+
recursive: true,
|
|
111
|
+
withFileTypes: true,
|
|
112
|
+
});
|
|
113
|
+
return entries
|
|
114
|
+
.filter((entry) => entry.isFile() && entry.name === "state.json")
|
|
115
|
+
.map((entry) => join(entry.parentPath, entry.name));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
getAgents(state) {
|
|
122
|
+
return filterEmpty([
|
|
123
|
+
state.editor,
|
|
124
|
+
state.creator,
|
|
125
|
+
state.operator,
|
|
126
|
+
...Object.values(state.reviewers ?? {}),
|
|
127
|
+
...Object.values(state.voters ?? {}),
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
async createState(dir, initialState) {
|
|
131
|
+
const id = `run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
132
|
+
const createdAt = new Date().toISOString();
|
|
133
|
+
const state = {
|
|
134
|
+
createdAt,
|
|
135
|
+
id,
|
|
136
|
+
output: join(this.getPath(dir), id),
|
|
137
|
+
status: "preparing",
|
|
138
|
+
updatedAt: createdAt,
|
|
139
|
+
...initialState,
|
|
140
|
+
};
|
|
141
|
+
await this.createStateFile(state);
|
|
142
|
+
return state;
|
|
143
|
+
}
|
|
144
|
+
async createStateFile(state) {
|
|
145
|
+
await mkdir(state.output, { recursive: true });
|
|
146
|
+
await writeFile(join(state.output, "state.json"), `${JSON.stringify(state, null, 2)}\n`);
|
|
147
|
+
}
|
|
148
|
+
async createAgentFile(output, phase, id, content, attempt = 1, cycle) {
|
|
149
|
+
const segments = [id, phase];
|
|
150
|
+
if (isNumber(cycle))
|
|
151
|
+
segments.push(cycle.toString());
|
|
152
|
+
if (isNumber(attempt))
|
|
153
|
+
segments.push(attempt.toString());
|
|
154
|
+
const path = join(output, segments.join("-") + ".md");
|
|
155
|
+
await writeFile(path, content);
|
|
156
|
+
}
|
|
157
|
+
async updateState(dir, next) {
|
|
158
|
+
next.updatedAt = new Date().toISOString();
|
|
159
|
+
const prev = await this.getState(dir);
|
|
160
|
+
const state = merge(prev, next);
|
|
161
|
+
await this.createStateFile(state);
|
|
162
|
+
return state;
|
|
163
|
+
}
|
|
164
|
+
async updateEvent(output, message) {
|
|
165
|
+
const event = {
|
|
166
|
+
createdAt: new Date().toISOString(),
|
|
167
|
+
message,
|
|
168
|
+
};
|
|
169
|
+
await appendFile(join(output, "events.jsonl"), `${JSON.stringify(event)}\n`);
|
|
170
|
+
}
|
|
171
|
+
async getEvents(output) {
|
|
172
|
+
try {
|
|
173
|
+
const content = await readFile(join(output, "events.jsonl"), "utf8");
|
|
174
|
+
return content
|
|
175
|
+
.split("\n")
|
|
176
|
+
.filter(Boolean)
|
|
177
|
+
.map((line) => JSON.parse(line));
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
if (e instanceof Error && "code" in e && e.code === "ENOENT")
|
|
181
|
+
return [];
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
getSessionIds(state) {
|
|
186
|
+
return [
|
|
187
|
+
state.sessionId,
|
|
188
|
+
...filterEmpty(this.getAgents(state).map(({ sessionId }) => sessionId)),
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
async createSession(parentID, title, { model, permissions, }, signal) {
|
|
192
|
+
if (isArray(model) || !isObject(model))
|
|
193
|
+
throw new Error();
|
|
194
|
+
const { id, variant } = model;
|
|
195
|
+
const [providerId, modelId] = id.split("/");
|
|
196
|
+
const result = await this.input.client.session.create({
|
|
197
|
+
model: {
|
|
198
|
+
id: modelId,
|
|
199
|
+
providerID: providerId,
|
|
200
|
+
variant,
|
|
201
|
+
},
|
|
202
|
+
parentID,
|
|
203
|
+
permission: resolvePermissions(permissions),
|
|
204
|
+
title,
|
|
205
|
+
}, { signal });
|
|
206
|
+
if (result.error) {
|
|
207
|
+
if (!isUndefined(result.response) && result.response.statusText)
|
|
208
|
+
throw new Error(result.response.statusText);
|
|
209
|
+
else if (result.error instanceof Error)
|
|
210
|
+
throw new Error(result.error.message);
|
|
211
|
+
else
|
|
212
|
+
throw new Error(JSON.stringify(result.error));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const id = result.data.id;
|
|
216
|
+
return id;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async promptSession(sessionID, text, signal) {
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
const events = await this.input.client.event.subscribe(undefined, {
|
|
222
|
+
signal: controller.signal,
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
const result = await Promise.race([
|
|
226
|
+
this.input.client.session.prompt({
|
|
227
|
+
parts: [{ text, type: "text" }],
|
|
228
|
+
sessionID,
|
|
229
|
+
}, { signal }),
|
|
230
|
+
this.monitorSession(sessionID, events.stream),
|
|
231
|
+
]);
|
|
232
|
+
if (result.error) {
|
|
233
|
+
if (!isUndefined(result.response) && result.response.statusText)
|
|
234
|
+
throw new Error(result.response.statusText);
|
|
235
|
+
else if (result.error instanceof Error)
|
|
236
|
+
throw new Error(result.error.message);
|
|
237
|
+
else
|
|
238
|
+
throw new Error(JSON.stringify(result.error));
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const output = result.data.parts
|
|
242
|
+
.filter((part) => part.type === "text")
|
|
243
|
+
.map((part) => part.text)
|
|
244
|
+
.join("\n");
|
|
245
|
+
if (!output)
|
|
246
|
+
throw new Error("OpenCode session.prompt did not return text output.");
|
|
247
|
+
return output;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
if (e instanceof MagiError)
|
|
252
|
+
await this.input.client.session.abort({ sessionID });
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
controller.abort();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async monitorSession(sessionID, events) {
|
|
260
|
+
for await (const ev of events) {
|
|
261
|
+
if (ev.type === "permission.asked" &&
|
|
262
|
+
ev.properties.sessionID === sessionID) {
|
|
263
|
+
const { id, patterns, permission } = ev.properties;
|
|
264
|
+
const result = await this.input.client.permission.reply({
|
|
265
|
+
reply: "reject",
|
|
266
|
+
requestID: id,
|
|
267
|
+
});
|
|
268
|
+
if (result.error)
|
|
269
|
+
throw new MagiError("blocked", "Could not reject permission request.");
|
|
270
|
+
throw new MagiError("blocked", `OpenCode session requested ${permission} permission for ${patterns.join(", ")}.`);
|
|
271
|
+
}
|
|
272
|
+
if (ev.type === "question.asked" &&
|
|
273
|
+
ev.properties.sessionID === sessionID) {
|
|
274
|
+
const result = await this.input.client.question.reject({
|
|
275
|
+
requestID: ev.properties.id,
|
|
276
|
+
});
|
|
277
|
+
if (result.error)
|
|
278
|
+
throw new MagiError("blocked", "Could not reject user question.");
|
|
279
|
+
throw new MagiError("blocked", "OpenCode session requested user input.");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return new Promise(() => { });
|
|
283
|
+
}
|
|
284
|
+
async deleteSessions(state) {
|
|
285
|
+
const sessionIds = this.getSessionIds(state);
|
|
286
|
+
let count = 0;
|
|
287
|
+
for (const sessionID of sessionIds)
|
|
288
|
+
try {
|
|
289
|
+
await this.input.client.session.delete({ sessionID });
|
|
290
|
+
count += 1;
|
|
291
|
+
}
|
|
292
|
+
catch { }
|
|
293
|
+
return count;
|
|
294
|
+
}
|
|
295
|
+
async createWorktree(dir, number, id, signal) {
|
|
296
|
+
const path = this.getPath(join(dir, number.toString(), id));
|
|
297
|
+
try {
|
|
298
|
+
await mkdir(dirname(path), { recursive: true });
|
|
299
|
+
await this.exec(command("git", "worktree", "add", "--detach", quote(path)), { signal });
|
|
300
|
+
await this.exec(command("gh", "pr", "checkout", number, "--detach"), {
|
|
301
|
+
cwd: path,
|
|
302
|
+
signal,
|
|
303
|
+
});
|
|
304
|
+
const branch = await this.exec("git branch --show-current", {
|
|
305
|
+
cwd: path,
|
|
306
|
+
signal,
|
|
307
|
+
});
|
|
308
|
+
return { branch: branch || undefined, path };
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
try {
|
|
312
|
+
await this.exec(command("git", "worktree", "remove", "--force", quote(path)));
|
|
313
|
+
await this.exec(command("git", "worktree", "prune"));
|
|
314
|
+
}
|
|
315
|
+
catch { }
|
|
316
|
+
throw e;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async deleteWorktree(value) {
|
|
320
|
+
try {
|
|
321
|
+
const path = this.getPath(value);
|
|
322
|
+
await this.exec(command("git", "worktree", "remove", "--force", quote(path)));
|
|
323
|
+
await rm(path, {
|
|
324
|
+
force: true,
|
|
325
|
+
prune: this.input.directory,
|
|
326
|
+
recursive: true,
|
|
327
|
+
});
|
|
328
|
+
return 1;
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async deleteBranch(branch) {
|
|
335
|
+
try {
|
|
336
|
+
await this.exec(command("git", "branch", "-D", quote(branch)));
|
|
337
|
+
return 1;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async deleteOutput(path) {
|
|
344
|
+
try {
|
|
345
|
+
const resolvedPath = this.getPath(path);
|
|
346
|
+
await rm(resolvedPath, {
|
|
347
|
+
force: true,
|
|
348
|
+
prune: this.input.directory,
|
|
349
|
+
recursive: true,
|
|
350
|
+
});
|
|
351
|
+
return 1;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
24
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
"bash": {
|
|
3
|
+
"bun *": "allow",
|
|
4
|
+
"bunx *": "allow",
|
|
5
|
+
"corepack *": "allow",
|
|
6
|
+
"git add*": "allow",
|
|
7
|
+
"git commit*": "allow",
|
|
8
|
+
"npm *": "allow",
|
|
9
|
+
"npx *": "allow",
|
|
10
|
+
"pnpm *": "allow",
|
|
11
|
+
"yarn *": "allow"
|
|
12
|
+
},
|
|
13
|
+
"edit": "allow"
|
|
14
14
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { isString, isUndefined } from "@/utils";
|
|
5
|
+
export class Prompt {
|
|
6
|
+
magi;
|
|
7
|
+
config;
|
|
8
|
+
pathname;
|
|
9
|
+
validateSchema;
|
|
10
|
+
constructor(magi, config, pathname, validateSchema) {
|
|
11
|
+
this.magi = magi;
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.pathname = pathname;
|
|
14
|
+
this.validateSchema = validateSchema;
|
|
15
|
+
}
|
|
16
|
+
static async init(magi, config, dir) {
|
|
17
|
+
const url = new URL(dir, import.meta.url);
|
|
18
|
+
let validateSchema;
|
|
19
|
+
try {
|
|
20
|
+
validateSchema = JSON.parse(await readFile(join(url.pathname, "validate.json"), "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
return new Prompt(magi, config, url.pathname, validateSchema);
|
|
24
|
+
}
|
|
25
|
+
async create(taskPath, tags, values) {
|
|
26
|
+
const [task, ...rest] = await Promise.all([
|
|
27
|
+
readFile(taskPath ? this.magi.getPath(taskPath) : join(this.pathname, "task.md"), "utf-8"),
|
|
28
|
+
...tags.map(async (tag) => {
|
|
29
|
+
if (isString(tag)) {
|
|
30
|
+
const path = join(this.pathname, `${tag.replaceAll("_", "-")}.md`);
|
|
31
|
+
const content = await readFile(path, "utf-8");
|
|
32
|
+
return { content, tag };
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
return { content: tag[1], tag: tag[0] };
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
]);
|
|
39
|
+
const content = [
|
|
40
|
+
`<task>\n${task}\n</task>`,
|
|
41
|
+
`<language>\n${this.config.language}\n</language>`,
|
|
42
|
+
...rest.map(({ content, tag }) => `<${tag}>\n${content}\n</${tag}>`),
|
|
43
|
+
].join("\n\n");
|
|
44
|
+
return Object.entries(values).reduce((prev, [key, value]) => prev.replaceAll(`{${key}}`, value), content);
|
|
45
|
+
}
|
|
46
|
+
async repair(e) {
|
|
47
|
+
const instructions = [
|
|
48
|
+
"Your previous output failed validation. Regenerate the result.",
|
|
49
|
+
!isUndefined(e)
|
|
50
|
+
? `Validation error: ${e instanceof Error ? e.message : "Invalid output."}`
|
|
51
|
+
: undefined,
|
|
52
|
+
"Return 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.",
|
|
53
|
+
]
|
|
54
|
+
.filter((instruction) => !isUndefined(instruction))
|
|
55
|
+
.join("\n\n");
|
|
56
|
+
try {
|
|
57
|
+
const outputContract = await readFile(join(this.pathname, "output-contract.md"), "utf-8");
|
|
58
|
+
return [
|
|
59
|
+
instructions,
|
|
60
|
+
`<output_contract>\n${outputContract}\n</output_contract>`,
|
|
61
|
+
].join("\n\n");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return instructions;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
parse(content) {
|
|
68
|
+
const trimmed = content.trim();
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(trimmed);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
const candidates = [];
|
|
74
|
+
for (const match of trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) {
|
|
75
|
+
const candidate = match.at(1)?.trim();
|
|
76
|
+
if (candidate)
|
|
77
|
+
candidates.unshift(candidate);
|
|
78
|
+
}
|
|
79
|
+
for (const match of trimmed.matchAll(/\{[\s\S]*?\}/g)) {
|
|
80
|
+
const candidate = match.at(0);
|
|
81
|
+
if (candidate)
|
|
82
|
+
candidates.unshift(candidate);
|
|
83
|
+
}
|
|
84
|
+
for (const candidate of candidates)
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(candidate);
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
}
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
validate(content) {
|
|
93
|
+
if (!this.validateSchema)
|
|
94
|
+
return true;
|
|
95
|
+
const validate = new Ajv({ allErrors: true }).compile(this.validateSchema);
|
|
96
|
+
return Boolean(validate(content));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
2
|
+
|
|
3
|
+
```json
|
|
4
|
+
{
|
|
5
|
+
"checks": [
|
|
6
|
+
{
|
|
7
|
+
"id": "exact failed check ID",
|
|
8
|
+
"classification": "SCOPE_IN" | "SCOPE_OUT",
|
|
9
|
+
"comment": "Short reason."
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Rules:
|
|
16
|
+
|
|
17
|
+
- Return one item for every failed check.
|
|
18
|
+
- `"id"` is the ID of the failed check.
|
|
19
|
+
- `"SCOPE_IN"` means the failure should be treated as caused by the PR changes or the editor changes and passed to reviewers/editor.
|
|
20
|
+
- `"SCOPE_OUT"` means the failure is likely flaky, external, or infrastructure-related and may be rerun.
|
|
21
|
+
- `"comment"` is a short reason for the classification.
|
|
22
|
+
- If uncertain, choose `"SCOPE_IN"`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Classify failed CI jobs after editor changes for pull request #{pr} in {owner}/{repo}.
|
|
2
|
+
Decide whether each failure is caused by the PR changes or the editor changes, or is likely flaky, external, or infrastructure-related.
|
|
3
|
+
The PR worktree is {worktreePath}.
|
|
4
|
+
Review only the diff from {baseSha} to {headSha}.
|
|
5
|
+
Use: git -C "{worktreePath}" diff {baseSha}...{headSha}
|
|
6
|
+
|
|
7
|
+
Treat failures that appeared after the editor changes as `"SCOPE_IN"` unless there is strong evidence they are unrelated.
|
|
8
|
+
If uncertain, choose `"SCOPE_IN"`.
|
|
9
|
+
|
|
10
|
+
Failed checks with structured failure evidence are provided in `<failed_checks>`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"required": ["checks"],
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"checks": {
|
|
7
|
+
"type": "array",
|
|
8
|
+
"items": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"required": ["id", "classification", "comment"],
|
|
11
|
+
"additionalProperties": false,
|
|
12
|
+
"properties": {
|
|
13
|
+
"id": { "type": "string", "minLength": 1 },
|
|
14
|
+
"classification": { "enum": ["SCOPE_IN", "SCOPE_OUT"] },
|
|
15
|
+
"comment": { "type": "string", "minLength": 1 }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
2
|
+
|
|
3
|
+
Resolve the conflicts, stage the resolved files, create the merge commit, then return this exact JSON object:
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{}
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Rules:
|
|
10
|
+
|
|
11
|
+
- Do not push. The orchestrator pushes after validating this envelope.
|
|
12
|
+
- Do not include commit metadata, touched files, responses, explanation, or markdown.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Fix pull request #{pr} for {owner}/{repo}.
|
|
2
|
+
The PR worktree is {worktreePath}.
|
|
3
|
+
|
|
4
|
+
Act as the PR author and resolve the merge conflicts provided in `<conflicted_files>`.
|
|
5
|
+
|
|
6
|
+
The worktree is already in the middle of a no-commit merge and contains conflict markers.
|
|
7
|
+
Preserve the intended PR behavior and the already-merged base branch changes.
|
|
8
|
+
Resolve every merge conflict in the worktree, stage changes, and create the merge commit.
|
|
9
|
+
Do not push.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
2
|
+
|
|
3
|
+
```json
|
|
4
|
+
{
|
|
5
|
+
"mode": "EDITED" | "REPLIED",
|
|
6
|
+
"commitSha": "full sha, required only when mode is EDITED; omit when mode is REPLIED",
|
|
7
|
+
"commitMessage": "fix(scope): short description, required only when mode is EDITED; omit when mode is REPLIED",
|
|
8
|
+
"filesTouched": ["relative/path.ext"],
|
|
9
|
+
"responses": [{ "commentId": 123, "action": "FIXED" | "DISAGREE" | "ASK", "body": "Fixed." }]
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Rules:
|
|
14
|
+
|
|
15
|
+
- Use `"EDITED"` only when you edited files, staged changes, and committed.
|
|
16
|
+
- Use `"REPLIED"` when you only replied without code changes.
|
|
17
|
+
- `"FIXED"` means you agreed with the reviewer and made a code change.
|
|
18
|
+
- `"DISAGREE"` means you did not edit because the requested change is incorrect or unnecessary.
|
|
19
|
+
- `"ASK"` means you need clarification and did not edit.
|
|
20
|
+
- Do not make changes just because a reviewer requested them; edit only when you understand and agree.
|
|
21
|
+
- Do not push. The orchestrator pushes after validating this envelope.
|
|
22
|
+
- `filesTouched` is required for `"EDITED"` and must include every final changed file.
|
|
23
|
+
- `responses` is required and must include a reply for each thread you addressed.
|
|
24
|
+
- `"EDITED"` requires at least one response.
|
|
25
|
+
- `"REPLIED"` may omit `filesTouched`. If present, it must be empty.
|
|
26
|
+
- `"REPLIED"` requires at least one `"DISAGREE"` or `"ASK"` response.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Fix pull request #{pr} for {owner}/{repo}.
|
|
2
|
+
The PR worktree is {worktreePath}.
|
|
3
|
+
|
|
4
|
+
Act as the PR author and address the unresolved review threads provided in `<unresolved_threads>`.
|
|
5
|
+
|
|
6
|
+
For each thread, decide whether you agree with the reviewer.
|
|
7
|
+
If you understand and agree with the requested change, edit the code, stage changes, commit, and reply with action `"FIXED"` for each related thread.
|
|
8
|
+
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.
|
|
9
|
+
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.
|
|
10
|
+
Do not make changes just because a reviewer requested them. Do not push.
|