@tutar/graph-engineering 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +10 -0
- package/bin/graph-engineering.mjs +60 -0
- package/lib/check.mjs +102 -0
- package/lib/constants.mjs +17 -0
- package/lib/files.mjs +83 -0
- package/lib/init.mjs +29 -0
- package/lib/manifest.mjs +31 -0
- package/lib/migrate.mjs +101 -0
- package/lib/package-assets.mjs +39 -0
- package/migrations/loop-engineering-927bd961/development/.github/loop-engineering/development-worktree.sh +32 -0
- package/migrations/loop-engineering-927bd961/development/.github/loop-engineering/github-development-ticket.mjs +246 -0
- package/migrations/loop-engineering-927bd961/development/.github/loop-engineering/stop-hook-drain.mjs +20 -0
- package/migrations/loop-engineering-927bd961/development/.github/loop-engineering/thread-record.mjs +36 -0
- package/migrations/loop-engineering-927bd961/development/.github/workflows/github-development-ticket.yml +124 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/capture-review.mjs +16 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/check-publication.mjs +23 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/codex-compatibility-profile.json +67 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/codex-compatible-executor.mjs +130 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/github-api.mjs +20 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/pr-review-case.mjs +125 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/pr-review-config.json +19 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/pr-review-contract.mjs +2 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/prepare-review.mjs +50 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/publish-review.mjs +103 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/review-result.schema.json +93 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/route-review.mjs +117 -0
- package/migrations/loop-engineering-927bd961/pr-review/.github/workflows/github-pr-review.yml +138 -0
- package/package.json +38 -0
- package/release-metadata.json +4 -0
- package/templates/development/.github/graph-engineering/development-worktree.sh +47 -0
- package/templates/development/.github/graph-engineering/github-development-ticket.mjs +246 -0
- package/templates/development/.github/graph-engineering/stop-hook-drain.mjs +20 -0
- package/templates/development/.github/graph-engineering/thread-record.mjs +39 -0
- package/templates/development/.github/workflows/github-development-ticket.yml +124 -0
- package/templates/pr-review/.github/graph-engineering/capture-review.mjs +16 -0
- package/templates/pr-review/.github/graph-engineering/check-publication.mjs +23 -0
- package/templates/pr-review/.github/graph-engineering/codex-compatibility-profile.json +67 -0
- package/templates/pr-review/.github/graph-engineering/codex-compatible-executor.mjs +130 -0
- package/templates/pr-review/.github/graph-engineering/github-api.mjs +20 -0
- package/templates/pr-review/.github/graph-engineering/pr-review-case.mjs +125 -0
- package/templates/pr-review/.github/graph-engineering/pr-review-config.json +19 -0
- package/templates/pr-review/.github/graph-engineering/pr-review-contract.mjs +2 -0
- package/templates/pr-review/.github/graph-engineering/prepare-review.mjs +50 -0
- package/templates/pr-review/.github/graph-engineering/publish-review.mjs +103 -0
- package/templates/pr-review/.github/graph-engineering/review-result.schema.json +93 -0
- package/templates/pr-review/.github/graph-engineering/route-review.mjs +117 -0
- package/templates/pr-review/.github/workflows/github-pr-review.yml +138 -0
package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/github-api.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export async function githubRequest(path, { method = "GET", body, token = required("GH_TOKEN") } = {}) {
|
|
2
|
+
const response = await fetch(`${process.env.GITHUB_API_URL ?? "https://api.github.com"}${path}`, {
|
|
3
|
+
method,
|
|
4
|
+
headers: {
|
|
5
|
+
Accept: "application/vnd.github+json",
|
|
6
|
+
Authorization: `Bearer ${token}`,
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
9
|
+
},
|
|
10
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) throw new Error(`GitHub API ${path} returned ${response.status}`);
|
|
13
|
+
return response.status === 204 ? null : response.json();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function required(name) {
|
|
17
|
+
const value = process.env[name]?.trim();
|
|
18
|
+
if (!value) throw new Error(`${name} is required`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/pr-review-case.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { CHECK_NAME } from "./check-publication.mjs";
|
|
2
|
+
|
|
3
|
+
export function formGoalPrompt({ eventPrompt, pullRequest }) {
|
|
4
|
+
return `${eventPrompt.trim()}
|
|
5
|
+
|
|
6
|
+
Use the Consumer Project's installed \`code-review\` Skill to review:
|
|
7
|
+
- repository: ${pullRequest.repository}
|
|
8
|
+
- PR #${pullRequest.number}
|
|
9
|
+
- base SHA: ${pullRequest.baseSha}
|
|
10
|
+
- head SHA: ${pullRequest.headSha}
|
|
11
|
+
- requested change: read \`pr-review-context.md\`
|
|
12
|
+
|
|
13
|
+
Report Standards and Spec as separate axes using the supplied candidate review output schema.
|
|
14
|
+
|
|
15
|
+
Always include standards, spec, and handoff. For completed review, provide both axis objects and handoff=null. For handoff, set both axes to null and provide a nonempty handoff summary. Never mix review axes with a handoff.
|
|
16
|
+
|
|
17
|
+
If the review cannot be completed from the available trusted facts, return the schema's handoff form with a concrete summary. Do not claim to publish, comment, label, push, or otherwise write to GitHub.
|
|
18
|
+
|
|
19
|
+
Completion Condition: finish only after both axes have a verdict and findings, and the result identifies the exact repository, PR, base SHA, and head SHA above.`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mapActionExecution({ actionOutcome, finalMessage }, target) {
|
|
23
|
+
if (actionOutcome === "cancelled") {
|
|
24
|
+
return { terminal: "cancelled", diagnostic: "agent-action: execution was cancelled" };
|
|
25
|
+
}
|
|
26
|
+
if (actionOutcome !== "success") {
|
|
27
|
+
return { terminal: "failed", diagnostic: `agent-action: execution ended with ${actionOutcome || "an unknown outcome"}` };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let candidate;
|
|
31
|
+
try {
|
|
32
|
+
if (!finalMessage?.trim()) throw new Error("empty final-message");
|
|
33
|
+
candidate = JSON.parse(finalMessage);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return { terminal: "failed", diagnostic: `candidate-output: ${error.message}` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
assertExactKeys(candidate, ["repository", "pullRequestNumber", "baseSha", "headSha", "standards", "spec", "handoff"]);
|
|
40
|
+
assertTrustedTarget(candidate, target);
|
|
41
|
+
if (candidate.handoff !== null) {
|
|
42
|
+
if (candidate.standards !== null || candidate.spec !== null) throw new Error("handoff must not contain review axes");
|
|
43
|
+
assertExactKeys(candidate.handoff, ["summary"]);
|
|
44
|
+
if (typeof candidate.handoff.summary !== "string" || !candidate.handoff.summary.trim()) {
|
|
45
|
+
throw new Error("handoff summary is missing");
|
|
46
|
+
}
|
|
47
|
+
return { terminal: "handoff", diagnostic: `goal-handoff: ${candidate.handoff.summary.trim()}` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { handoff, ...reviewCandidate } = candidate;
|
|
51
|
+
assertCandidateReview(reviewCandidate, target);
|
|
52
|
+
return {
|
|
53
|
+
terminal: "completed",
|
|
54
|
+
review: {
|
|
55
|
+
...reviewCandidate,
|
|
56
|
+
runtime: {
|
|
57
|
+
terminal: "completed",
|
|
58
|
+
summary: "tutar/codex-action completed and returned a validated final-message.",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { terminal: "failed", diagnostic: `candidate-output: ${error.message}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function assertReviewResult(review, target) {
|
|
68
|
+
assertExactKeys(review, ["repository", "pullRequestNumber", "baseSha", "headSha", "standards", "spec", "runtime"]);
|
|
69
|
+
assertTrustedTarget(review, target);
|
|
70
|
+
for (const axis of ["standards", "spec"]) {
|
|
71
|
+
if (!review[axis] || typeof review[axis] !== "object") {
|
|
72
|
+
throw new Error(`review output is missing the ${axis} axis`);
|
|
73
|
+
}
|
|
74
|
+
assertExactKeys(review[axis], ["verdict", "findings"]);
|
|
75
|
+
if (!["pass", "fail"].includes(review[axis].verdict)
|
|
76
|
+
|| !Array.isArray(review[axis].findings)
|
|
77
|
+
|| !review[axis].findings.every((finding) => typeof finding === "string" && finding.trim())) {
|
|
78
|
+
throw new Error(`review output is missing the ${axis} axis`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!review.runtime || review.runtime.terminal !== "completed" || !review.runtime.summary) {
|
|
82
|
+
throw new Error("Candidate review output Runtime terminal is not completed");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function assertCandidateReview(candidate, target) {
|
|
87
|
+
assertExactKeys(candidate, ["repository", "pullRequestNumber", "baseSha", "headSha", "standards", "spec"]);
|
|
88
|
+
assertReviewResult({
|
|
89
|
+
...candidate,
|
|
90
|
+
runtime: { terminal: "completed", summary: "validated" },
|
|
91
|
+
}, target);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertTrustedTarget(candidate, target) {
|
|
95
|
+
if (
|
|
96
|
+
candidate.repository !== target.repository
|
|
97
|
+
|| candidate.pullRequestNumber !== target.number
|
|
98
|
+
|| candidate.baseSha !== target.baseSha
|
|
99
|
+
|| candidate.headSha !== target.headSha
|
|
100
|
+
) throw new Error("target does not match trusted GitHub facts");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertExactKeys(value, expected) {
|
|
104
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected an object");
|
|
105
|
+
const actual = Object.keys(value).sort();
|
|
106
|
+
const wanted = [...expected].sort();
|
|
107
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
108
|
+
throw new Error(`unexpected fields: expected ${wanted.join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildCheck(review, target, settings = { name: CHECK_NAME, title: "PR Review" }) {
|
|
113
|
+
assertReviewResult(review, target);
|
|
114
|
+
return {
|
|
115
|
+
name: settings.name,
|
|
116
|
+
title: settings.title,
|
|
117
|
+
repository: target.repository,
|
|
118
|
+
pullRequestNumber: target.number,
|
|
119
|
+
headSha: target.headSha,
|
|
120
|
+
conclusion: review.standards.verdict === "pass" && review.spec.verdict === "pass" ? "success" : "failure",
|
|
121
|
+
standards: review.standards,
|
|
122
|
+
spec: review.spec,
|
|
123
|
+
runtime: review.runtime,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"definition": "github-pr-review/current",
|
|
3
|
+
"profile": "github-pr-review/codex/current",
|
|
4
|
+
"events": {
|
|
5
|
+
"pullRequestActions": ["opened", "reopened", "synchronize", "ready_for_review"],
|
|
6
|
+
"manualDispatch": true,
|
|
7
|
+
"includeDrafts": false
|
|
8
|
+
},
|
|
9
|
+
"eventPrompt": "Review this change against the repository rules and its requested behavior.",
|
|
10
|
+
"codex": {
|
|
11
|
+
"model": "",
|
|
12
|
+
"effort": "",
|
|
13
|
+
"safetyStrategy": "read-only"
|
|
14
|
+
},
|
|
15
|
+
"check": {
|
|
16
|
+
"name": "Loop Engineering / PR Review",
|
|
17
|
+
"title": "PR Review"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/prepare-review.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { githubRequest } from "./github-api.mjs";
|
|
4
|
+
import { formGoalPrompt } from "./pr-review-case.mjs";
|
|
5
|
+
|
|
6
|
+
const repository = required("GITHUB_REPOSITORY");
|
|
7
|
+
const pullRequestNumber = Number.parseInt(required("PULL_REQUEST_NUMBER"), 10);
|
|
8
|
+
if (!Number.isInteger(pullRequestNumber) || pullRequestNumber < 1) throw new Error("PULL_REQUEST_NUMBER must be positive");
|
|
9
|
+
|
|
10
|
+
const pull = await githubRequest(`/repos/${repository}/pulls/${pullRequestNumber}`);
|
|
11
|
+
if (pull.head?.repo?.full_name !== repository) throw new Error("Only same-repository pull requests are supported");
|
|
12
|
+
const checks = await githubRequest(`/repos/${repository}/commits/${pull.head.sha}/check-runs`);
|
|
13
|
+
const target = {
|
|
14
|
+
repository,
|
|
15
|
+
number: pullRequestNumber,
|
|
16
|
+
baseSha: pull.base.sha,
|
|
17
|
+
headSha: pull.head.sha,
|
|
18
|
+
baseRef: pull.base.ref,
|
|
19
|
+
headRef: pull.head.ref,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
await writeFile("pr-review-target.json", `${JSON.stringify(target, null, 2)}\n`);
|
|
23
|
+
await writeFile("pr-review-context.md", [
|
|
24
|
+
`# PR #${pullRequestNumber}: ${pull.title}`,
|
|
25
|
+
"",
|
|
26
|
+
pull.body || "No pull request description was provided.",
|
|
27
|
+
"",
|
|
28
|
+
`Base branch: ${target.baseRef}`,
|
|
29
|
+
`Head branch: ${target.headRef}`,
|
|
30
|
+
`Existing Checks on head: ${(checks.check_runs ?? []).map((check) => `${check.name}=${check.conclusion ?? check.status}`).join(", ") || "none"}`,
|
|
31
|
+
"",
|
|
32
|
+
].join("\n"));
|
|
33
|
+
await writeFile("pr-review-goal.md", `${formGoalPrompt({
|
|
34
|
+
eventPrompt: required("EVENT_PROMPT"),
|
|
35
|
+
pullRequest: target,
|
|
36
|
+
})}\n`);
|
|
37
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
38
|
+
await writeFile(process.env.GITHUB_OUTPUT, [
|
|
39
|
+
`base_sha=${target.baseSha}`,
|
|
40
|
+
`head_sha=${target.headSha}`,
|
|
41
|
+
`target_json=${JSON.stringify(target)}`,
|
|
42
|
+
"",
|
|
43
|
+
].join("\n"), { flag: "a" });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function required(name) {
|
|
47
|
+
const value = process.env[name]?.trim();
|
|
48
|
+
if (!value) throw new Error(`${name} is required`);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/publish-review.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { githubRequest } from "./github-api.mjs";
|
|
4
|
+
import { loadBundledCompatibility } from "./codex-compatible-executor.mjs";
|
|
5
|
+
import { planCheckPublication } from "./check-publication.mjs";
|
|
6
|
+
import { buildCheck } from "./pr-review-case.mjs";
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
await publish();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
await writeDiagnostic(error.message);
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function publish() {
|
|
16
|
+
const compatible = await loadBundledCompatibility();
|
|
17
|
+
const upstreamResult = process.env.UPSTREAM_RESULT ?? "success";
|
|
18
|
+
if (upstreamResult !== "success") {
|
|
19
|
+
const execution = await readOptionalJson("pr-review-artifact/review-execution.json");
|
|
20
|
+
throw new Error(execution?.diagnostic ?? `upstream-review: review job ended with ${upstreamResult}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const target = JSON.parse(await readFile("pr-review-artifact/pr-review-target.json", "utf8"));
|
|
24
|
+
const review = JSON.parse(await readFile("pr-review-artifact/review-result.json", "utf8"));
|
|
25
|
+
const check = buildCheck(review, target, compatible.check);
|
|
26
|
+
|
|
27
|
+
const current = await githubRequest(`/repos/${target.repository}/pulls/${target.number}`);
|
|
28
|
+
if (current.head.sha !== target.headSha) throw new Error("stale-target: PR head changed before publication");
|
|
29
|
+
const checks = await readCheckHistory(target, check.name);
|
|
30
|
+
const publication = planCheckPublication({ target, existingChecks: checks, checkName: check.name });
|
|
31
|
+
|
|
32
|
+
// History queries can take multiple requests. Recheck freshness before writing.
|
|
33
|
+
const beforeWrite = await githubRequest(`/repos/${target.repository}/pulls/${target.number}`);
|
|
34
|
+
if (beforeWrite.head.sha !== target.headSha) throw new Error("stale-target: PR head changed before publication");
|
|
35
|
+
|
|
36
|
+
// Nonsecret audit signal: never print tokens, headers or Agent contents.
|
|
37
|
+
console.log(JSON.stringify({
|
|
38
|
+
publicationMethod: publication.method,
|
|
39
|
+
externalId: publication.externalId,
|
|
40
|
+
existingCheckId: publication.method === "PATCH" ? Number(publication.path.split("/").at(-1)) : null,
|
|
41
|
+
historyCount: checks.length,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const summary = [
|
|
45
|
+
`Standards: ${review.standards.verdict}`,
|
|
46
|
+
...review.standards.findings.map((finding) => `- ${finding}`),
|
|
47
|
+
`Spec: ${review.spec.verdict}`,
|
|
48
|
+
...review.spec.findings.map((finding) => `- ${finding}`),
|
|
49
|
+
`Runtime: ${review.runtime.summary}`,
|
|
50
|
+
].join("\n");
|
|
51
|
+
const body = {
|
|
52
|
+
name: check.name,
|
|
53
|
+
external_id: publication.externalId,
|
|
54
|
+
status: "completed",
|
|
55
|
+
conclusion: check.conclusion,
|
|
56
|
+
output: { title: check.title, summary },
|
|
57
|
+
};
|
|
58
|
+
if (publication.method === "POST") body.head_sha = check.headSha;
|
|
59
|
+
|
|
60
|
+
await githubRequest(publication.path, { method: publication.method, body });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readCheckHistory(target, name) {
|
|
64
|
+
const history = [];
|
|
65
|
+
const ids = new Set();
|
|
66
|
+
for (let page = 1; page <= 100; page += 1) {
|
|
67
|
+
const response = await githubRequest(
|
|
68
|
+
`/repos/${target.repository}/commits/${target.headSha}/check-runs?check_name=${encodeURIComponent(name)}&filter=all&per_page=100&page=${page}`,
|
|
69
|
+
);
|
|
70
|
+
if (!Array.isArray(response.check_runs) || response.check_runs.length > 100) {
|
|
71
|
+
throw new Error("check-history: malformed Check listing");
|
|
72
|
+
}
|
|
73
|
+
for (const check of response.check_runs) {
|
|
74
|
+
if (!Number.isSafeInteger(check?.id) || check.id <= 0 || ids.has(check.id)) {
|
|
75
|
+
throw new Error("check-history: invalid or repeated Check ID across pages");
|
|
76
|
+
}
|
|
77
|
+
if (typeof check.name !== "string" || !check.name
|
|
78
|
+
|| check.head_sha !== target.headSha
|
|
79
|
+
|| !(check.external_id === null || typeof check.external_id === "string")) {
|
|
80
|
+
throw new Error("check-history: incomplete or mismatched Check identity fields");
|
|
81
|
+
}
|
|
82
|
+
ids.add(check.id);
|
|
83
|
+
history.push(check);
|
|
84
|
+
}
|
|
85
|
+
if (response.check_runs.length < 100) return history;
|
|
86
|
+
}
|
|
87
|
+
// Never interpret incomplete history as absence or create a new Check.
|
|
88
|
+
throw new Error("check-history: pagination limit reached before complete listing");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readOptionalJson(path) {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function writeDiagnostic(message) {
|
|
100
|
+
const text = `## PR Review diagnostic\n\n${message}\n\nNo Check Run was published.\n`;
|
|
101
|
+
await writeFile("review-diagnostic.md", text);
|
|
102
|
+
if (process.env.GITHUB_STEP_SUMMARY) await writeFile(process.env.GITHUB_STEP_SUMMARY, text, { flag: "a" });
|
|
103
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"required": [
|
|
6
|
+
"repository",
|
|
7
|
+
"pullRequestNumber",
|
|
8
|
+
"baseSha",
|
|
9
|
+
"headSha",
|
|
10
|
+
"standards",
|
|
11
|
+
"spec",
|
|
12
|
+
"handoff"
|
|
13
|
+
],
|
|
14
|
+
"properties": {
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1
|
|
18
|
+
},
|
|
19
|
+
"pullRequestNumber": {
|
|
20
|
+
"type": "integer",
|
|
21
|
+
"minimum": 1
|
|
22
|
+
},
|
|
23
|
+
"baseSha": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"minLength": 1
|
|
26
|
+
},
|
|
27
|
+
"headSha": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"minLength": 1
|
|
30
|
+
},
|
|
31
|
+
"standards": {
|
|
32
|
+
"anyOf": [
|
|
33
|
+
{
|
|
34
|
+
"$ref": "#/$defs/axis"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "null"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"spec": {
|
|
42
|
+
"anyOf": [
|
|
43
|
+
{
|
|
44
|
+
"$ref": "#/$defs/axis"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "null"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"handoff": {
|
|
52
|
+
"type": [
|
|
53
|
+
"object",
|
|
54
|
+
"null"
|
|
55
|
+
],
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"required": [
|
|
58
|
+
"summary"
|
|
59
|
+
],
|
|
60
|
+
"properties": {
|
|
61
|
+
"summary": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"minLength": 1
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"$defs": {
|
|
69
|
+
"axis": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"required": [
|
|
73
|
+
"verdict",
|
|
74
|
+
"findings"
|
|
75
|
+
],
|
|
76
|
+
"properties": {
|
|
77
|
+
"verdict": {
|
|
78
|
+
"enum": [
|
|
79
|
+
"pass",
|
|
80
|
+
"fail"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"findings": {
|
|
84
|
+
"type": "array",
|
|
85
|
+
"items": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"minLength": 1
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/migrations/loop-engineering-927bd961/pr-review/.github/loop-engineering/route-review.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { readBundledContracts, resolveCompatibleExecution } from "./codex-compatible-executor.mjs";
|
|
6
|
+
import { githubRequest } from "./github-api.mjs";
|
|
7
|
+
import { SUPPORTED_ACTIONS } from "./pr-review-contract.mjs";
|
|
8
|
+
|
|
9
|
+
export { SUPPORTED_ACTIONS } from "./pr-review-contract.mjs";
|
|
10
|
+
|
|
11
|
+
// This deliberately repeats the Workflow trigger as a fail-closed runtime check.
|
|
12
|
+
// The template contract test requires exact parity between both boundaries.
|
|
13
|
+
export function routeReviewEvent({
|
|
14
|
+
eventName,
|
|
15
|
+
event,
|
|
16
|
+
eventConfiguration = { pullRequestActions: [...SUPPORTED_ACTIONS], manualDispatch: true, includeDrafts: false },
|
|
17
|
+
}) {
|
|
18
|
+
if (eventName === "workflow_dispatch") {
|
|
19
|
+
if (!eventConfiguration.manualDispatch) return { shouldStart: false, pullRequestNumber: null, reason: "manual-dispatch-disabled" };
|
|
20
|
+
const pullRequestNumber = Number.parseInt(event.inputs?.pull_request_number, 10);
|
|
21
|
+
if (!Number.isInteger(pullRequestNumber) || pullRequestNumber < 1) {
|
|
22
|
+
return { shouldStart: false, pullRequestNumber: null, reason: "invalid-manual-input" };
|
|
23
|
+
}
|
|
24
|
+
return { shouldStart: true, pullRequestNumber, reason: "manual-dispatch" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (eventName !== "pull_request" || !SUPPORTED_ACTIONS.has(event.action) || !eventConfiguration.pullRequestActions.includes(event.action)) {
|
|
28
|
+
return { shouldStart: false, pullRequestNumber: null, reason: "unsupported-event" };
|
|
29
|
+
}
|
|
30
|
+
const pullRequestNumber = event.pull_request.number;
|
|
31
|
+
if (event.pull_request.draft && !eventConfiguration.includeDrafts) {
|
|
32
|
+
return { shouldStart: false, pullRequestNumber, reason: "draft-pull-request" };
|
|
33
|
+
}
|
|
34
|
+
return { shouldStart: true, pullRequestNumber, reason: "supported-pull-request-event" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function planReviewRun({ eventName, event, config, profile }) {
|
|
38
|
+
try {
|
|
39
|
+
const compatible = resolveCompatibleExecution({ config, profile });
|
|
40
|
+
return {
|
|
41
|
+
configurationStatus: "compatible",
|
|
42
|
+
...routeReviewEvent({ eventName, event, eventConfiguration: compatible.events }),
|
|
43
|
+
compatible,
|
|
44
|
+
};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return {
|
|
47
|
+
configurationStatus: "handoff",
|
|
48
|
+
shouldStart: false,
|
|
49
|
+
pullRequestNumber: null,
|
|
50
|
+
reason: "configuration-handoff",
|
|
51
|
+
diagnostic: error.message,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function enforceManualPullRequestState(planned, { repository, request = githubRequest } = {}) {
|
|
57
|
+
if (!planned.shouldStart || planned.reason !== "manual-dispatch") return planned;
|
|
58
|
+
try {
|
|
59
|
+
const pullRequest = await request(`/repos/${repository}/pulls/${planned.pullRequestNumber}`);
|
|
60
|
+
if (pullRequest.state !== "open") return { ...planned, shouldStart: false, reason: "manual-pull-request-not-open" };
|
|
61
|
+
if (pullRequest.draft && !planned.compatible.events.includeDrafts) return { ...planned, shouldStart: false, reason: "draft-pull-request" };
|
|
62
|
+
return planned;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return { ...planned, shouldStart: false, reason: "manual-pull-request-unreadable", diagnostic: error.message };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
const event = JSON.parse(await readFile(required("GITHUB_EVENT_PATH"), "utf8"));
|
|
70
|
+
let contracts;
|
|
71
|
+
try {
|
|
72
|
+
contracts = await readBundledContracts();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const diagnostic = `configuration-handoff: ${error.message}`;
|
|
75
|
+
await writeOutputs({ configurationStatus: "handoff", shouldStart: false, pullRequestNumber: null, reason: "configuration-handoff", diagnostic });
|
|
76
|
+
if (process.env.GITHUB_STEP_SUMMARY) await writeFile(process.env.GITHUB_STEP_SUMMARY, `## PR Review handoff\n\n${diagnostic}\n`, { flag: "a" });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const eventName = required("GITHUB_EVENT_NAME");
|
|
80
|
+
let planned = planReviewRun({ eventName, event, ...contracts });
|
|
81
|
+
if (planned.configurationStatus === "handoff") {
|
|
82
|
+
await writeOutputs(planned);
|
|
83
|
+
if (process.env.GITHUB_STEP_SUMMARY) await writeFile(process.env.GITHUB_STEP_SUMMARY, `## PR Review handoff\n\n${planned.diagnostic}\n`, { flag: "a" });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (eventName === "workflow_dispatch") planned = await enforceManualPullRequestState(planned, { repository: required("GITHUB_REPOSITORY") });
|
|
87
|
+
const manualPrompt = eventName === "workflow_dispatch" ? event.inputs?.event_prompt?.trim() : "";
|
|
88
|
+
await writeOutputs({
|
|
89
|
+
shouldStart: planned.shouldStart,
|
|
90
|
+
pullRequestNumber: planned.pullRequestNumber,
|
|
91
|
+
reason: planned.reason,
|
|
92
|
+
eventPrompt: manualPrompt || planned.compatible.eventPrompt,
|
|
93
|
+
codexModel: planned.compatible.action.model,
|
|
94
|
+
codexEffort: planned.compatible.action.effort,
|
|
95
|
+
safetyStrategy: planned.compatible.action.safetyStrategy,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function writeOutputs(values) {
|
|
100
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
101
|
+
const lines = [];
|
|
102
|
+
for (const [key, value] of Object.entries(values)) {
|
|
103
|
+
const outputKey = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
104
|
+
const delimiter = `loop_${randomUUID()}`;
|
|
105
|
+
lines.push(`${outputKey}<<${delimiter}`, String(value ?? ""), delimiter);
|
|
106
|
+
}
|
|
107
|
+
await writeFile(process.env.GITHUB_OUTPUT, `${lines.join("\n")}\n`, { flag: "a" });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function required(name) {
|
|
112
|
+
const value = process.env[name]?.trim();
|
|
113
|
+
if (!value) throw new Error(`${name} is required`);
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await main();
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
name: Loop Engineering PR Review
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, reopened, synchronize, ready_for_review]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
pull_request_number:
|
|
9
|
+
description: Pull request number to review
|
|
10
|
+
required: true
|
|
11
|
+
type: number
|
|
12
|
+
event_prompt:
|
|
13
|
+
description: Review objective placed at the start of the Goal Prompt
|
|
14
|
+
required: false
|
|
15
|
+
default: Review this change against the repository rules and its requested behavior.
|
|
16
|
+
type: string
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: ${{ github.repository }}:pull-request:${{ github.event.pull_request.number || inputs.pull_request_number }}
|
|
20
|
+
cancel-in-progress: false
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
route:
|
|
24
|
+
runs-on: ubuntu-24.04
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
pull-requests: read
|
|
28
|
+
outputs:
|
|
29
|
+
should_start: ${{ steps.route.outputs.should_start }}
|
|
30
|
+
pull_request_number: ${{ steps.route.outputs.pull_request_number }}
|
|
31
|
+
event_prompt: ${{ steps.route.outputs.event_prompt }}
|
|
32
|
+
codex_model: ${{ steps.route.outputs.codex_model }}
|
|
33
|
+
codex_effort: ${{ steps.route.outputs.codex_effort }}
|
|
34
|
+
safety_strategy: ${{ steps.route.outputs.safety_strategy }}
|
|
35
|
+
steps:
|
|
36
|
+
- name: Check out the trusted router
|
|
37
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
38
|
+
with:
|
|
39
|
+
ref: ${{ github.event.repository.default_branch }}
|
|
40
|
+
persist-credentials: false
|
|
41
|
+
- name: Route the GitHub Event
|
|
42
|
+
id: route
|
|
43
|
+
env:
|
|
44
|
+
GH_TOKEN: ${{ github.token }}
|
|
45
|
+
run: node .github/loop-engineering/route-review.mjs
|
|
46
|
+
|
|
47
|
+
review:
|
|
48
|
+
needs: route
|
|
49
|
+
if: >-
|
|
50
|
+
needs.route.outputs.should_start == 'true' &&
|
|
51
|
+
(github.event_name != 'workflow_dispatch' ||
|
|
52
|
+
github.ref == format('refs/heads/{0}', github.event.repository.default_branch))
|
|
53
|
+
runs-on: [self-hosted, Linux, X64, codex]
|
|
54
|
+
permissions:
|
|
55
|
+
contents: read
|
|
56
|
+
pull-requests: read
|
|
57
|
+
checks: read
|
|
58
|
+
steps:
|
|
59
|
+
- name: Check out the trusted review control plane
|
|
60
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
61
|
+
with:
|
|
62
|
+
ref: ${{ github.event.repository.default_branch }}
|
|
63
|
+
path: .loop-engineering-trusted
|
|
64
|
+
persist-credentials: false
|
|
65
|
+
- name: Read trusted PR facts and form the Goal Prompt
|
|
66
|
+
id: target
|
|
67
|
+
env:
|
|
68
|
+
GH_TOKEN: ${{ github.token }}
|
|
69
|
+
PULL_REQUEST_NUMBER: ${{ needs.route.outputs.pull_request_number }}
|
|
70
|
+
EVENT_PROMPT: ${{ needs.route.outputs.event_prompt }}
|
|
71
|
+
run: node .loop-engineering-trusted/.github/loop-engineering/prepare-review.mjs
|
|
72
|
+
- name: Check out the trusted PR target
|
|
73
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
74
|
+
with:
|
|
75
|
+
ref: ${{ steps.target.outputs.head_sha }}
|
|
76
|
+
path: review-workspace
|
|
77
|
+
fetch-depth: 0
|
|
78
|
+
persist-credentials: false
|
|
79
|
+
- name: Run the Codex Compatible Executor
|
|
80
|
+
id: codex
|
|
81
|
+
continue-on-error: true
|
|
82
|
+
uses: tutar/codex-action@f33581290086e62dc34d420a7f1862477fc2b503 # maintained runner-login patch
|
|
83
|
+
with:
|
|
84
|
+
prompt-file: ${{ github.workspace }}/pr-review-goal.md
|
|
85
|
+
output-schema-file: ${{ github.workspace }}/.loop-engineering-trusted/.github/loop-engineering/review-result.schema.json
|
|
86
|
+
working-directory: ${{ github.workspace }}/review-workspace
|
|
87
|
+
codex-version: 0.153.4
|
|
88
|
+
model: ${{ needs.route.outputs.codex_model }}
|
|
89
|
+
effort: ${{ needs.route.outputs.codex_effort }}
|
|
90
|
+
safety-strategy: ${{ needs.route.outputs.safety_strategy }}
|
|
91
|
+
- name: Map the Action terminal and final-message
|
|
92
|
+
if: always()
|
|
93
|
+
env:
|
|
94
|
+
ACTION_OUTCOME: ${{ steps.codex.outcome }}
|
|
95
|
+
FINAL_MESSAGE: ${{ steps.codex.outputs.final-message }}
|
|
96
|
+
TRUSTED_TARGET: ${{ steps.target.outputs.target_json }}
|
|
97
|
+
run: node .loop-engineering-trusted/.github/loop-engineering/capture-review.mjs
|
|
98
|
+
- name: Preserve the trusted target and candidate Review Result
|
|
99
|
+
if: always()
|
|
100
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
101
|
+
with:
|
|
102
|
+
name: pr-review-result
|
|
103
|
+
path: |
|
|
104
|
+
pr-review-target.json
|
|
105
|
+
pr-review-context.md
|
|
106
|
+
review-execution.json
|
|
107
|
+
review-result.json
|
|
108
|
+
if-no-files-found: error
|
|
109
|
+
|
|
110
|
+
publish:
|
|
111
|
+
needs: [route, review]
|
|
112
|
+
if: >-
|
|
113
|
+
always() &&
|
|
114
|
+
needs.route.result == 'success' &&
|
|
115
|
+
needs.route.outputs.should_start == 'true'
|
|
116
|
+
runs-on: ubuntu-24.04
|
|
117
|
+
permissions:
|
|
118
|
+
contents: read
|
|
119
|
+
pull-requests: read
|
|
120
|
+
checks: write
|
|
121
|
+
steps:
|
|
122
|
+
- name: Check out the trusted publisher
|
|
123
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
124
|
+
with:
|
|
125
|
+
ref: ${{ github.event.repository.default_branch }}
|
|
126
|
+
path: .loop-engineering-publisher
|
|
127
|
+
persist-credentials: false
|
|
128
|
+
- name: Download the candidate Review Result
|
|
129
|
+
continue-on-error: true
|
|
130
|
+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
131
|
+
with:
|
|
132
|
+
name: pr-review-result
|
|
133
|
+
path: pr-review-artifact
|
|
134
|
+
- name: Validate and publish the Check Run
|
|
135
|
+
env:
|
|
136
|
+
GH_TOKEN: ${{ github.token }}
|
|
137
|
+
UPSTREAM_RESULT: ${{ needs.review.result }}
|
|
138
|
+
run: node .loop-engineering-publisher/.github/loop-engineering/publish-review.mjs
|