opencode-magi 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -10
- package/dist/config/output.js +1 -1
- package/dist/config/resolve.js +44 -26
- package/dist/config/validate.js +199 -191
- package/dist/config/worktree.js +13 -0
- package/dist/github/commands.js +8 -4
- package/dist/index.js +5 -1
- package/dist/orchestrator/merge.js +8 -1
- package/dist/orchestrator/review.js +30 -2
- package/dist/orchestrator/run-manager.js +10 -3
- package/dist/prompts/compose.js +10 -14
- package/dist/prompts/templates/{close-reconsideration.md → review/close-reconsideration.md} +1 -2
- package/package.json +24 -26
- package/schema.json +110 -91
- package/dist/prompts/templates/rereview-close-reconsideration.md +0 -6
- /package/dist/prompts/templates/{ci-classification-after-edit.md → merge/ci-classification.md} +0 -0
- /package/dist/prompts/templates/{edit.md → merge/edit.md} +0 -0
- /package/dist/prompts/templates/{ci-classification.md → review/ci-classification.md} +0 -0
- /package/dist/prompts/templates/{finding-validation.md → review/finding-validation.md} +0 -0
- /package/dist/prompts/templates/{rereview.md → review/rereview.md} +0 -0
- /package/dist/prompts/templates/{review.md → review/review.md} +0 -0
package/dist/config/validate.js
CHANGED
|
@@ -12,25 +12,20 @@ const validateSchema = AJV.compile(schema);
|
|
|
12
12
|
const CONFIG_KEYS = new Set([
|
|
13
13
|
"$schema",
|
|
14
14
|
"agents",
|
|
15
|
-
"automation",
|
|
16
15
|
"clear",
|
|
17
|
-
"checks",
|
|
18
|
-
"concurrency",
|
|
19
16
|
"github",
|
|
20
17
|
"language",
|
|
21
18
|
"merge",
|
|
22
19
|
"output",
|
|
23
|
-
"
|
|
24
|
-
"safety",
|
|
25
|
-
"worktree",
|
|
20
|
+
"review",
|
|
26
21
|
]);
|
|
27
|
-
const AGENTS_KEYS = new Set(["
|
|
22
|
+
const AGENTS_KEYS = new Set(["permissions"]);
|
|
28
23
|
const REVIEWER_KEYS = new Set([
|
|
29
24
|
"account",
|
|
30
25
|
"id",
|
|
31
26
|
"model",
|
|
32
27
|
"options",
|
|
33
|
-
"
|
|
28
|
+
"permissions",
|
|
34
29
|
"persona",
|
|
35
30
|
]);
|
|
36
31
|
const EDITOR_KEYS = new Set([
|
|
@@ -38,50 +33,61 @@ const EDITOR_KEYS = new Set([
|
|
|
38
33
|
"author",
|
|
39
34
|
"model",
|
|
40
35
|
"options",
|
|
41
|
-
"
|
|
36
|
+
"permissions",
|
|
42
37
|
"persona",
|
|
43
38
|
]);
|
|
44
39
|
const AUTHOR_KEYS = new Set(["email", "name"]);
|
|
45
40
|
const GITHUB_KEYS = new Set(["apiRetryAttempts", "host", "owner", "repo"]);
|
|
41
|
+
const REVIEW_KEYS = new Set([
|
|
42
|
+
"agents",
|
|
43
|
+
"automation",
|
|
44
|
+
"checks",
|
|
45
|
+
"concurrency",
|
|
46
|
+
"merge",
|
|
47
|
+
"output",
|
|
48
|
+
"prompts",
|
|
49
|
+
"safety",
|
|
50
|
+
"worktree",
|
|
51
|
+
]);
|
|
46
52
|
const MERGE_KEYS = new Set([
|
|
53
|
+
"automation",
|
|
54
|
+
"checks",
|
|
55
|
+
"editor",
|
|
56
|
+
"maxThreadResolutionCycles",
|
|
57
|
+
"prompts",
|
|
58
|
+
]);
|
|
59
|
+
const REVIEW_MERGE_KEYS = new Set([
|
|
47
60
|
"approvalPolicy",
|
|
48
61
|
"auto",
|
|
49
62
|
"deleteBranch",
|
|
50
|
-
"maxThreadResolutionCycles",
|
|
51
|
-
"mergeQueue",
|
|
52
63
|
"method",
|
|
64
|
+
"queue",
|
|
53
65
|
]);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
"retryFailedJobs",
|
|
57
|
-
"waitAfterEdit",
|
|
58
|
-
"waitBeforeReview",
|
|
59
|
-
]);
|
|
66
|
+
const REVIEW_CHECKS_KEYS = new Set(["exclude", "retryFailedJobs", "wait"]);
|
|
67
|
+
const MERGE_CHECKS_KEYS = new Set(["wait"]);
|
|
60
68
|
const AUTOMATION_KEYS = new Set(["close", "merge"]);
|
|
61
69
|
const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
|
|
62
70
|
const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
|
|
63
|
-
const OUTPUT_KEYS = new Set(["
|
|
64
|
-
const OUTPUT_DIR_KEYS = new Set(["pr"]);
|
|
65
|
-
const WORKTREE_KEYS = new Set(["dir"]);
|
|
71
|
+
const OUTPUT_KEYS = new Set(["repairAttempts"]);
|
|
66
72
|
const SAFETY_KEYS = new Set([
|
|
67
73
|
"allowAuthors",
|
|
68
74
|
"blockedPaths",
|
|
69
75
|
"maxChangedFiles",
|
|
70
76
|
"requiredLabels",
|
|
71
77
|
]);
|
|
72
|
-
const
|
|
78
|
+
const REVIEW_PROMPT_KEYS = new Set([
|
|
73
79
|
"ciClassification",
|
|
74
|
-
"ciClassificationAfterEdit",
|
|
75
80
|
"closeReconsideration",
|
|
76
|
-
"edit",
|
|
77
|
-
"editGuidelines",
|
|
78
81
|
"findingValidation",
|
|
79
|
-
"report",
|
|
80
82
|
"rereview",
|
|
81
|
-
"rereviewCloseReconsideration",
|
|
82
83
|
"review",
|
|
83
84
|
"reviewGuidelines",
|
|
84
85
|
]);
|
|
86
|
+
const MERGE_PROMPT_KEYS = new Set([
|
|
87
|
+
"ciClassification",
|
|
88
|
+
"edit",
|
|
89
|
+
"editGuidelines",
|
|
90
|
+
]);
|
|
85
91
|
function githubHost(config) {
|
|
86
92
|
return config.github?.host ?? "github.com";
|
|
87
93
|
}
|
|
@@ -210,7 +216,7 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
210
216
|
validateString(reviewer.persona, `${path}[${index}].persona`, errors);
|
|
211
217
|
if (reviewer.options != null && !isPlainObject(reviewer.options))
|
|
212
218
|
errors.push(`${path}[${index}].options must be an object`);
|
|
213
|
-
validatePermissionConfig(reviewer.
|
|
219
|
+
validatePermissionConfig(reviewer.permissions, `${path}[${index}].permissions`, errors);
|
|
214
220
|
if (reviewer.id) {
|
|
215
221
|
if (!validateReviewerId(reviewer.id)) {
|
|
216
222
|
errors.push(`${path}[${index}].id may contain only letters, numbers, underscores, and hyphens`);
|
|
@@ -233,7 +239,51 @@ function validateResolvedReviewers(reviewers, path, errors) {
|
|
|
233
239
|
accounts.add(reviewer.account);
|
|
234
240
|
}
|
|
235
241
|
}
|
|
242
|
+
function validateEditor(editor, path, errors, catalog) {
|
|
243
|
+
if (!editor)
|
|
244
|
+
return;
|
|
245
|
+
if (!isPlainObject(editor)) {
|
|
246
|
+
errors.push(`${path} must be an object`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!editor.model)
|
|
250
|
+
errors.push(`${path}.model is required`);
|
|
251
|
+
validateKnownKeys(editor, path, EDITOR_KEYS, errors);
|
|
252
|
+
validateString(editor.model, `${path}.model`, errors);
|
|
253
|
+
validateString(editor.account, `${path}.account`, errors);
|
|
254
|
+
validateString(editor.persona, `${path}.persona`, errors);
|
|
255
|
+
validateModel(editor.model, `${path}.model`, errors, catalog);
|
|
256
|
+
if (!editor.account)
|
|
257
|
+
errors.push(`${path}.account is required`);
|
|
258
|
+
if (editor.options != null && !isPlainObject(editor.options)) {
|
|
259
|
+
errors.push(`${path}.options must be an object`);
|
|
260
|
+
}
|
|
261
|
+
validatePermissionConfig(editor.permissions, `${path}.permissions`, errors);
|
|
262
|
+
const author = editor.author;
|
|
263
|
+
if (!author || !isPlainObject(author)) {
|
|
264
|
+
if (author != null)
|
|
265
|
+
errors.push(`${path}.author must be an object`);
|
|
266
|
+
errors.push(`${path}.author.name is required`);
|
|
267
|
+
errors.push(`${path}.author.email is required`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
validateKnownKeys(author, `${path}.author`, AUTHOR_KEYS, errors);
|
|
271
|
+
if (!author.name) {
|
|
272
|
+
errors.push(`${path}.author.name is required`);
|
|
273
|
+
}
|
|
274
|
+
else if (typeof author.name !== "string") {
|
|
275
|
+
errors.push(`${path}.author.name must be a string`);
|
|
276
|
+
}
|
|
277
|
+
if (!author.email) {
|
|
278
|
+
errors.push(`${path}.author.email is required`);
|
|
279
|
+
}
|
|
280
|
+
else if (typeof author.email !== "string") {
|
|
281
|
+
errors.push(`${path}.author.email must be a string`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
236
285
|
function validateMerge(config, errors, options) {
|
|
286
|
+
const merge = config.merge;
|
|
237
287
|
if (options.requireGithub ?? true) {
|
|
238
288
|
if (!config.github?.owner)
|
|
239
289
|
errors.push("github.owner is required");
|
|
@@ -247,102 +297,102 @@ function validateMerge(config, errors, options) {
|
|
|
247
297
|
if (config.github != null && !isPlainObject(config.github)) {
|
|
248
298
|
errors.push("github must be an object");
|
|
249
299
|
}
|
|
250
|
-
if (config.merge != null && !isPlainObject(config.merge)) {
|
|
251
|
-
errors.push("merge must be an object");
|
|
252
|
-
}
|
|
253
|
-
validateKnownKeys(config.merge, "merge", MERGE_KEYS, errors);
|
|
254
|
-
validateBoolean(config.merge?.auto, "merge.auto", errors);
|
|
255
|
-
validateBoolean(config.merge?.deleteBranch, "merge.deleteBranch", errors);
|
|
256
|
-
validateBoolean(config.merge?.mergeQueue, "merge.mergeQueue", errors);
|
|
257
300
|
if (config.github?.apiRetryAttempts != null &&
|
|
258
301
|
(typeof config.github.apiRetryAttempts !== "number" ||
|
|
259
302
|
!Number.isInteger(config.github.apiRetryAttempts) ||
|
|
260
303
|
config.github.apiRetryAttempts < 0)) {
|
|
261
304
|
errors.push("github.apiRetryAttempts must be a non-negative integer");
|
|
262
305
|
}
|
|
263
|
-
if (
|
|
264
|
-
(
|
|
265
|
-
!["merge", "rebase", "squash"].includes(config.merge.method))) {
|
|
266
|
-
errors.push("merge.method must be merge, squash, or rebase");
|
|
267
|
-
}
|
|
268
|
-
if (config.merge?.approvalPolicy != null &&
|
|
269
|
-
(typeof config.merge.approvalPolicy !== "string" ||
|
|
270
|
-
!["majority", "unanimous"].includes(config.merge.approvalPolicy))) {
|
|
271
|
-
errors.push("merge.approvalPolicy must be majority or unanimous");
|
|
306
|
+
if (merge != null && !isPlainObject(merge)) {
|
|
307
|
+
errors.push("merge must be an object");
|
|
272
308
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
309
|
+
validateKnownKeys(merge, "merge", MERGE_KEYS, errors);
|
|
310
|
+
validateBooleanObject(merge?.automation, "merge.automation", AUTOMATION_KEYS, errors);
|
|
311
|
+
const checks = merge?.checks;
|
|
312
|
+
validateKnownKeys(checks, "merge.checks", MERGE_CHECKS_KEYS, errors);
|
|
313
|
+
validateBoolean(checks?.wait, "merge.checks.wait", errors);
|
|
314
|
+
validateEditor(merge?.editor, "merge.editor", errors, options.modelCatalog);
|
|
315
|
+
if (merge?.maxThreadResolutionCycles != null &&
|
|
316
|
+
(typeof merge.maxThreadResolutionCycles !== "number" ||
|
|
317
|
+
!Number.isInteger(merge.maxThreadResolutionCycles) ||
|
|
318
|
+
merge.maxThreadResolutionCycles < 0)) {
|
|
277
319
|
errors.push("merge.maxThreadResolutionCycles must be a non-negative integer");
|
|
278
320
|
}
|
|
321
|
+
if (options.requireEditor && !merge?.editor)
|
|
322
|
+
errors.push("merge.editor is required");
|
|
323
|
+
}
|
|
324
|
+
function validateReviewMerge(config, errors) {
|
|
325
|
+
const merge = config.review?.merge;
|
|
326
|
+
if (merge != null && !isPlainObject(merge)) {
|
|
327
|
+
errors.push("review.merge must be an object");
|
|
328
|
+
}
|
|
329
|
+
validateKnownKeys(merge, "review.merge", REVIEW_MERGE_KEYS, errors);
|
|
330
|
+
validateBoolean(merge?.auto, "review.merge.auto", errors);
|
|
331
|
+
validateBoolean(merge?.deleteBranch, "review.merge.deleteBranch", errors);
|
|
332
|
+
validateBoolean(merge?.queue, "review.merge.queue", errors);
|
|
333
|
+
if (merge?.method != null &&
|
|
334
|
+
(typeof merge.method !== "string" ||
|
|
335
|
+
!["merge", "rebase", "squash"].includes(merge.method))) {
|
|
336
|
+
errors.push("review.merge.method must be merge, squash, or rebase");
|
|
337
|
+
}
|
|
338
|
+
if (merge?.approvalPolicy != null &&
|
|
339
|
+
(typeof merge.approvalPolicy !== "string" ||
|
|
340
|
+
!["majority", "unanimous"].includes(merge.approvalPolicy))) {
|
|
341
|
+
errors.push("review.merge.approvalPolicy must be majority or unanimous");
|
|
342
|
+
}
|
|
279
343
|
}
|
|
280
344
|
function validateConcurrency(config, errors) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
345
|
+
const concurrency = config.review?.concurrency;
|
|
346
|
+
if (concurrency != null && !isPlainObject(concurrency)) {
|
|
347
|
+
errors.push("review.concurrency must be an object");
|
|
348
|
+
}
|
|
349
|
+
validateKnownKeys(concurrency, "review.concurrency", CONCURRENCY_KEYS, errors);
|
|
350
|
+
if (concurrency?.runs != null) {
|
|
351
|
+
if (typeof concurrency.runs !== "number" ||
|
|
352
|
+
!Number.isInteger(concurrency.runs) ||
|
|
353
|
+
concurrency.runs < 1) {
|
|
354
|
+
errors.push("review.concurrency.runs must be a positive integer");
|
|
290
355
|
}
|
|
291
356
|
}
|
|
292
|
-
if (
|
|
293
|
-
if (typeof
|
|
294
|
-
!Number.isInteger(
|
|
295
|
-
|
|
296
|
-
errors.push("concurrency.reviewers must be a positive integer");
|
|
357
|
+
if (concurrency?.reviewers != null) {
|
|
358
|
+
if (typeof concurrency.reviewers !== "number" ||
|
|
359
|
+
!Number.isInteger(concurrency.reviewers) ||
|
|
360
|
+
concurrency.reviewers < 1) {
|
|
361
|
+
errors.push("review.concurrency.reviewers must be a positive integer");
|
|
297
362
|
}
|
|
298
363
|
}
|
|
299
364
|
}
|
|
300
365
|
function validateAutomation(config, errors) {
|
|
301
|
-
|
|
302
|
-
errors.push("automation must be an object");
|
|
303
|
-
}
|
|
304
|
-
validateKnownKeys(config.automation, "automation", AUTOMATION_KEYS, errors);
|
|
305
|
-
if (config.automation?.merge != null &&
|
|
306
|
-
typeof config.automation.merge !== "boolean") {
|
|
307
|
-
errors.push("automation.merge must be a boolean");
|
|
308
|
-
}
|
|
309
|
-
if (config.automation?.close != null &&
|
|
310
|
-
typeof config.automation.close !== "boolean") {
|
|
311
|
-
errors.push("automation.close must be a boolean");
|
|
312
|
-
}
|
|
366
|
+
validateBooleanObject(config.review?.automation, "review.automation", AUTOMATION_KEYS, errors);
|
|
313
367
|
}
|
|
314
368
|
function validateClear(config, errors) {
|
|
315
369
|
validateBooleanObject(config.clear, "clear", CLEAR_KEYS, errors);
|
|
316
370
|
}
|
|
317
371
|
function validateChecks(config, errors) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
372
|
+
const checks = config.review?.checks;
|
|
373
|
+
if (checks != null && !isPlainObject(checks)) {
|
|
374
|
+
errors.push("review.checks must be an object");
|
|
375
|
+
}
|
|
376
|
+
validateKnownKeys(checks, "review.checks", REVIEW_CHECKS_KEYS, errors);
|
|
377
|
+
if (checks?.exclude != null) {
|
|
378
|
+
if (!Array.isArray(checks.exclude)) {
|
|
379
|
+
errors.push("review.checks.exclude must be an array");
|
|
325
380
|
}
|
|
326
381
|
else {
|
|
327
|
-
|
|
382
|
+
checks.exclude.forEach((item, index) => {
|
|
328
383
|
if (typeof item !== "string")
|
|
329
|
-
errors.push(`checks.exclude[${index}] must be a string`);
|
|
384
|
+
errors.push(`review.checks.exclude[${index}] must be a string`);
|
|
330
385
|
});
|
|
331
386
|
}
|
|
332
387
|
}
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
errors.push("checks.waitBeforeReview must be a boolean");
|
|
336
|
-
}
|
|
337
|
-
if (config.checks?.waitAfterEdit != null &&
|
|
338
|
-
typeof config.checks.waitAfterEdit !== "boolean") {
|
|
339
|
-
errors.push("checks.waitAfterEdit must be a boolean");
|
|
388
|
+
if (checks?.wait != null && typeof checks.wait !== "boolean") {
|
|
389
|
+
errors.push("review.checks.wait must be a boolean");
|
|
340
390
|
}
|
|
341
|
-
if (
|
|
342
|
-
(typeof
|
|
343
|
-
!Number.isInteger(
|
|
344
|
-
|
|
345
|
-
errors.push("checks.retryFailedJobs must be a non-negative integer");
|
|
391
|
+
if (checks?.retryFailedJobs != null &&
|
|
392
|
+
(typeof checks.retryFailedJobs !== "number" ||
|
|
393
|
+
!Number.isInteger(checks.retryFailedJobs) ||
|
|
394
|
+
checks.retryFailedJobs < 0)) {
|
|
395
|
+
errors.push("review.checks.retryFailedJobs must be a non-negative integer");
|
|
346
396
|
}
|
|
347
397
|
}
|
|
348
398
|
function validateStringArray(value, path, errors) {
|
|
@@ -358,33 +408,44 @@ function validateStringArray(value, path, errors) {
|
|
|
358
408
|
});
|
|
359
409
|
}
|
|
360
410
|
function validateSafety(config, errors) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
validateStringArray(
|
|
367
|
-
validateStringArray(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
411
|
+
const safety = config.review?.safety;
|
|
412
|
+
if (safety != null && !isPlainObject(safety)) {
|
|
413
|
+
errors.push("review.safety must be an object");
|
|
414
|
+
}
|
|
415
|
+
validateKnownKeys(safety, "review.safety", SAFETY_KEYS, errors);
|
|
416
|
+
validateStringArray(safety?.allowAuthors, "review.safety.allowAuthors", errors);
|
|
417
|
+
validateStringArray(safety?.blockedPaths, "review.safety.blockedPaths", errors);
|
|
418
|
+
validateStringArray(safety?.requiredLabels, "review.safety.requiredLabels", errors);
|
|
419
|
+
if (safety?.maxChangedFiles != null &&
|
|
420
|
+
(typeof safety.maxChangedFiles !== "number" ||
|
|
421
|
+
!Number.isInteger(safety.maxChangedFiles) ||
|
|
422
|
+
safety.maxChangedFiles < 0)) {
|
|
423
|
+
errors.push("review.safety.maxChangedFiles must be a non-negative integer");
|
|
373
424
|
}
|
|
374
425
|
}
|
|
375
|
-
|
|
376
|
-
if (
|
|
426
|
+
function validatePromptObject(prompts, path, keys, errors) {
|
|
427
|
+
if (prompts == null)
|
|
377
428
|
return;
|
|
378
|
-
if (!isPlainObject(
|
|
379
|
-
errors.push(
|
|
429
|
+
if (!isPlainObject(prompts)) {
|
|
430
|
+
errors.push(`${path} must be an object`);
|
|
380
431
|
return;
|
|
381
432
|
}
|
|
382
|
-
validateKnownKeys(
|
|
383
|
-
|
|
384
|
-
if (typeof value !== "string")
|
|
385
|
-
errors.push(
|
|
433
|
+
validateKnownKeys(prompts, path, keys, errors);
|
|
434
|
+
for (const [key, value] of Object.entries(prompts)) {
|
|
435
|
+
if (typeof value !== "string")
|
|
436
|
+
errors.push(`${path}.${key} must be a string`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async function validatePrompts(config, errors, directory) {
|
|
440
|
+
validatePromptObject(config.review?.prompts, "review.prompts", REVIEW_PROMPT_KEYS, errors);
|
|
441
|
+
validatePromptObject(config.merge?.prompts, "merge.prompts", MERGE_PROMPT_KEYS, errors);
|
|
442
|
+
const promptEntries = [
|
|
443
|
+
...Object.entries(config.review?.prompts ?? {}).map(([key, value]) => [`review.prompts.${key}`, value]),
|
|
444
|
+
...Object.entries(config.merge?.prompts ?? {}).map(([key, value]) => [`merge.prompts.${key}`, value]),
|
|
445
|
+
];
|
|
446
|
+
await Promise.all(promptEntries.map(async ([path, value]) => {
|
|
447
|
+
if (typeof value !== "string")
|
|
386
448
|
return;
|
|
387
|
-
}
|
|
388
449
|
if (!directory)
|
|
389
450
|
return;
|
|
390
451
|
const fullPath = promptPath(directory, value);
|
|
@@ -392,13 +453,13 @@ async function validatePrompts(config, errors, directory) {
|
|
|
392
453
|
await access(fullPath, constants.R_OK);
|
|
393
454
|
}
|
|
394
455
|
catch {
|
|
395
|
-
errors.push(
|
|
456
|
+
errors.push(`${path} file is not readable: ${value}`);
|
|
396
457
|
}
|
|
397
458
|
}));
|
|
398
459
|
}
|
|
399
460
|
async function validateAuth(config, exec, errors) {
|
|
400
461
|
const accounts = new Set();
|
|
401
|
-
const agents = resolveAgents(config
|
|
462
|
+
const agents = resolveAgents(config);
|
|
402
463
|
for (const reviewer of agents.reviewers)
|
|
403
464
|
accounts.add(reviewer.account);
|
|
404
465
|
if (agents.editor)
|
|
@@ -420,7 +481,7 @@ async function fetchPermissions(config, exec, account) {
|
|
|
420
481
|
async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
421
482
|
if (!config.github?.owner || !config.github.repo)
|
|
422
483
|
return;
|
|
423
|
-
const agents = resolveAgents(config
|
|
484
|
+
const agents = resolveAgents(config);
|
|
424
485
|
await Promise.all(agents.reviewers.map(async (reviewer) => {
|
|
425
486
|
try {
|
|
426
487
|
const permissions = await fetchPermissions(config, exec, reviewer.account);
|
|
@@ -454,65 +515,32 @@ export async function validateConfig(config, options = {}) {
|
|
|
454
515
|
validateKnownKeys(config, "config", CONFIG_KEYS, errors);
|
|
455
516
|
validateString(config.$schema, "$schema", errors);
|
|
456
517
|
validateString(config.language, "language", errors);
|
|
457
|
-
if (!config.agents) {
|
|
458
|
-
errors.push("agents
|
|
518
|
+
if (config.agents != null && !isPlainObject(config.agents)) {
|
|
519
|
+
errors.push("agents must be an object");
|
|
459
520
|
}
|
|
460
521
|
else {
|
|
461
|
-
|
|
462
|
-
|
|
522
|
+
validateKnownKeys(config.agents, "agents", AGENTS_KEYS, errors);
|
|
523
|
+
validatePermissionConfig(config.agents?.permissions, "agents.permissions", errors);
|
|
524
|
+
}
|
|
525
|
+
if (!config.review) {
|
|
526
|
+
errors.push("review is required");
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
if (!isPlainObject(config.review)) {
|
|
530
|
+
errors.push("review must be an object");
|
|
463
531
|
}
|
|
464
532
|
else {
|
|
465
|
-
validateKnownKeys(config.
|
|
466
|
-
}
|
|
467
|
-
validatePermissionConfig(config.agents.permissions, "agents.permissions", errors);
|
|
468
|
-
if (!config.agents.reviewers)
|
|
469
|
-
errors.push("agents.reviewers is required");
|
|
470
|
-
validateReviewerList(config.agents.reviewers, "agents.reviewers", errors, options.modelCatalog);
|
|
471
|
-
if (options.requireEditor && !config.agents.editor)
|
|
472
|
-
errors.push("agents.editor is required");
|
|
473
|
-
if (config.agents.editor) {
|
|
474
|
-
if (!config.agents.editor.model)
|
|
475
|
-
errors.push("agents.editor.model is required");
|
|
476
|
-
validateKnownKeys(config.agents.editor, "agents.editor", EDITOR_KEYS, errors);
|
|
477
|
-
validateString(config.agents.editor.model, "agents.editor.model", errors);
|
|
478
|
-
validateString(config.agents.editor.account, "agents.editor.account", errors);
|
|
479
|
-
validateString(config.agents.editor.persona, "agents.editor.persona", errors);
|
|
480
|
-
validateModel(config.agents.editor.model, "agents.editor.model", errors, options.modelCatalog);
|
|
481
|
-
if (!config.agents.editor.account)
|
|
482
|
-
errors.push("agents.editor.account is required");
|
|
483
|
-
if (config.agents.editor.options != null &&
|
|
484
|
-
!isPlainObject(config.agents.editor.options)) {
|
|
485
|
-
errors.push("agents.editor.options must be an object");
|
|
486
|
-
}
|
|
487
|
-
validatePermissionConfig(config.agents.editor.permission, "agents.editor.permission", errors);
|
|
488
|
-
const author = config.agents.editor.author;
|
|
489
|
-
if (!author || !isPlainObject(author)) {
|
|
490
|
-
if (author != null)
|
|
491
|
-
errors.push("agents.editor.author must be an object");
|
|
492
|
-
errors.push("agents.editor.author.name is required");
|
|
493
|
-
errors.push("agents.editor.author.email is required");
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
validateKnownKeys(author, "agents.editor.author", AUTHOR_KEYS, errors);
|
|
497
|
-
if (!author.name) {
|
|
498
|
-
errors.push("agents.editor.author.name is required");
|
|
499
|
-
}
|
|
500
|
-
else if (typeof author.name !== "string") {
|
|
501
|
-
errors.push("agents.editor.author.name must be a string");
|
|
502
|
-
}
|
|
503
|
-
if (!author.email) {
|
|
504
|
-
errors.push("agents.editor.author.email is required");
|
|
505
|
-
}
|
|
506
|
-
else if (typeof author.email !== "string") {
|
|
507
|
-
errors.push("agents.editor.author.email must be a string");
|
|
508
|
-
}
|
|
509
|
-
}
|
|
533
|
+
validateKnownKeys(config.review, "review", REVIEW_KEYS, errors);
|
|
510
534
|
}
|
|
511
|
-
if (
|
|
512
|
-
|
|
535
|
+
if (!config.review.agents)
|
|
536
|
+
errors.push("review.agents is required");
|
|
537
|
+
validateReviewerList(config.review.agents, "review.agents", errors, options.modelCatalog);
|
|
538
|
+
if (Array.isArray(config.review.agents)) {
|
|
539
|
+
validateResolvedReviewers(resolveAgents(config).reviewers, "review.resolvedAgents", errors);
|
|
513
540
|
}
|
|
514
541
|
}
|
|
515
542
|
validateMerge(config, errors, options);
|
|
543
|
+
validateReviewMerge(config, errors);
|
|
516
544
|
validateAutomation(config, errors);
|
|
517
545
|
validateClear(config, errors);
|
|
518
546
|
validateChecks(config, errors);
|
|
@@ -530,28 +558,8 @@ export async function validateConfig(config, options = {}) {
|
|
|
530
558
|
errors.push("output.repairAttempts must be a non-negative integer");
|
|
531
559
|
}
|
|
532
560
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
errors.push("output.dirs must be an object");
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
validateKnownKeys(config.output.dirs, "output.dirs", OUTPUT_DIR_KEYS, errors);
|
|
539
|
-
const dirs = config.output.dirs;
|
|
540
|
-
for (const key of OUTPUT_DIR_KEYS) {
|
|
541
|
-
const value = dirs[key];
|
|
542
|
-
if (value != null && typeof value !== "string") {
|
|
543
|
-
errors.push(`output.dirs.${key} must be a string`);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
if (config.worktree != null && !isPlainObject(config.worktree)) {
|
|
549
|
-
errors.push("worktree must be an object");
|
|
550
|
-
}
|
|
551
|
-
validateKnownKeys(config.worktree, "worktree", WORKTREE_KEYS, errors);
|
|
552
|
-
if (config.worktree?.dir != null && typeof config.worktree.dir !== "string") {
|
|
553
|
-
errors.push("worktree.dir must be a string");
|
|
554
|
-
}
|
|
561
|
+
validateString(config.review?.output, "review.output", errors);
|
|
562
|
+
validateString(config.review?.worktree, "review.worktree", errors);
|
|
555
563
|
if (options.checkAuth && !errors.length) {
|
|
556
564
|
if (!options.exec) {
|
|
557
565
|
errors.push("validateConfig requires exec when checkAuth is true");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isAbsolute, join } from "node:path";
|
|
2
|
+
const DEFAULT_WORKTREE_DIRS = {
|
|
3
|
+
pr: ".magi/worktrees/pr",
|
|
4
|
+
};
|
|
5
|
+
function resolvePath(directory, path) {
|
|
6
|
+
return isAbsolute(path) ? path : join(directory, path);
|
|
7
|
+
}
|
|
8
|
+
export function worktreeBaseDir(directory, config, kind) {
|
|
9
|
+
return resolvePath(directory, config.review?.worktree ?? DEFAULT_WORKTREE_DIRS[kind]);
|
|
10
|
+
}
|
|
11
|
+
export function worktreeBaseDirs(directory, config = {}) {
|
|
12
|
+
return [worktreeBaseDir(directory, config, "pr")];
|
|
13
|
+
}
|
package/dist/github/commands.js
CHANGED
|
@@ -41,7 +41,7 @@ async function withWorktreeCreateLock(key, run) {
|
|
|
41
41
|
async function checkoutPullRequestWithRetry(exec, repository, pr, worktreePath) {
|
|
42
42
|
for (let attempt = 0;; attempt += 1) {
|
|
43
43
|
try {
|
|
44
|
-
await exec(`gh pr checkout ${pr} --repo ${shellQuote(repoSpecifier(repository))}`, {
|
|
44
|
+
await exec(`gh pr checkout ${pr} --repo ${shellQuote(repoSpecifier(repository))} --detach`, {
|
|
45
45
|
cwd: worktreePath,
|
|
46
46
|
});
|
|
47
47
|
return;
|
|
@@ -70,6 +70,9 @@ export function repoSpecifier(repository) {
|
|
|
70
70
|
? repoSlug(repository)
|
|
71
71
|
: `${host}/${repoSlug(repository)}`;
|
|
72
72
|
}
|
|
73
|
+
function repositoryGitUrl(repository, owner, repo) {
|
|
74
|
+
return `https://${githubHost(repository)}/${owner}/${repo}.git`;
|
|
75
|
+
}
|
|
73
76
|
export function ghHostOption(repository) {
|
|
74
77
|
const host = githubHost(repository);
|
|
75
78
|
return host === "github.com" ? "" : ` --hostname ${shellQuote(host)}`;
|
|
@@ -78,7 +81,7 @@ export async function ghToken(exec, repository, account) {
|
|
|
78
81
|
return (await exec(`gh auth token${ghHostOption(repository)} --user ${shellQuote(account)}`)).trim();
|
|
79
82
|
}
|
|
80
83
|
export async function fetchPullRequest(exec, repository, pr) {
|
|
81
|
-
const json = await exec(`gh pr view ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,url,isDraft,baseRefOid,headRefOid,baseRefName,headRefName`);
|
|
84
|
+
const json = await exec(`gh pr view ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,url,isDraft,baseRefOid,headRefOid,baseRefName,headRefName,headRepository,headRepositoryOwner`);
|
|
82
85
|
return JSON.parse(json);
|
|
83
86
|
}
|
|
84
87
|
export async function fetchPullRequestReviews(exec, repository, pr) {
|
|
@@ -318,9 +321,10 @@ export async function closePullRequest(exec, repository, pr, account) {
|
|
|
318
321
|
const token = await ghToken(exec, repository, account);
|
|
319
322
|
return exec(`GH_TOKEN=${shellQuote(token)} gh pr close ${pr} --repo ${shellQuote(repoSpecifier(repository))}`);
|
|
320
323
|
}
|
|
321
|
-
export async function pushHead(exec, repository, worktreePath, account) {
|
|
324
|
+
export async function pushHead(exec, repository, worktreePath, account, head) {
|
|
322
325
|
const token = await ghToken(exec, repository, account);
|
|
323
|
-
|
|
326
|
+
const url = repositoryGitUrl(repository, head.owner, head.repo);
|
|
327
|
+
await exec(`git -c credential.helper= -c credential.helper=${shellQuote(`!f() { echo username=x-access-token; echo password=${token}; }; f`)} push ${shellQuote(url)} ${shellQuote(`HEAD:refs/heads/${head.ref}`)}`, { cwd: worktreePath });
|
|
324
328
|
}
|
|
325
329
|
export async function configureGitIdentity(exec, worktreePath, identity) {
|
|
326
330
|
if (identity.name) {
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
|
|
|
7
7
|
import { MAGI_COMMANDS } from "./commands";
|
|
8
8
|
import { loadConfig, mergeMagiConfig } from "./config/load";
|
|
9
9
|
import { outputBaseDirs } from "./config/output";
|
|
10
|
+
import { worktreeBaseDirs } from "./config/worktree";
|
|
10
11
|
import { resolveRepository } from "./config/resolve";
|
|
11
12
|
import { validateConfig } from "./config/validate";
|
|
12
13
|
import { withGitHubApiRetry } from "./github/retry";
|
|
@@ -189,7 +190,7 @@ export async function validateMagiConfigFiles(directory, options = {}) {
|
|
|
189
190
|
? withGitHubApiRetry(options.exec, mergedConfig.github?.apiRetryAttempts ?? 3)
|
|
190
191
|
: undefined,
|
|
191
192
|
modelCatalog: options.modelCatalog,
|
|
192
|
-
requireGithub: hasProjectConfig && Boolean(mergedConfig.agents
|
|
193
|
+
requireGithub: hasProjectConfig && Boolean(mergedConfig.review?.agents),
|
|
193
194
|
});
|
|
194
195
|
loadedFrom = existing.map((status) => status.path).join(", ");
|
|
195
196
|
errors.push(...validation.errors);
|
|
@@ -435,6 +436,9 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
435
436
|
: undefined,
|
|
436
437
|
pr: parseOptionalPr(args.pr),
|
|
437
438
|
runId: args.runId,
|
|
439
|
+
worktreeDir: loaded
|
|
440
|
+
? worktreeBaseDirs(directory, loaded.config)
|
|
441
|
+
: undefined,
|
|
438
442
|
});
|
|
439
443
|
},
|
|
440
444
|
}),
|
|
@@ -109,7 +109,13 @@ async function runEditor(input, worktreePath, cycle, unresolvedThreads) {
|
|
|
109
109
|
await input.onProgress?.({ cycle, type: "editor_completed" });
|
|
110
110
|
if (!input.dryRun) {
|
|
111
111
|
if (result.value.mode === "EDITED") {
|
|
112
|
-
await
|
|
112
|
+
const meta = await fetchPullRequest(input.exec, input.repository, input.pr);
|
|
113
|
+
const headOwner = meta.headRepositoryOwner?.login;
|
|
114
|
+
const headRepo = meta.headRepository?.name;
|
|
115
|
+
if (!headOwner || !headRepo) {
|
|
116
|
+
throw new Error("Pull request head repository is missing");
|
|
117
|
+
}
|
|
118
|
+
await pushHead(input.exec, input.repository, worktreePath, editor.account, { owner: headOwner, ref: meta.headRefName, repo: headRepo });
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
throwIfAborted(input.signal);
|
|
@@ -565,6 +571,7 @@ export async function runMerge(input) {
|
|
|
565
571
|
...abortableInput,
|
|
566
572
|
allowAlreadyReviewed: true,
|
|
567
573
|
approvalPolicy: input.repository.merge.approvalPolicy,
|
|
574
|
+
enableReviewAutomation: false,
|
|
568
575
|
onProgress: (progress) => input.onProgress?.(progress),
|
|
569
576
|
runId: input.runId,
|
|
570
577
|
dryRun: input.dryRun,
|