opencode-magi 0.9.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 +37 -34
- package/dist/commands.js +11 -9
- package/dist/config/index.js +2 -0
- package/dist/config/resolve.js +120 -248
- package/dist/config/validate.js +103 -1100
- 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 +9 -7
- 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 -1478
- 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/github/retry.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
function errorText(error) {
|
|
2
|
-
if (!error || typeof error !== "object")
|
|
3
|
-
return String(error);
|
|
4
|
-
const value = error;
|
|
5
|
-
return [value.message, value.stderr, value.stdout]
|
|
6
|
-
.filter((item) => typeof item === "string")
|
|
7
|
-
.join("\n");
|
|
8
|
-
}
|
|
9
|
-
function isGitHubCommand(command) {
|
|
10
|
-
return /(^|\s)gh\s+(api|auth|pr|run)\b/.test(command);
|
|
11
|
-
}
|
|
12
|
-
function isRateLimitError(error) {
|
|
13
|
-
return /rate limit/i.test(errorText(error));
|
|
14
|
-
}
|
|
15
|
-
async function delay(ms, signal) {
|
|
16
|
-
if (ms <= 0)
|
|
17
|
-
return;
|
|
18
|
-
if (signal?.aborted)
|
|
19
|
-
throw new DOMException("Aborted", "AbortError");
|
|
20
|
-
await new Promise((resolve, reject) => {
|
|
21
|
-
const timeout = setTimeout(resolve, ms);
|
|
22
|
-
signal?.addEventListener("abort", () => {
|
|
23
|
-
clearTimeout(timeout);
|
|
24
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
25
|
-
}, { once: true });
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
export function withGitHubApiRetry(exec, retryAttempts, delayMs = 1_000) {
|
|
29
|
-
return async (command, options) => {
|
|
30
|
-
for (let attempt = 0;; attempt += 1) {
|
|
31
|
-
try {
|
|
32
|
-
return await exec(command, options);
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
if (attempt >= retryAttempts ||
|
|
36
|
-
!isGitHubCommand(command) ||
|
|
37
|
-
!isRateLimitError(error)) {
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
await delay(delayMs * 2 ** attempt, options?.signal);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export function throwIfAborted(signal) {
|
|
2
|
-
signal?.throwIfAborted();
|
|
3
|
-
}
|
|
4
|
-
export function withAbortSignal(exec, signal) {
|
|
5
|
-
return async (command, options) => {
|
|
6
|
-
throwIfAborted(signal);
|
|
7
|
-
return exec(command, { ...options, signal: options?.signal ?? signal });
|
|
8
|
-
};
|
|
9
|
-
}
|
package/dist/orchestrator/ci.js
DELETED
|
@@ -1,579 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { applyCheckExclusions, checkJobId, checkRunId, fetchCheckFailureLog, fetchPullRequestChecks, fetchWorkflowRunMeta, isCancelledCheck, isFailedCheck, rerunCheckJob, watchChecks, watchRun, } from "../github/commands";
|
|
4
|
-
import { composeCiClassificationAfterEditPrompt, composeCiClassificationPrompt, } from "../prompts/compose";
|
|
5
|
-
import { parseCiClassificationOutput } from "../prompts/output";
|
|
6
|
-
import { majorityThreshold } from "./majority";
|
|
7
|
-
import { runModelWithRepair } from "./model";
|
|
8
|
-
import { mapPool } from "./pool";
|
|
9
|
-
function cleanLogLine(line) {
|
|
10
|
-
return (line
|
|
11
|
-
// oxlint-disable-next-line no-control-regex
|
|
12
|
-
.replaceAll(/\u001B\[[0-9;]*m/g, "")
|
|
13
|
-
.replace(/^\S+\s+UNKNOWN STEP\s+/, "")
|
|
14
|
-
.replace(/^\d{4}-\d{2}-\d{2}T\S+Z\s*/, "")
|
|
15
|
-
.replace(/^##\[error\]/, "")
|
|
16
|
-
.trim());
|
|
17
|
-
}
|
|
18
|
-
function uniqueLimited(values, limit) {
|
|
19
|
-
const seen = new Set();
|
|
20
|
-
const result = [];
|
|
21
|
-
for (const value of values) {
|
|
22
|
-
const clean = value.trim();
|
|
23
|
-
if (!clean || seen.has(clean))
|
|
24
|
-
continue;
|
|
25
|
-
seen.add(clean);
|
|
26
|
-
result.push(clean);
|
|
27
|
-
if (result.length >= limit)
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
30
|
-
return result;
|
|
31
|
-
}
|
|
32
|
-
function compactRepeated(values, limit) {
|
|
33
|
-
const counts = new Map();
|
|
34
|
-
const order = [];
|
|
35
|
-
for (const value of values) {
|
|
36
|
-
const clean = value.trim();
|
|
37
|
-
if (!clean)
|
|
38
|
-
continue;
|
|
39
|
-
if (!counts.has(clean))
|
|
40
|
-
order.push(clean);
|
|
41
|
-
counts.set(clean, (counts.get(clean) ?? 0) + 1);
|
|
42
|
-
}
|
|
43
|
-
return order.slice(0, limit).map((value) => {
|
|
44
|
-
const count = counts.get(value) ?? 1;
|
|
45
|
-
return count > 1 ? `${value} (repeated ${count} times)` : value;
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
function representativeLog(lines, maxChars = 2_000) {
|
|
49
|
-
const selected = uniqueLimited(lines.filter((line) => {
|
|
50
|
-
return (/\b(error|failed|failure|exception|traceback|panic)\b/i.test(line) ||
|
|
51
|
-
/\b(assertionerror|rangeerror|timeouterror|typeerror)\b/i.test(line) ||
|
|
52
|
-
/\bFAIL(?:ED)?\b/.test(line) ||
|
|
53
|
-
/(?:\u276f|\bat\s+).+\.\w+(?::\d+)/.test(line));
|
|
54
|
-
}), 40).join("\n");
|
|
55
|
-
if (selected.length <= maxChars)
|
|
56
|
-
return selected;
|
|
57
|
-
return `${selected.slice(0, maxChars).trimEnd()}\n...`;
|
|
58
|
-
}
|
|
59
|
-
export function extractFailureEvidence(log) {
|
|
60
|
-
const lines = log
|
|
61
|
-
.replaceAll("\r\n", "\n")
|
|
62
|
-
.split("\n")
|
|
63
|
-
.map(cleanLogLine)
|
|
64
|
-
.filter(Boolean);
|
|
65
|
-
const files = [];
|
|
66
|
-
const frames = [];
|
|
67
|
-
const errors = [];
|
|
68
|
-
const tests = [];
|
|
69
|
-
for (const line of lines) {
|
|
70
|
-
if (/\bFAIL(?:ED)?\b/.test(line))
|
|
71
|
-
tests.push(line);
|
|
72
|
-
if (/\b(?:RangeError|TypeError|ReferenceError|SyntaxError|AssertionError|TimeoutError|Error):\s+.+/i.test(line)) {
|
|
73
|
-
errors.push(line.replace(/^.*?((?:RangeError|TypeError|ReferenceError|SyntaxError|AssertionError|TimeoutError|Error):\s+.+)$/i, "$1"));
|
|
74
|
-
}
|
|
75
|
-
if (/\b(error|failed|failure|exception|traceback|panic|command failed|exit code)\b/i.test(line)) {
|
|
76
|
-
errors.push(line);
|
|
77
|
-
}
|
|
78
|
-
if (/(?:\u276f|\bat\s+).+\.\w+(?::\d+)/.test(line))
|
|
79
|
-
frames.push(line);
|
|
80
|
-
for (const match of line.matchAll(/(?:^|\s)([A-Za-z0-9_./-]+\.(?:cjs|css|jsx|mdx|mjs|scss|tsx|ts|js|vue|svelte))(?::\d+(?::\d+)?)?/g)) {
|
|
81
|
-
files.push(match[1]);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
errorMessages: uniqueLimited(errors, 5),
|
|
86
|
-
failingFiles: uniqueLimited(files, 8),
|
|
87
|
-
failingTests: uniqueLimited(tests, 8),
|
|
88
|
-
relevantFrames: compactRepeated(frames, 8),
|
|
89
|
-
representativeLog: representativeLog(lines),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
function emptyReport() {
|
|
93
|
-
return {
|
|
94
|
-
attempts: 0,
|
|
95
|
-
classifierRuns: [],
|
|
96
|
-
dryRunRerun: [],
|
|
97
|
-
excluded: [],
|
|
98
|
-
failed: [],
|
|
99
|
-
rerun: [],
|
|
100
|
-
scopeInside: [],
|
|
101
|
-
scopeOutsideRecovered: [],
|
|
102
|
-
scopeOutsideUnresolved: [],
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
export function compactLog(log, maxChars = 18_000) {
|
|
106
|
-
const lines = log.replaceAll("\r\n", "\n").split("\n");
|
|
107
|
-
const patterns = [
|
|
108
|
-
/\b(error|failed|failure|exception|traceback|panic)\b/i,
|
|
109
|
-
/\b(assertionerror|timeouterror|typeerror|referenceerror)\b/i,
|
|
110
|
-
/\b(exit code|exited with|command failed)\b/i,
|
|
111
|
-
/^\s*(FAIL|FAILED)\b/i,
|
|
112
|
-
];
|
|
113
|
-
const selected = new Map();
|
|
114
|
-
function includeRange(start, end) {
|
|
115
|
-
for (let index = Math.max(0, start); index < Math.min(lines.length, end); index += 1) {
|
|
116
|
-
selected.set(index, lines[index]);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
includeRange(0, Math.min(40, lines.length));
|
|
120
|
-
includeRange(Math.max(0, lines.length - 220), lines.length);
|
|
121
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
122
|
-
if (patterns.some((pattern) => pattern.test(lines[index]))) {
|
|
123
|
-
includeRange(index - 12, index + 35);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const compacted = [...selected]
|
|
127
|
-
.sort(([left], [right]) => left - right)
|
|
128
|
-
.map(([index, line], position, all) => {
|
|
129
|
-
const previous = all[position - 1]?.[0];
|
|
130
|
-
const prefix = previous != null && index > previous + 1 ? "\n...\n" : "";
|
|
131
|
-
return `${prefix}${line}`;
|
|
132
|
-
})
|
|
133
|
-
.join("\n")
|
|
134
|
-
.trim();
|
|
135
|
-
if (compacted.length <= maxChars)
|
|
136
|
-
return compacted;
|
|
137
|
-
return [
|
|
138
|
-
compacted.slice(0, Math.floor(maxChars * 0.35)).trimEnd(),
|
|
139
|
-
"\n... log truncated ...\n",
|
|
140
|
-
compacted.slice(compacted.length - Math.floor(maxChars * 0.65)).trimStart(),
|
|
141
|
-
].join("");
|
|
142
|
-
}
|
|
143
|
-
function ciFailureContextForClassified(items, classified) {
|
|
144
|
-
const classifiedByName = new Map(classified.map((item) => [item.check.name, item]));
|
|
145
|
-
const sections = items
|
|
146
|
-
.filter((item) => classifiedByName.has(item.check.name))
|
|
147
|
-
.map((item) => {
|
|
148
|
-
const classifiedCheck = classifiedByName.get(item.check.name);
|
|
149
|
-
const evidence = item.evidence;
|
|
150
|
-
const lines = [
|
|
151
|
-
`## ${item.check.name} (${item.check.workflow || "unknown workflow"})`,
|
|
152
|
-
`State: ${item.check.state}`,
|
|
153
|
-
item.check.link ? `Link: ${item.check.link}` : "",
|
|
154
|
-
classifiedCheck?.reason
|
|
155
|
-
? `Classifier reason: ${classifiedCheck.reason}`
|
|
156
|
-
: "",
|
|
157
|
-
evidence.errorMessages.length
|
|
158
|
-
? `Errors:\n${evidence.errorMessages.map((line) => `- ${line}`).join("\n")}`
|
|
159
|
-
: "",
|
|
160
|
-
evidence.failingFiles.length
|
|
161
|
-
? `Files mentioned:\n${evidence.failingFiles.map((line) => `- ${line}`).join("\n")}`
|
|
162
|
-
: "",
|
|
163
|
-
evidence.failingTests.length
|
|
164
|
-
? `Failing tests:\n${evidence.failingTests.map((line) => `- ${line}`).join("\n")}`
|
|
165
|
-
: "",
|
|
166
|
-
evidence.relevantFrames.length
|
|
167
|
-
? `Relevant frames:\n${evidence.relevantFrames.map((line) => `- ${line}`).join("\n")}`
|
|
168
|
-
: "",
|
|
169
|
-
evidence.representativeLog
|
|
170
|
-
? `Representative log:\n\`\`\`text\n${evidence.representativeLog}\n\`\`\``
|
|
171
|
-
: "",
|
|
172
|
-
];
|
|
173
|
-
return lines.filter(Boolean).join("\n");
|
|
174
|
-
});
|
|
175
|
-
if (!sections.length)
|
|
176
|
-
return "";
|
|
177
|
-
return [
|
|
178
|
-
"CI has scope-in failures that may be caused by this PR.",
|
|
179
|
-
"Treat these failures as blocking review issues until the checks pass.",
|
|
180
|
-
[
|
|
181
|
-
"Do not approve this PR while this ci_failure_context is present.",
|
|
182
|
-
"Return CHANGES_REQUESTED and include a finding for each failing CI check.",
|
|
183
|
-
].join(" "),
|
|
184
|
-
[
|
|
185
|
-
"Still inspect the PR diff before reporting findings.",
|
|
186
|
-
"If a CI failure does not map to an exact changed line, anchor the finding to the nearest responsible or first relevant changed line.",
|
|
187
|
-
].join(" "),
|
|
188
|
-
"",
|
|
189
|
-
...sections,
|
|
190
|
-
].join("\n\n");
|
|
191
|
-
}
|
|
192
|
-
async function checksWithLogs(exec, repository, checks) {
|
|
193
|
-
return Promise.all(checks.map(async (check) => {
|
|
194
|
-
const jobId = checkJobId(check);
|
|
195
|
-
const rawLog = jobId
|
|
196
|
-
? await fetchCheckFailureLog(exec, repository, jobId).catch((error) => `Could not fetch failed log: ${error.message}`)
|
|
197
|
-
: "This check is not a GitHub Actions job and cannot be rerun.";
|
|
198
|
-
const log = compactLog(rawLog);
|
|
199
|
-
return { check, evidence: extractFailureEvidence(log), jobId };
|
|
200
|
-
}));
|
|
201
|
-
}
|
|
202
|
-
function errorMessage(error) {
|
|
203
|
-
return error instanceof Error ? error.message : String(error);
|
|
204
|
-
}
|
|
205
|
-
function isPendingCheck(check) {
|
|
206
|
-
return (check.bucket === "pending" ||
|
|
207
|
-
check.state === "ACTION_REQUIRED" ||
|
|
208
|
-
check.state === "EXPECTED" ||
|
|
209
|
-
check.state === "IN_PROGRESS" ||
|
|
210
|
-
check.state === "PENDING" ||
|
|
211
|
-
check.state === "QUEUED" ||
|
|
212
|
-
check.state === "REQUESTED" ||
|
|
213
|
-
check.state === "WAITING");
|
|
214
|
-
}
|
|
215
|
-
function cancelledClassification(check) {
|
|
216
|
-
return {
|
|
217
|
-
check,
|
|
218
|
-
classification: "SCOPE_OUT",
|
|
219
|
-
reason: "Check was cancelled; rerun without CI scope classification.",
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
async function watchRerunRuns(exec, repository, checks) {
|
|
223
|
-
const runIds = [
|
|
224
|
-
...new Set(checks.flatMap((item) => {
|
|
225
|
-
const runId = checkRunId(item.check);
|
|
226
|
-
return runId ? [runId] : [];
|
|
227
|
-
})),
|
|
228
|
-
];
|
|
229
|
-
await Promise.all(runIds.map((runId) => watchRun(exec, repository, runId)));
|
|
230
|
-
}
|
|
231
|
-
async function checksForHead(input) {
|
|
232
|
-
const checks = await fetchPullRequestChecks(input.exec, input.repository, input.pr, { requiredOnly: true, tolerateMissingChecks: Boolean(input.headSha) });
|
|
233
|
-
const targetChecks = [];
|
|
234
|
-
let hasAnyActionCheck = false;
|
|
235
|
-
let hasTargetActionCheck = false;
|
|
236
|
-
for (const check of checks) {
|
|
237
|
-
const runId = checkRunId(check);
|
|
238
|
-
if (!input.headSha || !runId) {
|
|
239
|
-
targetChecks.push(check);
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
hasAnyActionCheck = true;
|
|
243
|
-
const runHead = await runHeadSha({
|
|
244
|
-
exec: input.exec,
|
|
245
|
-
repository: input.repository,
|
|
246
|
-
runHeadCache: input.runHeadCache,
|
|
247
|
-
runId,
|
|
248
|
-
});
|
|
249
|
-
if (runHead === input.headSha) {
|
|
250
|
-
hasTargetActionCheck = true;
|
|
251
|
-
targetChecks.push(check);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return {
|
|
255
|
-
blocking: targetChecks.filter((check) => isFailedCheck(check) || isCancelledCheck(check)),
|
|
256
|
-
hasAnyActionCheck,
|
|
257
|
-
hasPending: targetChecks.some(isPendingCheck),
|
|
258
|
-
hasTargetActionCheck,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
function runHeadSha(input) {
|
|
262
|
-
const cached = input.runHeadCache.get(input.runId);
|
|
263
|
-
if (cached)
|
|
264
|
-
return cached;
|
|
265
|
-
const promise = fetchWorkflowRunMeta(input.exec, input.repository, input.runId)
|
|
266
|
-
.then((run) => run.headSha || undefined)
|
|
267
|
-
.catch(() => undefined);
|
|
268
|
-
input.runHeadCache.set(input.runId, promise);
|
|
269
|
-
return promise;
|
|
270
|
-
}
|
|
271
|
-
async function sleep(ms) {
|
|
272
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
273
|
-
}
|
|
274
|
-
async function classifyChecks(input) {
|
|
275
|
-
const reviewers = input.repository.agents.reviewers;
|
|
276
|
-
const classifierRuns = [];
|
|
277
|
-
const names = new Set(input.checks.map((item) => item.check.name));
|
|
278
|
-
const checks = input.checks.map((item) => ({
|
|
279
|
-
evidence: item.evidence,
|
|
280
|
-
link: item.check.link,
|
|
281
|
-
name: item.check.name,
|
|
282
|
-
state: item.check.state,
|
|
283
|
-
workflow: item.check.workflow,
|
|
284
|
-
}));
|
|
285
|
-
const prompt = input.afterEdit
|
|
286
|
-
? await composeCiClassificationAfterEditPrompt({
|
|
287
|
-
...input.afterEdit,
|
|
288
|
-
checks,
|
|
289
|
-
directory: input.directory,
|
|
290
|
-
pr: input.pr,
|
|
291
|
-
repository: input.repository,
|
|
292
|
-
})
|
|
293
|
-
: await composeCiClassificationPrompt({
|
|
294
|
-
checks,
|
|
295
|
-
directory: input.directory,
|
|
296
|
-
pr: input.pr,
|
|
297
|
-
repository: input.repository,
|
|
298
|
-
});
|
|
299
|
-
if (!reviewers.length) {
|
|
300
|
-
return {
|
|
301
|
-
classified: input.checks.map((item) => ({
|
|
302
|
-
check: item.check,
|
|
303
|
-
classification: "SCOPE_IN",
|
|
304
|
-
reason: "No reviewer model is configured for CI classification.",
|
|
305
|
-
})),
|
|
306
|
-
classifierRuns,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
if (input.outputDir)
|
|
310
|
-
await mkdir(input.outputDir, { recursive: true });
|
|
311
|
-
const votes = await mapPool(reviewers, input.repository.concurrency.reviewers, async (reviewer) => {
|
|
312
|
-
const run = {
|
|
313
|
-
repairAttempts: 0,
|
|
314
|
-
reviewer: reviewer.key,
|
|
315
|
-
status: "running",
|
|
316
|
-
};
|
|
317
|
-
const promptPath = input.outputDir
|
|
318
|
-
? join(input.outputDir, `${reviewer.key}.ci-classification.prompt.txt`)
|
|
319
|
-
: undefined;
|
|
320
|
-
run.promptPath = promptPath;
|
|
321
|
-
classifierRuns.push(run);
|
|
322
|
-
if (promptPath)
|
|
323
|
-
await writeFile(promptPath, prompt);
|
|
324
|
-
await input.onClassifierProgress?.({
|
|
325
|
-
promptPath,
|
|
326
|
-
reviewer: reviewer.key,
|
|
327
|
-
type: "classifier_started",
|
|
328
|
-
});
|
|
329
|
-
try {
|
|
330
|
-
const result = await runModelWithRepair({
|
|
331
|
-
client: input.client,
|
|
332
|
-
model: reviewer.model,
|
|
333
|
-
onProgress: async (progress) => {
|
|
334
|
-
if (progress.type === "session_created") {
|
|
335
|
-
run.sessionId = progress.sessionId;
|
|
336
|
-
await input.onClassifierProgress?.({
|
|
337
|
-
reviewer: reviewer.key,
|
|
338
|
-
runAttempt: progress.runAttempt,
|
|
339
|
-
sessionId: progress.sessionId,
|
|
340
|
-
type: "classifier_session",
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
if (progress.type === "repair") {
|
|
344
|
-
run.status = "repairing";
|
|
345
|
-
run.repairAttempts += 1;
|
|
346
|
-
await input.onClassifierProgress?.({
|
|
347
|
-
reviewer: reviewer.key,
|
|
348
|
-
type: "classifier_repair",
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
options: reviewer.options,
|
|
353
|
-
parentSessionId: input.parentSessionId,
|
|
354
|
-
parse: (text) => {
|
|
355
|
-
const output = parseCiClassificationOutput(text);
|
|
356
|
-
for (const check of output.checks) {
|
|
357
|
-
if (!names.has(check.name))
|
|
358
|
-
throw new Error(`unexpected CI check classification: ${check.name}`);
|
|
359
|
-
}
|
|
360
|
-
for (const name of names) {
|
|
361
|
-
if (!output.checks.some((check) => check.name === name))
|
|
362
|
-
throw new Error(`missing CI check classification: ${name}`);
|
|
363
|
-
}
|
|
364
|
-
return output;
|
|
365
|
-
},
|
|
366
|
-
permission: reviewer.permission,
|
|
367
|
-
prompt,
|
|
368
|
-
repairAttempts: input.repairAttempts,
|
|
369
|
-
runAttempts: 2,
|
|
370
|
-
schemaName: "CI classification",
|
|
371
|
-
signal: input.signal,
|
|
372
|
-
title: `magi classify ci ${input.repository.alias}#${input.pr} ${reviewer.key}`,
|
|
373
|
-
});
|
|
374
|
-
const rawPath = input.outputDir
|
|
375
|
-
? join(input.outputDir, `${reviewer.key}.ci-classification.raw.txt`)
|
|
376
|
-
: undefined;
|
|
377
|
-
const checks = result.value.checks.map((check) => ({
|
|
378
|
-
classification: check.classification,
|
|
379
|
-
name: check.name,
|
|
380
|
-
reason: check.reason,
|
|
381
|
-
}));
|
|
382
|
-
if (rawPath)
|
|
383
|
-
await writeFile(rawPath, result.raw);
|
|
384
|
-
run.checks = checks;
|
|
385
|
-
run.rawPath = rawPath;
|
|
386
|
-
run.sessionId = result.sessionId;
|
|
387
|
-
run.status = "completed";
|
|
388
|
-
await input.onClassifierProgress?.({
|
|
389
|
-
checks,
|
|
390
|
-
rawPath,
|
|
391
|
-
reviewer: reviewer.key,
|
|
392
|
-
sessionId: result.sessionId,
|
|
393
|
-
type: "classifier_completed",
|
|
394
|
-
});
|
|
395
|
-
return { reviewer: reviewer.key, output: result.value };
|
|
396
|
-
}
|
|
397
|
-
catch (error) {
|
|
398
|
-
run.error = errorMessage(error);
|
|
399
|
-
run.status = "failed";
|
|
400
|
-
await input.onClassifierProgress?.({
|
|
401
|
-
error: run.error,
|
|
402
|
-
reviewer: reviewer.key,
|
|
403
|
-
type: "classifier_failed",
|
|
404
|
-
});
|
|
405
|
-
throw error;
|
|
406
|
-
}
|
|
407
|
-
}, { signal: input.signal });
|
|
408
|
-
const threshold = majorityThreshold(reviewers.length);
|
|
409
|
-
return {
|
|
410
|
-
classified: input.checks.map((item) => {
|
|
411
|
-
const checkVotes = votes.map((vote) => {
|
|
412
|
-
const check = vote.output.checks.find((output) => output.name === item.check.name);
|
|
413
|
-
return {
|
|
414
|
-
classification: check?.classification ?? "SCOPE_IN",
|
|
415
|
-
reason: check?.reason ?? "Missing classification; treated as scope-in.",
|
|
416
|
-
reviewer: vote.reviewer,
|
|
417
|
-
};
|
|
418
|
-
});
|
|
419
|
-
const scopeIn = checkVotes.filter((vote) => vote.classification === "SCOPE_IN");
|
|
420
|
-
const scopeOut = checkVotes.filter((vote) => vote.classification === "SCOPE_OUT");
|
|
421
|
-
const classification = scopeOut.length >= threshold
|
|
422
|
-
? "SCOPE_OUT"
|
|
423
|
-
: scopeIn.length >= threshold
|
|
424
|
-
? "SCOPE_IN"
|
|
425
|
-
: undefined;
|
|
426
|
-
if (!classification) {
|
|
427
|
-
throw new Error(`CI classification did not reach majority for ${item.check.name}`);
|
|
428
|
-
}
|
|
429
|
-
const reasons = checkVotes
|
|
430
|
-
.filter((vote) => vote.classification === classification)
|
|
431
|
-
.map((vote) => `${vote.reviewer}: ${vote.reason}`);
|
|
432
|
-
return {
|
|
433
|
-
check: item.check,
|
|
434
|
-
classification,
|
|
435
|
-
reason: reasons.join("; ") || "No majority reason was provided.",
|
|
436
|
-
};
|
|
437
|
-
}),
|
|
438
|
-
classifierRuns,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
export async function waitForChecksWithClassification(input) {
|
|
442
|
-
const report = emptyReport();
|
|
443
|
-
let investigated = false;
|
|
444
|
-
const runHeadCache = new Map();
|
|
445
|
-
async function readTargetChecks() {
|
|
446
|
-
return checksForHead({
|
|
447
|
-
exec: input.exec,
|
|
448
|
-
headSha: input.headSha,
|
|
449
|
-
pr: input.pr,
|
|
450
|
-
repository: input.repository,
|
|
451
|
-
runHeadCache,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
async function assignBlockingChecks(checks) {
|
|
455
|
-
report.failed = applyCheckExclusions({
|
|
456
|
-
checks,
|
|
457
|
-
excluded: report.excluded,
|
|
458
|
-
patterns: input.repository.checks.exclude,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
if (input.wait) {
|
|
462
|
-
await input.onProgress?.("waiting for CI checks");
|
|
463
|
-
for (let attempt = 0;; attempt += 1) {
|
|
464
|
-
try {
|
|
465
|
-
await watchChecks(input.exec, input.repository, input.pr, {
|
|
466
|
-
requiredOnly: true,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
catch {
|
|
470
|
-
// gh exits non-zero for pending checks too; re-read check state below.
|
|
471
|
-
}
|
|
472
|
-
const target = await readTargetChecks();
|
|
473
|
-
const waitingForTargetHead = Boolean(input.headSha) &&
|
|
474
|
-
target.hasAnyActionCheck &&
|
|
475
|
-
!target.hasTargetActionCheck;
|
|
476
|
-
if (!waitingForTargetHead && !target.hasPending) {
|
|
477
|
-
await assignBlockingChecks(target.blocking);
|
|
478
|
-
break;
|
|
479
|
-
}
|
|
480
|
-
if (attempt >= (input.waitPollLimit ?? 60)) {
|
|
481
|
-
await assignBlockingChecks(target.blocking);
|
|
482
|
-
break;
|
|
483
|
-
}
|
|
484
|
-
await sleep(input.waitPollIntervalMs ?? 1_000);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
await assignBlockingChecks((await readTargetChecks()).blocking);
|
|
489
|
-
}
|
|
490
|
-
if (report.failed.length && !investigated) {
|
|
491
|
-
await input.onProgress?.("investigating failed CI checks");
|
|
492
|
-
}
|
|
493
|
-
if (!report.failed.length && input.wait && !investigated) {
|
|
494
|
-
await input.onProgress?.("CI checks passed");
|
|
495
|
-
}
|
|
496
|
-
for (;;) {
|
|
497
|
-
if (!report.failed.length)
|
|
498
|
-
return { ciFailureContext: "", report };
|
|
499
|
-
const cancelled = report.failed.filter(isCancelledCheck);
|
|
500
|
-
const failed = report.failed.filter((check) => !isCancelledCheck(check));
|
|
501
|
-
const scopeOut = cancelled.map(cancelledClassification);
|
|
502
|
-
let scopeIn = [];
|
|
503
|
-
let withLogs = [];
|
|
504
|
-
if (failed.length) {
|
|
505
|
-
await input.onProgress?.("fetching failed CI logs");
|
|
506
|
-
withLogs = await checksWithLogs(input.exec, input.repository, failed);
|
|
507
|
-
await input.onProgress?.("classifying CI failures");
|
|
508
|
-
const classifiedResult = await classifyChecks({
|
|
509
|
-
afterEdit: input.afterEdit,
|
|
510
|
-
checks: withLogs,
|
|
511
|
-
client: input.client,
|
|
512
|
-
directory: input.directory,
|
|
513
|
-
onClassifierProgress: input.onClassifierProgress,
|
|
514
|
-
outputDir: input.outputDir,
|
|
515
|
-
parentSessionId: input.parentSessionId,
|
|
516
|
-
pr: input.pr,
|
|
517
|
-
repairAttempts: input.repairAttempts,
|
|
518
|
-
repository: input.repository,
|
|
519
|
-
signal: input.signal,
|
|
520
|
-
});
|
|
521
|
-
const classified = classifiedResult.classified;
|
|
522
|
-
report.classifierRuns?.push(...classifiedResult.classifierRuns);
|
|
523
|
-
scopeIn = classified.filter((item) => item.classification === "SCOPE_IN");
|
|
524
|
-
scopeOut.push(...classified.filter((item) => item.classification === "SCOPE_OUT"));
|
|
525
|
-
}
|
|
526
|
-
if (scopeIn.length) {
|
|
527
|
-
await input.onProgress?.("CI failures classified as scope-in");
|
|
528
|
-
report.scopeInside.push(...scopeIn);
|
|
529
|
-
report.scopeOutsideUnresolved.push(...scopeOut);
|
|
530
|
-
return {
|
|
531
|
-
ciFailureContext: ciFailureContextForClassified(withLogs, scopeIn),
|
|
532
|
-
report,
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
const rerunnable = scopeOut.filter((item) => checkJobId(item.check));
|
|
536
|
-
const notRerunnable = scopeOut.filter((item) => !checkJobId(item.check));
|
|
537
|
-
if (notRerunnable.length ||
|
|
538
|
-
report.attempts >= input.repository.checks.retryFailedJobs) {
|
|
539
|
-
await input.onProgress?.("scope-out CI failures remain unresolved");
|
|
540
|
-
report.scopeOutsideUnresolved.push(...scopeOut);
|
|
541
|
-
return { ciFailureContext: "", report };
|
|
542
|
-
}
|
|
543
|
-
if (input.dryRun) {
|
|
544
|
-
report.dryRunRerun?.push(...rerunnable);
|
|
545
|
-
report.scopeOutsideUnresolved.push(...scopeOut);
|
|
546
|
-
await input.onProgress?.("dry-run skipping scope-out CI reruns");
|
|
547
|
-
return { ciFailureContext: "", report };
|
|
548
|
-
}
|
|
549
|
-
report.attempts += 1;
|
|
550
|
-
report.rerun.push(...rerunnable);
|
|
551
|
-
await input.onProgress?.("rerunning scope-out CI jobs");
|
|
552
|
-
await Promise.all(rerunnable.map((item) => rerunCheckJob(input.exec, input.repository, checkJobId(item.check) ?? "")));
|
|
553
|
-
try {
|
|
554
|
-
await input.onProgress?.("waiting for rerun CI checks");
|
|
555
|
-
await watchRerunRuns(input.exec, input.repository, rerunnable);
|
|
556
|
-
if (input.wait) {
|
|
557
|
-
await watchChecks(input.exec, input.repository, input.pr, {
|
|
558
|
-
requiredOnly: true,
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
catch {
|
|
563
|
-
// Re-read the PR checks below so stale failed checks are not trusted.
|
|
564
|
-
}
|
|
565
|
-
report.failed = applyCheckExclusions({
|
|
566
|
-
checks: (await readTargetChecks()).blocking,
|
|
567
|
-
excluded: report.excluded,
|
|
568
|
-
patterns: input.repository.checks.exclude,
|
|
569
|
-
});
|
|
570
|
-
if (!report.failed.length) {
|
|
571
|
-
await input.onProgress?.("rerun CI checks passed");
|
|
572
|
-
report.scopeOutsideRecovered.push(...rerunnable);
|
|
573
|
-
return { ciFailureContext: "", report };
|
|
574
|
-
}
|
|
575
|
-
if (report.failed.length) {
|
|
576
|
-
await input.onProgress?.("investigating failed CI checks");
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|