hq-jr 0.1.2 → 0.1.4
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.md +20 -7
- package/dist/index.d.ts +5 -0
- package/dist/index.js +319 -84
- package/dist/index.js.map +1 -1
- package/dist/services/action-ref-verifier.d.ts +41 -0
- package/dist/services/action-ref-verifier.js +142 -0
- package/dist/services/action-ref-verifier.js.map +1 -0
- package/dist/services/ai.d.ts +40 -0
- package/dist/services/ai.js +165 -0
- package/dist/services/ai.js.map +1 -1
- package/dist/services/db.d.ts +41 -0
- package/dist/services/db.js +139 -0
- package/dist/services/db.js.map +1 -1
- package/dist/services/review-memory.d.ts +7 -2
- package/dist/services/review-memory.js +122 -45
- package/dist/services/review-memory.js.map +1 -1
- package/dist/services/sandbox-runner.d.ts +30 -2
- package/dist/services/sandbox-runner.js +167 -30
- package/dist/services/sandbox-runner.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
### Why hq-jr
|
|
17
17
|
|
|
18
|
-
- **Sandbox proof.** Suspicious findings can be
|
|
18
|
+
- **Sandbox proof.** Suspicious findings can be dispatched to an isolated Antigravity container; hq-jr polls for a real verdict (not success-on-dispatch). Tier 3 remains experimental until validated in your environment.
|
|
19
19
|
- **Mention manners.** Peer-to-peer PR chat is ignored until someone addresses `@hq-jr`.
|
|
20
20
|
- **Self-hosted on Vertex.** Runs in your account. Diffs go to your GCP project via Vertex AI, not a third-party review SaaS.
|
|
21
21
|
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
|
|
32
32
|
Junior sits on your webhook path. Humans skim diffs and type LGTM. Junior does not skim.
|
|
33
33
|
|
|
34
|
-
It filters noise, scrubs credentials, then runs a two-step Gemini Flash path: triage (narrow schema) then deep review. Both steps use High Thinking; triage is cheaper because the prompt and JSON schema are smaller. SQLite WAL
|
|
34
|
+
It filters noise, scrubs credentials, then runs a two-step Gemini Flash path: triage (narrow schema) then deep review. Both steps use High Thinking; triage is cheaper because the prompt and JSON schema are smaller. SQLite WAL stores prior findings across pushes (GitHub hq-jr reviews still supplement). When a break looks real, experimental Tier 3 dispatches Antigravity and polls for a reproduction verdict. Tag `@hq-jr fix` (explicit command only) if you want a remediation branch.
|
|
35
35
|
|
|
36
36
|
### Generic AI review bots vs hq-jr
|
|
37
37
|
|
|
38
38
|
| Dimension | Generic AI review bots | hq-jr |
|
|
39
39
|
| :--- | :--- | :--- |
|
|
40
|
-
| **Verification** | Guesses from completions. |
|
|
41
|
-
| **Memory** | Stateless; repeats nags after force-push. | SQLite WAL
|
|
40
|
+
| **Verification** | Guesses from completions. | Polls Tier 3 sandbox for a real verdict before concluding the check (experimental). |
|
|
41
|
+
| **Memory** | Stateless; repeats nags after force-push. | SQLite WAL stores findings; GitHub reviews used as supplement. |
|
|
42
42
|
| **Noise** | Reviews lockfiles and vendor junk. | Heuristic filter + Tier 1 triage skip low-risk diffs. |
|
|
43
43
|
| **Etiquette** | Hijacks human threads. | Speaks when `@hq-jr` is mentioned (or already in-thread). |
|
|
44
44
|
| **Hosting** | Third-party SaaS sees your diffs. | Self-hosted; Vertex AI in *your* GCP project. |
|
|
@@ -93,7 +93,8 @@ Defaults match [`src/config.ts`](src/config.ts): Tier 1 and Tier 2 use `gemini-3
|
|
|
93
93
|
- Line-anchored comments with suggestion blocks; HTTP 422 fallback if lines drift.
|
|
94
94
|
4. **Tier 3: Sandbox Verification (experimental)** (CLI & runner):
|
|
95
95
|
- Antigravity agent (`HQ_JR_AGENT_TIER3`, default `antigravity-preview-05-2026`) for empirical reproduction in isolated containers via `src/cli.ts` / [`src/services/sandbox-runner.ts`](src/services/sandbox-runner.ts).
|
|
96
|
-
-
|
|
96
|
+
- After dispatch the Check Run stays `in_progress` while `pollSandboxInteraction` waits for a verdict; conclusion is success/failure/neutral from that result (not success-on-dispatch).
|
|
97
|
+
- Treat as experimental until you validate live Antigravity in your environment.
|
|
97
98
|
|
|
98
99
|
---
|
|
99
100
|
|
|
@@ -103,7 +104,9 @@ Defaults match [`src/config.ts`](src/config.ts): Tier 1 and Tier 2 use `gemini-3
|
|
|
103
104
|
- **GitHub Check Runs**: Status via `hq-jr AI Code Review`.
|
|
104
105
|
- **Interactive PR Chat**: Mention `@hq-jr` to ask questions. Human-to-human threads stay quiet.
|
|
105
106
|
- **On-Demand Review**: `@hq-jr review`, `@hq-jr audit`, or `@hq-jr scan`.
|
|
106
|
-
- **Remediation with RBAC**: `@hq-jr fix` / `@hq-jr patch`
|
|
107
|
+
- **Remediation with RBAC**: Explicit `@hq-jr fix` / `@hq-jr patch` / `@hq-jr remediate` (optional `and merge`) only; loose wording like "how to fix naming" does not trigger. Synthesizes branch `hq-jr/fix-pr-<id>`. Requires collaborator `admin` or `write`.
|
|
108
|
+
- **Commit repro test**: Check action **Commit Repro Test** or `@hq-jr commit-repro` commits the synthesized test from the sandbox check summary.
|
|
109
|
+
- **Re-run sandbox**: Check action **Re-run Sandbox** re-dispatches Tier 3 with prior probes when available.
|
|
107
110
|
|
|
108
111
|
---
|
|
109
112
|
|
|
@@ -175,12 +178,22 @@ npm run dev # watch mode
|
|
|
175
178
|
|
|
176
179
|
### Docker
|
|
177
180
|
|
|
181
|
+
Published image (App daemon):
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
docker pull ghcr.io/adi-il/hq-jr:0.1.2
|
|
185
|
+
# or: docker pull ghcr.io/adi-il/hq-jr:latest
|
|
186
|
+
docker run --rm -p 3000:3000 --env-file .env ghcr.io/adi-il/hq-jr:0.1.2
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Build from source:
|
|
190
|
+
|
|
178
191
|
```bash
|
|
179
192
|
docker build -t hq-jr:local .
|
|
180
193
|
docker run --rm -p 3000:3000 --env-file .env hq-jr:local
|
|
181
194
|
```
|
|
182
195
|
|
|
183
|
-
For Cloud Run and systemd options, see [docs/05-deployment-and-operations.md](docs/05-deployment-and-operations.md).
|
|
196
|
+
Image runs Probot under `tini` (PID 1) with `probot` on `PATH`. For Cloud Run and systemd options, see [docs/05-deployment-and-operations.md](docs/05-deployment-and-operations.md).
|
|
184
197
|
|
|
185
198
|
---
|
|
186
199
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { Probot } from "probot";
|
|
2
|
+
/** Remediation only on explicit @hq-jr fix|patch|remediate (optional and merge). */
|
|
3
|
+
/** True when path is a file under tests/ (not bare tests, not traversal). */
|
|
4
|
+
export declare function isSafeReproTestPath(path: string | undefined | null): boolean;
|
|
5
|
+
export declare const REMEDIATE_COMMAND_RE: RegExp;
|
|
6
|
+
export declare const COMMIT_REPRO_COMMAND_RE: RegExp;
|
|
2
7
|
declare const _default: (app: Probot, { getRouter }?: {
|
|
3
8
|
getRouter?: (path?: string) => any;
|
|
4
9
|
}) => void;
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,135 @@ import { replyToDiscussion } from "./services/discussion.js";
|
|
|
5
5
|
import { getPreviousReviewContext, getReviewCommentThread } from "./services/review-memory.js";
|
|
6
6
|
import { executeRemediation } from "./services/remediation.js";
|
|
7
7
|
import { scrubSecrets } from "./services/scrubber.js";
|
|
8
|
-
import {
|
|
8
|
+
import { verifyReviewActionRefs } from "./services/action-ref-verifier.js";
|
|
9
|
+
import { addRemediationCommit, hasRemediationCommit, acquireRunLock, releaseRunLock, saveReviewFindings, getSandboxJobByCheckRun } from "./services/db.js";
|
|
9
10
|
import { executeSandboxCheckRun } from "./services/sandbox-runner.js";
|
|
11
|
+
/** Remediation only on explicit @hq-jr fix|patch|remediate (optional and merge). */
|
|
12
|
+
/** True when path is a file under tests/ (not bare tests, not traversal). */
|
|
13
|
+
export function isSafeReproTestPath(path) {
|
|
14
|
+
const normalized = (path ?? "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
15
|
+
return (normalized.startsWith("tests/") &&
|
|
16
|
+
normalized.length > "tests/".length &&
|
|
17
|
+
!normalized.includes("..") &&
|
|
18
|
+
!normalized.endsWith("/"));
|
|
19
|
+
}
|
|
20
|
+
export const REMEDIATE_COMMAND_RE = /@hq-jr(?:\[bot\])?\s+(fix|patch|remediate)(\s+and\s+merge)?\b/i;
|
|
21
|
+
export const COMMIT_REPRO_COMMAND_RE = /@hq-jr(?:\[bot\])?\s+commit-repro\b/i;
|
|
10
22
|
export default (app, { getRouter } = {}) => {
|
|
23
|
+
async function resolveInstallationToken(octokit) {
|
|
24
|
+
try {
|
|
25
|
+
if (typeof octokit.auth !== "function")
|
|
26
|
+
return undefined;
|
|
27
|
+
const authResult = await octokit.auth({ type: "installation" });
|
|
28
|
+
const token = authResult?.token;
|
|
29
|
+
return typeof token === "string" && token.length > 0 ? token : undefined;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function commitReproTestFromSummary(params) {
|
|
36
|
+
const { octokit, owner, repoName, pullNumber, sender, summaryText, headSha, targetBranch } = params;
|
|
37
|
+
// Fail closed on fork PRs: never updateRef into a head that is not this repo.
|
|
38
|
+
try {
|
|
39
|
+
const { data: prMeta } = await octokit.pulls.get({
|
|
40
|
+
owner,
|
|
41
|
+
repo: repoName,
|
|
42
|
+
pull_number: pullNumber,
|
|
43
|
+
});
|
|
44
|
+
const headFullName = prMeta?.head?.repo?.full_name;
|
|
45
|
+
const expected = `${owner}/${repoName}`;
|
|
46
|
+
if (!headFullName || headFullName !== expected) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
message: `⚠️ **hq-jr** refuses to commit a reproduction test on fork PR heads ` +
|
|
50
|
+
`(head repo: \`${headFullName || "unknown"}\`, expected \`${expected}\`). ` +
|
|
51
|
+
`Open the PR from a branch in this repository or push the test manually.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (forkErr) {
|
|
56
|
+
const msg = forkErr instanceof Error ? forkErr.message : String(forkErr);
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
message: `⚠️ **hq-jr** could not verify PR head repository before commit-repro: ${msg}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Prefer the synthesized-test section (heading + optional path in parentheses + fence).
|
|
63
|
+
const sectionMatch = summaryText.match(/###\s*Synthesized Reproduction Test(?:\s*\(([^)]+)\))?[\s\S]*?```(?:rust|typescript|javascript|python|go)?\s*([\s\S]*?)```/i);
|
|
64
|
+
const extractedPath = sectionMatch?.[1]?.trim() || undefined;
|
|
65
|
+
const testCode = sectionMatch?.[2]?.trim()
|
|
66
|
+
|| summaryText.match(/```(?:rust|typescript|javascript|python|go)?\s*([\s\S]*?)```/)?.[1]?.trim()
|
|
67
|
+
|| null;
|
|
68
|
+
if (!testCode) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
message: `⚠️ **hq-jr** could not extract a synthesized reproduction test from this check run output.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
let testFileName;
|
|
75
|
+
const normalizedExtracted = extractedPath?.replace(/^\/+/, "").replace(/\/+$/, "") ?? "";
|
|
76
|
+
const safeExtractedPath = isSafeReproTestPath(normalizedExtracted)
|
|
77
|
+
? normalizedExtracted
|
|
78
|
+
: null;
|
|
79
|
+
if (safeExtractedPath) {
|
|
80
|
+
testFileName = safeExtractedPath;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const isRust = summaryText.includes("cargo") || summaryText.includes(".rs") || /\brust\b/i.test(summaryText);
|
|
84
|
+
const isPython = summaryText.includes("pytest") || summaryText.includes(".py");
|
|
85
|
+
const isGo = summaryText.includes("go test") || summaryText.includes(".go");
|
|
86
|
+
testFileName = isRust
|
|
87
|
+
? `tests/repro_issue_${pullNumber}.rs`
|
|
88
|
+
: isPython
|
|
89
|
+
? `tests/test_repro_issue_${pullNumber}.py`
|
|
90
|
+
: isGo
|
|
91
|
+
? `tests/repro_issue_${pullNumber}_test.go`
|
|
92
|
+
: `tests/repro_issue_${pullNumber}.test.ts`;
|
|
93
|
+
}
|
|
94
|
+
const { data: blob } = await octokit.git.createBlob({
|
|
95
|
+
owner,
|
|
96
|
+
repo: repoName,
|
|
97
|
+
content: Buffer.from(testCode).toString("base64"),
|
|
98
|
+
encoding: "base64",
|
|
99
|
+
});
|
|
100
|
+
const { data: baseCommit } = await octokit.git.getCommit({
|
|
101
|
+
owner,
|
|
102
|
+
repo: repoName,
|
|
103
|
+
commit_sha: headSha,
|
|
104
|
+
});
|
|
105
|
+
const { data: newTree } = await octokit.git.createTree({
|
|
106
|
+
owner,
|
|
107
|
+
repo: repoName,
|
|
108
|
+
base_tree: baseCommit.tree.sha,
|
|
109
|
+
tree: [
|
|
110
|
+
{
|
|
111
|
+
path: testFileName,
|
|
112
|
+
mode: "100644",
|
|
113
|
+
type: "blob",
|
|
114
|
+
sha: blob.sha,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
const { data: newCommit } = await octokit.git.createCommit({
|
|
119
|
+
owner,
|
|
120
|
+
repo: repoName,
|
|
121
|
+
message: `test: add synthesized adversarial reproduction test for PR #${pullNumber} [skip ci]`,
|
|
122
|
+
tree: newTree.sha,
|
|
123
|
+
parents: [headSha],
|
|
124
|
+
});
|
|
125
|
+
addRemediationCommit(newCommit.sha);
|
|
126
|
+
await octokit.git.updateRef({
|
|
127
|
+
owner,
|
|
128
|
+
repo: repoName,
|
|
129
|
+
ref: `heads/${targetBranch}`,
|
|
130
|
+
sha: newCommit.sha,
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
message: `✅ **Reproduction Test Committed:** \`${testFileName}\` has been committed directly to branch \`${targetBranch}\` (Commit: \`${newCommit.sha.slice(0, 7)}\`) by @${sender}.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
11
137
|
process.on("unhandledRejection", (reason) => {
|
|
12
138
|
app.log.warn({ reason }, "Handled unhandledRejection to ensure daemon stability");
|
|
13
139
|
});
|
|
@@ -149,6 +275,7 @@ export default (app, { getRouter } = {}) => {
|
|
|
149
275
|
owner,
|
|
150
276
|
repo: repoName,
|
|
151
277
|
pullNumber,
|
|
278
|
+
currentHeadSha: pr.head.sha,
|
|
152
279
|
});
|
|
153
280
|
if (previousReview) {
|
|
154
281
|
app.log.info({
|
|
@@ -168,7 +295,8 @@ ${filesToReview.map((f) => `- ${f.path} (Risk: ${f.risk}, Reason: ${f.reason})`)
|
|
|
168
295
|
|
|
169
296
|
${contextPackage.promptPayload}
|
|
170
297
|
`;
|
|
171
|
-
const
|
|
298
|
+
const rawReview = await runDeepReview(reviewPrompt, { previousReview });
|
|
299
|
+
const review = await verifyReviewActionRefs(octokit, rawReview);
|
|
172
300
|
// 6. Post inline review comments via Octokit REST (with line anchor validation)
|
|
173
301
|
const normalizePath = (p) => p.replace(/^[./]+/, "").replace(/\\/g, "/");
|
|
174
302
|
const fileMap = new Map();
|
|
@@ -250,6 +378,27 @@ ${contextPackage.promptPayload}
|
|
|
250
378
|
}
|
|
251
379
|
}
|
|
252
380
|
}
|
|
381
|
+
// Persist review findings to SQLite for cross-push memory
|
|
382
|
+
try {
|
|
383
|
+
const findingRows = review.comments.map((c) => ({
|
|
384
|
+
owner,
|
|
385
|
+
repo: repoName,
|
|
386
|
+
pullNumber,
|
|
387
|
+
headSha: pr.head.sha,
|
|
388
|
+
path: c.path,
|
|
389
|
+
line: c.line,
|
|
390
|
+
side: c.side,
|
|
391
|
+
severity: c.severity,
|
|
392
|
+
title: c.title,
|
|
393
|
+
body: c.body,
|
|
394
|
+
}));
|
|
395
|
+
if (findingRows.length > 0) {
|
|
396
|
+
saveReviewFindings(findingRows);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch (persistErr) {
|
|
400
|
+
app.log.warn({ persistErr, pullNumber }, "Failed to persist review findings to SQLite");
|
|
401
|
+
}
|
|
253
402
|
// 7. Complete Check Run
|
|
254
403
|
if (checkRun?.data?.id) {
|
|
255
404
|
const conclusion = review.verdict === "REQUEST_CHANGES"
|
|
@@ -273,6 +422,7 @@ ${contextPackage.promptPayload}
|
|
|
273
422
|
if (review.requiresSandboxVerification) {
|
|
274
423
|
const goal = review.verificationGoal || "Verify flagged critical issues in isolated container sandbox";
|
|
275
424
|
app.log.info({ pullNumber, goal }, "Triggering Tier 3 asynchronous sandbox check run");
|
|
425
|
+
const authToken = await resolveInstallationToken(octokit);
|
|
276
426
|
executeSandboxCheckRun({
|
|
277
427
|
octokit,
|
|
278
428
|
owner,
|
|
@@ -283,6 +433,7 @@ ${contextPackage.promptPayload}
|
|
|
283
433
|
verificationGoal: goal,
|
|
284
434
|
modifiedFiles: parsedDiff.files.map((f) => f.newPath || f.oldPath),
|
|
285
435
|
probes: review.sandboxProbes,
|
|
436
|
+
authToken,
|
|
286
437
|
}).catch((sandboxErr) => {
|
|
287
438
|
app.log.warn({ sandboxErr, pullNumber }, "Async sandbox verification check run encountered error");
|
|
288
439
|
});
|
|
@@ -437,10 +588,8 @@ ${contextPackage.promptPayload}
|
|
|
437
588
|
app.log.info({ pullNumber, user: user.login }, "Responding to main PR thread comment mentioning @hq-jr");
|
|
438
589
|
try {
|
|
439
590
|
const lower = body.toLowerCase();
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
lower.includes("remediate") ||
|
|
443
|
-
lower.includes("resolve");
|
|
591
|
+
const remediateMatch = body.match(REMEDIATE_COMMAND_RE);
|
|
592
|
+
const isRemediateCommand = Boolean(remediateMatch);
|
|
444
593
|
if (isRemediateCommand) {
|
|
445
594
|
// Enforce RBAC: Verify commenter has write or admin permissions to repo
|
|
446
595
|
let hasWrite = false;
|
|
@@ -481,6 +630,7 @@ ${contextPackage.promptPayload}
|
|
|
481
630
|
owner,
|
|
482
631
|
repo: repoName,
|
|
483
632
|
pullNumber,
|
|
633
|
+
currentHeadSha: prResponse.data.head.sha,
|
|
484
634
|
});
|
|
485
635
|
if (!prevReview || prevReview.openIssues.length === 0) {
|
|
486
636
|
await context.octokit.issues.createComment({
|
|
@@ -492,7 +642,7 @@ ${contextPackage.promptPayload}
|
|
|
492
642
|
return;
|
|
493
643
|
}
|
|
494
644
|
const hasNegativeMerge = /\b(?:do not|don't|no|never)\s+merge\b/i.test(body);
|
|
495
|
-
const hasExplicitMergeCommand = /\b(?:and\s+merge|auto-?merge)\b/i.test(body);
|
|
645
|
+
const hasExplicitMergeCommand = Boolean(remediateMatch?.[2]) || /\b(?:and\s+merge|auto-?merge)\b/i.test(body);
|
|
496
646
|
const autoMerge = hasExplicitMergeCommand && !hasNegativeMerge;
|
|
497
647
|
const result = await executeRemediation({
|
|
498
648
|
octokit: context.octokit,
|
|
@@ -522,6 +672,67 @@ ${contextPackage.promptPayload}
|
|
|
522
672
|
});
|
|
523
673
|
return;
|
|
524
674
|
}
|
|
675
|
+
if (COMMIT_REPRO_COMMAND_RE.test(body)) {
|
|
676
|
+
let hasWrite = false;
|
|
677
|
+
try {
|
|
678
|
+
const { data: perm } = await context.octokit.repos.getCollaboratorPermissionLevel({
|
|
679
|
+
owner,
|
|
680
|
+
repo: repoName,
|
|
681
|
+
username: user.login,
|
|
682
|
+
});
|
|
683
|
+
hasWrite = perm.permission === "admin" || perm.permission === "write";
|
|
684
|
+
}
|
|
685
|
+
catch (permErr) {
|
|
686
|
+
app.log.warn({ permErr, user: user.login }, "Failed to verify collaborator permissions for commit-repro; failing closed");
|
|
687
|
+
}
|
|
688
|
+
if (!hasWrite) {
|
|
689
|
+
await context.octokit.issues.createComment({
|
|
690
|
+
owner,
|
|
691
|
+
repo: repoName,
|
|
692
|
+
issue_number: pullNumber,
|
|
693
|
+
body: `⛔ **Access Denied:** Only repository collaborators with write or admin permissions can commit reproduction tests.`,
|
|
694
|
+
});
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const prResponse = await context.octokit.pulls.get({
|
|
698
|
+
owner,
|
|
699
|
+
repo: repoName,
|
|
700
|
+
pull_number: pullNumber,
|
|
701
|
+
});
|
|
702
|
+
const pr = prResponse.data;
|
|
703
|
+
let summaryText = "";
|
|
704
|
+
try {
|
|
705
|
+
const checks = await context.octokit.checks.listForRef({
|
|
706
|
+
owner,
|
|
707
|
+
repo: repoName,
|
|
708
|
+
ref: pr.head.sha,
|
|
709
|
+
check_name: "hq-jr AI Sandbox Verification",
|
|
710
|
+
per_page: 10,
|
|
711
|
+
});
|
|
712
|
+
const latest = (checks.data.check_runs || [])[0];
|
|
713
|
+
summaryText = latest?.output?.summary || "";
|
|
714
|
+
}
|
|
715
|
+
catch (checkErr) {
|
|
716
|
+
app.log.warn({ checkErr }, "Could not list sandbox check runs for commit-repro");
|
|
717
|
+
}
|
|
718
|
+
const result = await commitReproTestFromSummary({
|
|
719
|
+
octokit: context.octokit,
|
|
720
|
+
owner,
|
|
721
|
+
repoName,
|
|
722
|
+
pullNumber,
|
|
723
|
+
sender: user.login,
|
|
724
|
+
summaryText,
|
|
725
|
+
headSha: pr.head.sha,
|
|
726
|
+
targetBranch: pr.head.ref,
|
|
727
|
+
});
|
|
728
|
+
await context.octokit.issues.createComment({
|
|
729
|
+
owner,
|
|
730
|
+
repo: repoName,
|
|
731
|
+
issue_number: pullNumber,
|
|
732
|
+
body: result.message,
|
|
733
|
+
});
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
525
736
|
const isReviewCommand = lower.includes("review") ||
|
|
526
737
|
lower.includes("audit") ||
|
|
527
738
|
lower.includes("scan") ||
|
|
@@ -716,110 +927,134 @@ ${contextPackage.promptPayload}
|
|
|
716
927
|
pr: prResponse.data,
|
|
717
928
|
});
|
|
718
929
|
});
|
|
719
|
-
// Handle Check Run action buttons (e.g. Commit Repro Test)
|
|
930
|
+
// Handle Check Run action buttons (e.g. Commit Repro Test / Re-run Sandbox)
|
|
720
931
|
app.on("check_run.requested_action", async (context) => {
|
|
721
932
|
const { check_run: checkRun, requested_action: requestedAction } = context.payload;
|
|
722
933
|
const identifier = requestedAction.identifier;
|
|
723
934
|
const owner = context.payload.repository.owner.login;
|
|
724
935
|
const repoName = context.payload.repository.name;
|
|
725
936
|
const sender = context.payload.sender.login;
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
catch (permErr) {
|
|
737
|
-
app.log.warn({ permErr, sender }, "Failed to verify collaborator permissions for check action; failing closed");
|
|
738
|
-
}
|
|
739
|
-
const pullRequests = checkRun.pull_requests;
|
|
740
|
-
if (!pullRequests || pullRequests.length === 0) {
|
|
741
|
-
app.log.warn({ checkRunId: checkRun.id }, "No pull request attached to check run action");
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
const pullNumber = pullRequests[0].number;
|
|
745
|
-
if (!hasWrite) {
|
|
746
|
-
await context.octokit.issues.createComment({
|
|
747
|
-
owner,
|
|
748
|
-
repo: repoName,
|
|
749
|
-
issue_number: pullNumber,
|
|
750
|
-
body: `⛔ **Access Denied:** Only repository collaborators with write or admin permissions can commit reproduction tests.`,
|
|
751
|
-
});
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
const prResponse = await context.octokit.pulls.get({
|
|
937
|
+
const pullRequests = checkRun.pull_requests;
|
|
938
|
+
if (!pullRequests || pullRequests.length === 0) {
|
|
939
|
+
app.log.warn({ checkRunId: checkRun.id }, "No pull request attached to check run action");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const pullNumber = pullRequests[0].number;
|
|
943
|
+
let hasWrite = false;
|
|
944
|
+
try {
|
|
945
|
+
const { data: perm } = await context.octokit.repos.getCollaboratorPermissionLevel({
|
|
755
946
|
owner,
|
|
756
947
|
repo: repoName,
|
|
757
|
-
|
|
948
|
+
username: sender,
|
|
758
949
|
});
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
owner,
|
|
767
|
-
repo: repoName,
|
|
768
|
-
issue_number: pullNumber,
|
|
769
|
-
body: `⚠️ **hq-jr** could not extract a synthesized reproduction test from this check run output.`,
|
|
770
|
-
});
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
const isRust = outputText.includes("cargo") || outputText.includes(".rs");
|
|
774
|
-
const isPython = outputText.includes("pytest") || outputText.includes(".py");
|
|
775
|
-
const testFileName = isRust
|
|
776
|
-
? `tests/repro_issue_${pullNumber}.rs`
|
|
777
|
-
: isPython
|
|
778
|
-
? `tests/test_repro_issue_${pullNumber}.py`
|
|
779
|
-
: `tests/repro_issue_${pullNumber}.test.ts`;
|
|
780
|
-
const { data: blob } = await context.octokit.git.createBlob({
|
|
950
|
+
hasWrite = perm.permission === "admin" || perm.permission === "write";
|
|
951
|
+
}
|
|
952
|
+
catch (permErr) {
|
|
953
|
+
app.log.warn({ permErr, sender }, "Failed to verify collaborator permissions for check action; failing closed");
|
|
954
|
+
}
|
|
955
|
+
if (!hasWrite) {
|
|
956
|
+
await context.octokit.issues.createComment({
|
|
781
957
|
owner,
|
|
782
958
|
repo: repoName,
|
|
783
|
-
|
|
784
|
-
|
|
959
|
+
issue_number: pullNumber,
|
|
960
|
+
body: `⛔ **Access Denied:** Only repository collaborators with write or admin permissions can trigger sandbox check actions.`,
|
|
785
961
|
});
|
|
786
|
-
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (identifier === "commit_repro_test") {
|
|
965
|
+
const prResponse = await context.octokit.pulls.get({
|
|
787
966
|
owner,
|
|
788
967
|
repo: repoName,
|
|
789
|
-
|
|
968
|
+
pull_number: pullNumber,
|
|
790
969
|
});
|
|
791
|
-
const
|
|
970
|
+
const pr = prResponse.data;
|
|
971
|
+
const outputText = checkRun.output?.summary || "";
|
|
972
|
+
const result = await commitReproTestFromSummary({
|
|
973
|
+
octokit: context.octokit,
|
|
792
974
|
owner,
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
type: "blob",
|
|
800
|
-
sha: blob.sha,
|
|
801
|
-
},
|
|
802
|
-
],
|
|
975
|
+
repoName,
|
|
976
|
+
pullNumber,
|
|
977
|
+
sender,
|
|
978
|
+
summaryText: outputText,
|
|
979
|
+
headSha: pr.head.sha,
|
|
980
|
+
targetBranch: pr.head.ref,
|
|
803
981
|
});
|
|
804
|
-
|
|
982
|
+
await context.octokit.issues.createComment({
|
|
805
983
|
owner,
|
|
806
984
|
repo: repoName,
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
parents: [pr.head.sha],
|
|
985
|
+
issue_number: pullNumber,
|
|
986
|
+
body: result.message,
|
|
810
987
|
});
|
|
811
|
-
|
|
812
|
-
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (identifier === "rerun_sandbox") {
|
|
991
|
+
const prResponse = await context.octokit.pulls.get({
|
|
813
992
|
owner,
|
|
814
993
|
repo: repoName,
|
|
815
|
-
|
|
816
|
-
sha: newCommit.sha,
|
|
994
|
+
pull_number: pullNumber,
|
|
817
995
|
});
|
|
996
|
+
const pr = prResponse.data;
|
|
997
|
+
let probes;
|
|
998
|
+
let verificationGoal = "Re-run autonomous sandbox verification with a fresh container";
|
|
999
|
+
let testCommand;
|
|
1000
|
+
let branch = pr.head.ref || "main";
|
|
1001
|
+
try {
|
|
1002
|
+
const job = getSandboxJobByCheckRun({
|
|
1003
|
+
owner,
|
|
1004
|
+
repo: repoName,
|
|
1005
|
+
checkRunId: checkRun.id,
|
|
1006
|
+
});
|
|
1007
|
+
if (job) {
|
|
1008
|
+
if (job.probesJson) {
|
|
1009
|
+
try {
|
|
1010
|
+
probes = JSON.parse(job.probesJson);
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
// ignore bad json
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (job.verificationGoal)
|
|
1017
|
+
verificationGoal = job.verificationGoal;
|
|
1018
|
+
if (job.testCommand)
|
|
1019
|
+
testCommand = job.testCommand;
|
|
1020
|
+
if (job.branch)
|
|
1021
|
+
branch = job.branch;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
catch (jobErr) {
|
|
1025
|
+
app.log.warn({ jobErr }, "Could not load prior sandbox job for rerun");
|
|
1026
|
+
}
|
|
1027
|
+
// Best-effort recovery of objective/test command from prior check summary
|
|
1028
|
+
// (probe matrix rows are not reconstructed here; probes come from sandbox_jobs).
|
|
1029
|
+
if ((!probes || probes.length === 0) && checkRun.output?.summary) {
|
|
1030
|
+
const summary = checkRun.output.summary;
|
|
1031
|
+
const goalMatch = summary.match(/\*\*Objective:\*\*\s*(.+)/);
|
|
1032
|
+
if (goalMatch)
|
|
1033
|
+
verificationGoal = goalMatch[1].trim();
|
|
1034
|
+
const cmdMatch = summary.match(/\*\*Baseline Command:\*\*\s*`([^`]+)`/);
|
|
1035
|
+
if (cmdMatch)
|
|
1036
|
+
testCommand = cmdMatch[1];
|
|
1037
|
+
}
|
|
818
1038
|
await context.octokit.issues.createComment({
|
|
819
1039
|
owner,
|
|
820
1040
|
repo: repoName,
|
|
821
1041
|
issue_number: pullNumber,
|
|
822
|
-
body:
|
|
1042
|
+
body: `🔄 **hq-jr** re-dispatching Tier 3 sandbox verification for PR #${pullNumber} (requested by @${sender}).`,
|
|
1043
|
+
});
|
|
1044
|
+
const authToken = await resolveInstallationToken(context.octokit);
|
|
1045
|
+
executeSandboxCheckRun({
|
|
1046
|
+
octokit: context.octokit,
|
|
1047
|
+
owner,
|
|
1048
|
+
repo: repoName,
|
|
1049
|
+
pullNumber,
|
|
1050
|
+
headSha: pr.head.sha,
|
|
1051
|
+
branch,
|
|
1052
|
+
verificationGoal,
|
|
1053
|
+
testCommand,
|
|
1054
|
+
probes,
|
|
1055
|
+
authToken,
|
|
1056
|
+
}).catch((sandboxErr) => {
|
|
1057
|
+
app.log.warn({ sandboxErr, pullNumber }, "Rerun sandbox verification failed");
|
|
823
1058
|
});
|
|
824
1059
|
}
|
|
825
1060
|
});
|