lastlight 0.7.6 → 0.7.7
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/dist/engine/github/github-app-client.d.ts +16 -0
- package/dist/engine/github/github-app-client.js +17 -0
- package/dist/engine/github/github-app-client.js.map +1 -1
- package/dist/engine/github/github.d.ts +31 -0
- package/dist/engine/github/github.js +50 -1
- package/dist/engine/github/github.js.map +1 -1
- package/dist/engine/github/review-poster.d.ts +98 -0
- package/dist/engine/github/review-poster.js +177 -0
- package/dist/engine/github/review-poster.js.map +1 -0
- package/dist/workflows/phase-executor.d.ts +24 -0
- package/dist/workflows/phase-executor.js +186 -0
- package/dist/workflows/phase-executor.js.map +1 -1
- package/dist/workflows/schema.d.ts +2 -0
- package/dist/workflows/schema.js +9 -1
- package/dist/workflows/schema.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lastlight/.claude-plugin/plugin.json +12 -3
- package/skills/pr-review/SKILL.md +12 -10
- package/skills/pr-review/references/findings-schema.md +9 -15
- package/workflows/pr-review.yaml +16 -141
package/workflows/pr-review.yaml
CHANGED
|
@@ -18,150 +18,25 @@ phases:
|
|
|
18
18
|
# validates that the change builds/runs.
|
|
19
19
|
#
|
|
20
20
|
# The agent does NOT submit the review itself — it writes structured
|
|
21
|
-
# findings
|
|
22
|
-
#
|
|
23
|
-
#
|
|
21
|
+
# findings (content only: skip? / summary / event / findings[]) to
|
|
22
|
+
# .lastlight/pr-review/findings.json. The first-class `post-review` action
|
|
23
|
+
# below reads that file and posts one formal review.
|
|
24
24
|
skills: [pr-review, code-review]
|
|
25
25
|
model: "{{models.review}}"
|
|
26
26
|
variant: "{{variants.review}}"
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
28
|
+
# First-class, in-process submission (type: post-review → PhaseExecutor.
|
|
29
|
+
# runPostReview). Reads the agent's findings.json for the review *content*
|
|
30
|
+
# (skip? / summary / event / findings[]) and supplies everything else from
|
|
31
|
+
# the harness itself — PR number (run context), base ref (baseBranch), head
|
|
32
|
+
# SHA + diff (the pre-cloned checkout) — so the AI never hand-copies metadata.
|
|
33
|
+
# It anchors each finding to a changed line, demotes off-diff findings to the
|
|
34
|
+
# body (GitHub 422s on off-diff lines), and posts ONE review via GitHubClient
|
|
35
|
+
# (App auth in prod; token + githubApiBaseUrl against the eval mock). A
|
|
36
|
+
# genuine failure — missing findings after a real review, or a GitHub error
|
|
37
|
+
# that survives the body-only retry — FAILS this phase visibly; a legitimate
|
|
38
|
+
# `skip` succeeds without posting. Idempotent on resume (no-op if a bot review
|
|
39
|
+
# already exists on the head SHA).
|
|
37
40
|
- name: post-review
|
|
38
41
|
label: Post inline review
|
|
39
|
-
type:
|
|
40
|
-
runtime: js
|
|
41
|
-
timeout_seconds: 120
|
|
42
|
-
script: |
|
|
43
|
-
import { readFileSync } from "node:fs";
|
|
44
|
-
import { execFileSync } from "node:child_process";
|
|
45
|
-
|
|
46
|
-
const api = (process.env.GITHUB_API_URL || "https://api.github.com").replace(/\/+$/, "");
|
|
47
|
-
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "";
|
|
48
|
-
const owner = "{{owner}}";
|
|
49
|
-
const repo = "{{repo}}";
|
|
50
|
-
const log = (m) => process.stderr.write("[post-review] " + m + "\n");
|
|
51
|
-
|
|
52
|
-
let doc;
|
|
53
|
-
try {
|
|
54
|
-
doc = JSON.parse(readFileSync(".lastlight/pr-review/findings.json", "utf8"));
|
|
55
|
-
} catch (e) {
|
|
56
|
-
log("no readable findings.json (" + e.message + "); nothing to post");
|
|
57
|
-
process.exit(0);
|
|
58
|
-
}
|
|
59
|
-
if (doc.skip) { log("skip: " + (doc.summary || "agent skipped review")); process.exit(0); }
|
|
60
|
-
|
|
61
|
-
const prNumber = doc.pr_number;
|
|
62
|
-
const baseRef = doc.base_ref;
|
|
63
|
-
const headSha = doc.head_sha;
|
|
64
|
-
const findings = Array.isArray(doc.findings) ? doc.findings : [];
|
|
65
|
-
if (!prNumber) { log("findings.json missing pr_number; cannot post"); process.exit(0); }
|
|
66
|
-
|
|
67
|
-
// path -> Set of "SIDE:line". "+"/context are RIGHT:newLine; "-"/context
|
|
68
|
-
// are LEFT:oldLine. Mirrors GitHub's three-dot PR diff anchoring.
|
|
69
|
-
function parseDiff(diff) {
|
|
70
|
-
const map = new Map();
|
|
71
|
-
let path = null, right = 0, left = 0, inHunk = false;
|
|
72
|
-
for (const line of diff.split("\n")) {
|
|
73
|
-
if (line.startsWith("+++ ")) {
|
|
74
|
-
const p = line.slice(4).replace(/^b\//, "");
|
|
75
|
-
path = p === "/dev/null" ? null : p;
|
|
76
|
-
if (path && !map.has(path)) map.set(path, new Set());
|
|
77
|
-
inHunk = false;
|
|
78
|
-
} else if (line.startsWith("@@")) {
|
|
79
|
-
const m = /@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
|
|
80
|
-
if (m) { left = parseInt(m[1], 10); right = parseInt(m[2], 10); inHunk = true; }
|
|
81
|
-
} else if (inHunk && path) {
|
|
82
|
-
const set = map.get(path);
|
|
83
|
-
if (line.startsWith("+")) { set.add("RIGHT:" + right); right++; }
|
|
84
|
-
else if (line.startsWith("-")) { set.add("LEFT:" + left); left++; }
|
|
85
|
-
else if (line.startsWith(" ")) { set.add("RIGHT:" + right); set.add("LEFT:" + left); right++; left++; }
|
|
86
|
-
else if (line.startsWith("\\")) { /* "" */ }
|
|
87
|
-
else { inHunk = false; }
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return map;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Commentable line set from the local checkout. Failure → null → demote all
|
|
94
|
-
// to the body (the review still posts; the judge/human still sees findings).
|
|
95
|
-
let commentable = null;
|
|
96
|
-
if (baseRef) {
|
|
97
|
-
try {
|
|
98
|
-
try { execFileSync("git", ["fetch", "origin", baseRef, "--depth", "50"], { stdio: "ignore" }); } catch {}
|
|
99
|
-
const diff = execFileSync("git", ["diff", "origin/" + baseRef + "...HEAD"], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
100
|
-
commentable = parseDiff(diff);
|
|
101
|
-
} catch (e) {
|
|
102
|
-
log("git diff failed (" + e.message + "); demoting all findings to the body");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const anchored = (f) => {
|
|
107
|
-
if (!commentable) return false;
|
|
108
|
-
const side = f.side === "LEFT" ? "LEFT" : "RIGHT";
|
|
109
|
-
const set = commentable.get(f.path);
|
|
110
|
-
if (!set || !set.has(side + ":" + f.line)) return false;
|
|
111
|
-
if (f.start_line && !set.has(side + ":" + f.start_line)) return false;
|
|
112
|
-
return true;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const inline = [], demoted = [];
|
|
116
|
-
for (const f of findings) {
|
|
117
|
-
if (f && f.path && f.line && anchored(f)) inline.push(f);
|
|
118
|
-
else if (f) demoted.push(f);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const FENCE = "```";
|
|
122
|
-
const commentBody = (f) => {
|
|
123
|
-
let b = "**[" + (f.severity || "Important") + "] " + (f.title || "") + "**\n\n" + (f.body || "");
|
|
124
|
-
if (f.suggestion) b += "\n\n" + FENCE + "suggestion\n" + f.suggestion + "\n" + FENCE;
|
|
125
|
-
return b;
|
|
126
|
-
};
|
|
127
|
-
const renderDemoted = (list) => list.length
|
|
128
|
-
? "\n\n### Additional findings\n" + list.map((f) =>
|
|
129
|
-
"- **[" + (f.severity || "Important") + "] " + (f.title || "") + "** (" + f.path + ":" + f.line + ") — " + (f.body || "")).join("\n")
|
|
130
|
-
: "";
|
|
131
|
-
const buildComments = (list) => list.map((f) => {
|
|
132
|
-
const side = f.side === "LEFT" ? "LEFT" : "RIGHT";
|
|
133
|
-
const c = { path: f.path, line: f.line, side, body: commentBody(f) };
|
|
134
|
-
if (f.start_line) { c.start_line = f.start_line; c.start_side = side; }
|
|
135
|
-
return c;
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const event = doc.event || (findings.length === 0 ? "APPROVE" : "COMMENT");
|
|
139
|
-
async function postReview(comments, extraBody) {
|
|
140
|
-
const payload = { body: (doc.summary || "") + (extraBody || ""), event, comments };
|
|
141
|
-
if (headSha) payload.commit_id = headSha;
|
|
142
|
-
return fetch(api + "/repos/" + owner + "/" + repo + "/pulls/" + prNumber + "/reviews", {
|
|
143
|
-
method: "POST",
|
|
144
|
-
headers: {
|
|
145
|
-
"Authorization": "Bearer " + token,
|
|
146
|
-
"Accept": "application/vnd.github+json",
|
|
147
|
-
"Content-Type": "application/json",
|
|
148
|
-
"User-Agent": "last-light",
|
|
149
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
150
|
-
},
|
|
151
|
-
body: JSON.stringify(payload),
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
let res = await postReview(buildComments(inline), renderDemoted(demoted));
|
|
156
|
-
if (!res.ok) {
|
|
157
|
-
const t = await res.text().catch(() => "");
|
|
158
|
-
log("inline review POST failed (" + res.status + "): " + t.slice(0, 300) + "; retrying body-only");
|
|
159
|
-
res = await postReview([], renderDemoted([...inline, ...demoted]));
|
|
160
|
-
if (!res.ok) {
|
|
161
|
-
const t2 = await res.text().catch(() => "");
|
|
162
|
-
log("body-only review POST also failed (" + res.status + "): " + t2.slice(0, 300));
|
|
163
|
-
process.exit(0);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
log("posted review: " + inline.length + " inline, " + demoted.length + " in body, event=" + event);
|
|
167
|
-
process.exit(0);
|
|
42
|
+
type: post-review
|