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.
@@ -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 deterministic `post-review`
5
- workflow phase reads this file and posts **one** formal GitHub review:
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 (missing `base_ref`, git failure), **all**
12
- findings go into the body — the review still posts, so nothing is lost.
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 never call `github_create_pull_request_review` yourself. Writing this file is
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 follow-up step posts nothing. |
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
  ```
@@ -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 to .lastlight/pr-review/findings.json (the "reference"). The
22
- # deterministic `post-review` phase below reads that file and posts one
23
- # formal review with the findings as line-anchored inline comments.
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
- # Deterministic submission step: read the agent's findings.json and post ONE
29
- # formal PR review, anchoring each finding to its diff line where possible and
30
- # demoting off-diff findings to the summary body (GitHub 422s on off-diff
31
- # lines). Terminal + always exits 0 a posting hiccup must not fail an
32
- # otherwise-good review (errors go to stderr, captured in the session log).
33
- # owner/repo come from the template context; pr_number/base_ref/head_sha come
34
- # from findings.json (the agent recorded them from github_get_pull_request).
35
- # GITHUB_TOKEN is the run's minted review-write token; in evals the harness
36
- # injects GITHUB_API_URL pointing at the mock so the review lands there.
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: script
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