lastlight 0.7.6 → 0.7.8
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/sandbox/sandbox.js +10 -5
- package/dist/sandbox/sandbox.js.map +1 -1
- package/dist/workflows/phase-executor.d.ts +24 -0
- package/dist/workflows/phase-executor.js +208 -0
- package/dist/workflows/phase-executor.js.map +1 -1
- package/dist/workflows/schema.d.ts +2 -0
- package/dist/workflows/schema.js +11 -2
- 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
|
@@ -1,33 +1,31 @@
|
|
|
1
1
|
# PR review findings schema
|
|
2
2
|
|
|
3
3
|
The `pr-review` skill writes its findings to `.lastlight/pr-review/findings.json`
|
|
4
|
-
(relative to the repo checkout — your cwd). The
|
|
5
|
-
|
|
4
|
+
(relative to the repo checkout — your cwd). The first-class `post-review` action
|
|
5
|
+
reads this file and posts **one** formal GitHub review:
|
|
6
6
|
|
|
7
7
|
- Each finding whose `line`/`side` anchors to a line that appears in the PR diff
|
|
8
8
|
becomes an **inline comment** on that exact line.
|
|
9
9
|
- Any finding whose anchor isn't in the diff is **demoted** into the review body
|
|
10
10
|
under an "Additional findings" heading (GitHub rejects comments off the diff).
|
|
11
|
-
- If the diff can't be computed (
|
|
12
|
-
|
|
11
|
+
- If the diff can't be computed (git failure), **all** findings go into the
|
|
12
|
+
body — the review still posts, so nothing is lost.
|
|
13
13
|
|
|
14
|
-
You
|
|
14
|
+
You write only the review **content** — `skip?` / `summary` / `event` /
|
|
15
|
+
`findings[]`. The PR number, base ref, head SHA and diff come from the harness's
|
|
16
|
+
own run context and the checkout, so you do **not** record any of that metadata.
|
|
17
|
+
You never call `github_create_pull_request_review` yourself; writing this file is
|
|
15
18
|
how you submit.
|
|
16
19
|
|
|
17
20
|
## Top-level object
|
|
18
21
|
|
|
19
22
|
| Field | Type | Required | Meaning |
|
|
20
23
|
|---|---|---|---|
|
|
21
|
-
| `skip` | boolean | no | `true` → you decided not to review (bot-authored / merged / already reviewed at head). The
|
|
24
|
+
| `skip` | boolean | no | `true` → you decided not to review (bot-authored / merged / already reviewed at head). The action posts nothing. |
|
|
22
25
|
| `summary` | string | yes | One or two sentences on what the PR does + your overall assessment. Becomes the review body. |
|
|
23
26
|
| `event` | string | yes | `APPROVE` \| `REQUEST_CHANGES` \| `COMMENT`. A clean PR is `APPROVE` with an empty `findings` array. |
|
|
24
|
-
| `base_ref` | string | yes | The PR base branch (e.g. `main`), from the `github_get_pull_request` call. Used to compute the diff for anchoring. |
|
|
25
|
-
| `head_sha` | string | yes | The PR head SHA, from the same call. Pins the review to the reviewed commit. |
|
|
26
27
|
| `findings` | array | yes | The surviving Critical/Important findings (may be empty). |
|
|
27
28
|
|
|
28
|
-
`base_ref` and `head_sha` are mandatory — without them the follow-up step can't
|
|
29
|
-
compute the diff and demotes every finding to the body.
|
|
30
|
-
|
|
31
29
|
## Finding object
|
|
32
30
|
|
|
33
31
|
| Field | Type | Required | Meaning |
|
|
@@ -48,8 +46,6 @@ compute the diff and demotes every finding to the body.
|
|
|
48
46
|
"skip": false,
|
|
49
47
|
"summary": "Adds a `--config` flag to the CLI and threads it into the connect path. Solid overall; one crash on the default path and one missing-await.",
|
|
50
48
|
"event": "REQUEST_CHANGES",
|
|
51
|
-
"base_ref": "main",
|
|
52
|
-
"head_sha": "9f3c1a2b7d4e5f60112233445566778899aabbcc",
|
|
53
49
|
"findings": [
|
|
54
50
|
{
|
|
55
51
|
"path": "src/cli.ts",
|
|
@@ -79,8 +75,6 @@ compute the diff and demotes every finding to the body.
|
|
|
79
75
|
"skip": false,
|
|
80
76
|
"summary": "Small, well-tested refactor of the retry helper. No correctness or regression concerns.",
|
|
81
77
|
"event": "APPROVE",
|
|
82
|
-
"base_ref": "main",
|
|
83
|
-
"head_sha": "1122334455667788990011223344556677889900",
|
|
84
78
|
"findings": []
|
|
85
79
|
}
|
|
86
80
|
```
|
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
|