hq-jr 0.1.2 → 0.1.3
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 +316 -83
- package/dist/index.js.map +1 -1
- 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,134 @@ 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 { addRemediationCommit, hasRemediationCommit, acquireRunLock, releaseRunLock } from "./services/db.js";
|
|
8
|
+
import { addRemediationCommit, hasRemediationCommit, acquireRunLock, releaseRunLock, saveReviewFindings, getSandboxJobByCheckRun } from "./services/db.js";
|
|
9
9
|
import { executeSandboxCheckRun } from "./services/sandbox-runner.js";
|
|
10
|
+
/** Remediation only on explicit @hq-jr fix|patch|remediate (optional and merge). */
|
|
11
|
+
/** True when path is a file under tests/ (not bare tests, not traversal). */
|
|
12
|
+
export function isSafeReproTestPath(path) {
|
|
13
|
+
const normalized = (path ?? "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
14
|
+
return (normalized.startsWith("tests/") &&
|
|
15
|
+
normalized.length > "tests/".length &&
|
|
16
|
+
!normalized.includes("..") &&
|
|
17
|
+
!normalized.endsWith("/"));
|
|
18
|
+
}
|
|
19
|
+
export const REMEDIATE_COMMAND_RE = /@hq-jr(?:\[bot\])?\s+(fix|patch|remediate)(\s+and\s+merge)?\b/i;
|
|
20
|
+
export const COMMIT_REPRO_COMMAND_RE = /@hq-jr(?:\[bot\])?\s+commit-repro\b/i;
|
|
10
21
|
export default (app, { getRouter } = {}) => {
|
|
22
|
+
async function resolveInstallationToken(octokit) {
|
|
23
|
+
try {
|
|
24
|
+
if (typeof octokit.auth !== "function")
|
|
25
|
+
return undefined;
|
|
26
|
+
const authResult = await octokit.auth({ type: "installation" });
|
|
27
|
+
const token = authResult?.token;
|
|
28
|
+
return typeof token === "string" && token.length > 0 ? token : undefined;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function commitReproTestFromSummary(params) {
|
|
35
|
+
const { octokit, owner, repoName, pullNumber, sender, summaryText, headSha, targetBranch } = params;
|
|
36
|
+
// Fail closed on fork PRs: never updateRef into a head that is not this repo.
|
|
37
|
+
try {
|
|
38
|
+
const { data: prMeta } = await octokit.pulls.get({
|
|
39
|
+
owner,
|
|
40
|
+
repo: repoName,
|
|
41
|
+
pull_number: pullNumber,
|
|
42
|
+
});
|
|
43
|
+
const headFullName = prMeta?.head?.repo?.full_name;
|
|
44
|
+
const expected = `${owner}/${repoName}`;
|
|
45
|
+
if (!headFullName || headFullName !== expected) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
message: `⚠️ **hq-jr** refuses to commit a reproduction test on fork PR heads ` +
|
|
49
|
+
`(head repo: \`${headFullName || "unknown"}\`, expected \`${expected}\`). ` +
|
|
50
|
+
`Open the PR from a branch in this repository or push the test manually.`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (forkErr) {
|
|
55
|
+
const msg = forkErr instanceof Error ? forkErr.message : String(forkErr);
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
message: `⚠️ **hq-jr** could not verify PR head repository before commit-repro: ${msg}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Prefer the synthesized-test section (heading + optional path in parentheses + fence).
|
|
62
|
+
const sectionMatch = summaryText.match(/###\s*Synthesized Reproduction Test(?:\s*\(([^)]+)\))?[\s\S]*?```(?:rust|typescript|javascript|python|go)?\s*([\s\S]*?)```/i);
|
|
63
|
+
const extractedPath = sectionMatch?.[1]?.trim() || undefined;
|
|
64
|
+
const testCode = sectionMatch?.[2]?.trim()
|
|
65
|
+
|| summaryText.match(/```(?:rust|typescript|javascript|python|go)?\s*([\s\S]*?)```/)?.[1]?.trim()
|
|
66
|
+
|| null;
|
|
67
|
+
if (!testCode) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
message: `⚠️ **hq-jr** could not extract a synthesized reproduction test from this check run output.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
let testFileName;
|
|
74
|
+
const normalizedExtracted = extractedPath?.replace(/^\/+/, "").replace(/\/+$/, "") ?? "";
|
|
75
|
+
const safeExtractedPath = isSafeReproTestPath(normalizedExtracted)
|
|
76
|
+
? normalizedExtracted
|
|
77
|
+
: null;
|
|
78
|
+
if (safeExtractedPath) {
|
|
79
|
+
testFileName = safeExtractedPath;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const isRust = summaryText.includes("cargo") || summaryText.includes(".rs") || /\brust\b/i.test(summaryText);
|
|
83
|
+
const isPython = summaryText.includes("pytest") || summaryText.includes(".py");
|
|
84
|
+
const isGo = summaryText.includes("go test") || summaryText.includes(".go");
|
|
85
|
+
testFileName = isRust
|
|
86
|
+
? `tests/repro_issue_${pullNumber}.rs`
|
|
87
|
+
: isPython
|
|
88
|
+
? `tests/test_repro_issue_${pullNumber}.py`
|
|
89
|
+
: isGo
|
|
90
|
+
? `tests/repro_issue_${pullNumber}_test.go`
|
|
91
|
+
: `tests/repro_issue_${pullNumber}.test.ts`;
|
|
92
|
+
}
|
|
93
|
+
const { data: blob } = await octokit.git.createBlob({
|
|
94
|
+
owner,
|
|
95
|
+
repo: repoName,
|
|
96
|
+
content: Buffer.from(testCode).toString("base64"),
|
|
97
|
+
encoding: "base64",
|
|
98
|
+
});
|
|
99
|
+
const { data: baseCommit } = await octokit.git.getCommit({
|
|
100
|
+
owner,
|
|
101
|
+
repo: repoName,
|
|
102
|
+
commit_sha: headSha,
|
|
103
|
+
});
|
|
104
|
+
const { data: newTree } = await octokit.git.createTree({
|
|
105
|
+
owner,
|
|
106
|
+
repo: repoName,
|
|
107
|
+
base_tree: baseCommit.tree.sha,
|
|
108
|
+
tree: [
|
|
109
|
+
{
|
|
110
|
+
path: testFileName,
|
|
111
|
+
mode: "100644",
|
|
112
|
+
type: "blob",
|
|
113
|
+
sha: blob.sha,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
const { data: newCommit } = await octokit.git.createCommit({
|
|
118
|
+
owner,
|
|
119
|
+
repo: repoName,
|
|
120
|
+
message: `test: add synthesized adversarial reproduction test for PR #${pullNumber} [skip ci]`,
|
|
121
|
+
tree: newTree.sha,
|
|
122
|
+
parents: [headSha],
|
|
123
|
+
});
|
|
124
|
+
addRemediationCommit(newCommit.sha);
|
|
125
|
+
await octokit.git.updateRef({
|
|
126
|
+
owner,
|
|
127
|
+
repo: repoName,
|
|
128
|
+
ref: `heads/${targetBranch}`,
|
|
129
|
+
sha: newCommit.sha,
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
message: `✅ **Reproduction Test Committed:** \`${testFileName}\` has been committed directly to branch \`${targetBranch}\` (Commit: \`${newCommit.sha.slice(0, 7)}\`) by @${sender}.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
11
136
|
process.on("unhandledRejection", (reason) => {
|
|
12
137
|
app.log.warn({ reason }, "Handled unhandledRejection to ensure daemon stability");
|
|
13
138
|
});
|
|
@@ -149,6 +274,7 @@ export default (app, { getRouter } = {}) => {
|
|
|
149
274
|
owner,
|
|
150
275
|
repo: repoName,
|
|
151
276
|
pullNumber,
|
|
277
|
+
currentHeadSha: pr.head.sha,
|
|
152
278
|
});
|
|
153
279
|
if (previousReview) {
|
|
154
280
|
app.log.info({
|
|
@@ -250,6 +376,27 @@ ${contextPackage.promptPayload}
|
|
|
250
376
|
}
|
|
251
377
|
}
|
|
252
378
|
}
|
|
379
|
+
// Persist review findings to SQLite for cross-push memory
|
|
380
|
+
try {
|
|
381
|
+
const findingRows = review.comments.map((c) => ({
|
|
382
|
+
owner,
|
|
383
|
+
repo: repoName,
|
|
384
|
+
pullNumber,
|
|
385
|
+
headSha: pr.head.sha,
|
|
386
|
+
path: c.path,
|
|
387
|
+
line: c.line,
|
|
388
|
+
side: c.side,
|
|
389
|
+
severity: c.severity,
|
|
390
|
+
title: c.title,
|
|
391
|
+
body: c.body,
|
|
392
|
+
}));
|
|
393
|
+
if (findingRows.length > 0) {
|
|
394
|
+
saveReviewFindings(findingRows);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (persistErr) {
|
|
398
|
+
app.log.warn({ persistErr, pullNumber }, "Failed to persist review findings to SQLite");
|
|
399
|
+
}
|
|
253
400
|
// 7. Complete Check Run
|
|
254
401
|
if (checkRun?.data?.id) {
|
|
255
402
|
const conclusion = review.verdict === "REQUEST_CHANGES"
|
|
@@ -273,6 +420,7 @@ ${contextPackage.promptPayload}
|
|
|
273
420
|
if (review.requiresSandboxVerification) {
|
|
274
421
|
const goal = review.verificationGoal || "Verify flagged critical issues in isolated container sandbox";
|
|
275
422
|
app.log.info({ pullNumber, goal }, "Triggering Tier 3 asynchronous sandbox check run");
|
|
423
|
+
const authToken = await resolveInstallationToken(octokit);
|
|
276
424
|
executeSandboxCheckRun({
|
|
277
425
|
octokit,
|
|
278
426
|
owner,
|
|
@@ -283,6 +431,7 @@ ${contextPackage.promptPayload}
|
|
|
283
431
|
verificationGoal: goal,
|
|
284
432
|
modifiedFiles: parsedDiff.files.map((f) => f.newPath || f.oldPath),
|
|
285
433
|
probes: review.sandboxProbes,
|
|
434
|
+
authToken,
|
|
286
435
|
}).catch((sandboxErr) => {
|
|
287
436
|
app.log.warn({ sandboxErr, pullNumber }, "Async sandbox verification check run encountered error");
|
|
288
437
|
});
|
|
@@ -437,10 +586,8 @@ ${contextPackage.promptPayload}
|
|
|
437
586
|
app.log.info({ pullNumber, user: user.login }, "Responding to main PR thread comment mentioning @hq-jr");
|
|
438
587
|
try {
|
|
439
588
|
const lower = body.toLowerCase();
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
lower.includes("remediate") ||
|
|
443
|
-
lower.includes("resolve");
|
|
589
|
+
const remediateMatch = body.match(REMEDIATE_COMMAND_RE);
|
|
590
|
+
const isRemediateCommand = Boolean(remediateMatch);
|
|
444
591
|
if (isRemediateCommand) {
|
|
445
592
|
// Enforce RBAC: Verify commenter has write or admin permissions to repo
|
|
446
593
|
let hasWrite = false;
|
|
@@ -481,6 +628,7 @@ ${contextPackage.promptPayload}
|
|
|
481
628
|
owner,
|
|
482
629
|
repo: repoName,
|
|
483
630
|
pullNumber,
|
|
631
|
+
currentHeadSha: prResponse.data.head.sha,
|
|
484
632
|
});
|
|
485
633
|
if (!prevReview || prevReview.openIssues.length === 0) {
|
|
486
634
|
await context.octokit.issues.createComment({
|
|
@@ -492,7 +640,7 @@ ${contextPackage.promptPayload}
|
|
|
492
640
|
return;
|
|
493
641
|
}
|
|
494
642
|
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);
|
|
643
|
+
const hasExplicitMergeCommand = Boolean(remediateMatch?.[2]) || /\b(?:and\s+merge|auto-?merge)\b/i.test(body);
|
|
496
644
|
const autoMerge = hasExplicitMergeCommand && !hasNegativeMerge;
|
|
497
645
|
const result = await executeRemediation({
|
|
498
646
|
octokit: context.octokit,
|
|
@@ -522,6 +670,67 @@ ${contextPackage.promptPayload}
|
|
|
522
670
|
});
|
|
523
671
|
return;
|
|
524
672
|
}
|
|
673
|
+
if (COMMIT_REPRO_COMMAND_RE.test(body)) {
|
|
674
|
+
let hasWrite = false;
|
|
675
|
+
try {
|
|
676
|
+
const { data: perm } = await context.octokit.repos.getCollaboratorPermissionLevel({
|
|
677
|
+
owner,
|
|
678
|
+
repo: repoName,
|
|
679
|
+
username: user.login,
|
|
680
|
+
});
|
|
681
|
+
hasWrite = perm.permission === "admin" || perm.permission === "write";
|
|
682
|
+
}
|
|
683
|
+
catch (permErr) {
|
|
684
|
+
app.log.warn({ permErr, user: user.login }, "Failed to verify collaborator permissions for commit-repro; failing closed");
|
|
685
|
+
}
|
|
686
|
+
if (!hasWrite) {
|
|
687
|
+
await context.octokit.issues.createComment({
|
|
688
|
+
owner,
|
|
689
|
+
repo: repoName,
|
|
690
|
+
issue_number: pullNumber,
|
|
691
|
+
body: `⛔ **Access Denied:** Only repository collaborators with write or admin permissions can commit reproduction tests.`,
|
|
692
|
+
});
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const prResponse = await context.octokit.pulls.get({
|
|
696
|
+
owner,
|
|
697
|
+
repo: repoName,
|
|
698
|
+
pull_number: pullNumber,
|
|
699
|
+
});
|
|
700
|
+
const pr = prResponse.data;
|
|
701
|
+
let summaryText = "";
|
|
702
|
+
try {
|
|
703
|
+
const checks = await context.octokit.checks.listForRef({
|
|
704
|
+
owner,
|
|
705
|
+
repo: repoName,
|
|
706
|
+
ref: pr.head.sha,
|
|
707
|
+
check_name: "hq-jr AI Sandbox Verification",
|
|
708
|
+
per_page: 10,
|
|
709
|
+
});
|
|
710
|
+
const latest = (checks.data.check_runs || [])[0];
|
|
711
|
+
summaryText = latest?.output?.summary || "";
|
|
712
|
+
}
|
|
713
|
+
catch (checkErr) {
|
|
714
|
+
app.log.warn({ checkErr }, "Could not list sandbox check runs for commit-repro");
|
|
715
|
+
}
|
|
716
|
+
const result = await commitReproTestFromSummary({
|
|
717
|
+
octokit: context.octokit,
|
|
718
|
+
owner,
|
|
719
|
+
repoName,
|
|
720
|
+
pullNumber,
|
|
721
|
+
sender: user.login,
|
|
722
|
+
summaryText,
|
|
723
|
+
headSha: pr.head.sha,
|
|
724
|
+
targetBranch: pr.head.ref,
|
|
725
|
+
});
|
|
726
|
+
await context.octokit.issues.createComment({
|
|
727
|
+
owner,
|
|
728
|
+
repo: repoName,
|
|
729
|
+
issue_number: pullNumber,
|
|
730
|
+
body: result.message,
|
|
731
|
+
});
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
525
734
|
const isReviewCommand = lower.includes("review") ||
|
|
526
735
|
lower.includes("audit") ||
|
|
527
736
|
lower.includes("scan") ||
|
|
@@ -716,110 +925,134 @@ ${contextPackage.promptPayload}
|
|
|
716
925
|
pr: prResponse.data,
|
|
717
926
|
});
|
|
718
927
|
});
|
|
719
|
-
// Handle Check Run action buttons (e.g. Commit Repro Test)
|
|
928
|
+
// Handle Check Run action buttons (e.g. Commit Repro Test / Re-run Sandbox)
|
|
720
929
|
app.on("check_run.requested_action", async (context) => {
|
|
721
930
|
const { check_run: checkRun, requested_action: requestedAction } = context.payload;
|
|
722
931
|
const identifier = requestedAction.identifier;
|
|
723
932
|
const owner = context.payload.repository.owner.login;
|
|
724
933
|
const repoName = context.payload.repository.name;
|
|
725
934
|
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({
|
|
935
|
+
const pullRequests = checkRun.pull_requests;
|
|
936
|
+
if (!pullRequests || pullRequests.length === 0) {
|
|
937
|
+
app.log.warn({ checkRunId: checkRun.id }, "No pull request attached to check run action");
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const pullNumber = pullRequests[0].number;
|
|
941
|
+
let hasWrite = false;
|
|
942
|
+
try {
|
|
943
|
+
const { data: perm } = await context.octokit.repos.getCollaboratorPermissionLevel({
|
|
755
944
|
owner,
|
|
756
945
|
repo: repoName,
|
|
757
|
-
|
|
946
|
+
username: sender,
|
|
758
947
|
});
|
|
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({
|
|
948
|
+
hasWrite = perm.permission === "admin" || perm.permission === "write";
|
|
949
|
+
}
|
|
950
|
+
catch (permErr) {
|
|
951
|
+
app.log.warn({ permErr, sender }, "Failed to verify collaborator permissions for check action; failing closed");
|
|
952
|
+
}
|
|
953
|
+
if (!hasWrite) {
|
|
954
|
+
await context.octokit.issues.createComment({
|
|
781
955
|
owner,
|
|
782
956
|
repo: repoName,
|
|
783
|
-
|
|
784
|
-
|
|
957
|
+
issue_number: pullNumber,
|
|
958
|
+
body: `⛔ **Access Denied:** Only repository collaborators with write or admin permissions can trigger sandbox check actions.`,
|
|
785
959
|
});
|
|
786
|
-
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (identifier === "commit_repro_test") {
|
|
963
|
+
const prResponse = await context.octokit.pulls.get({
|
|
787
964
|
owner,
|
|
788
965
|
repo: repoName,
|
|
789
|
-
|
|
966
|
+
pull_number: pullNumber,
|
|
790
967
|
});
|
|
791
|
-
const
|
|
968
|
+
const pr = prResponse.data;
|
|
969
|
+
const outputText = checkRun.output?.summary || "";
|
|
970
|
+
const result = await commitReproTestFromSummary({
|
|
971
|
+
octokit: context.octokit,
|
|
792
972
|
owner,
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
type: "blob",
|
|
800
|
-
sha: blob.sha,
|
|
801
|
-
},
|
|
802
|
-
],
|
|
973
|
+
repoName,
|
|
974
|
+
pullNumber,
|
|
975
|
+
sender,
|
|
976
|
+
summaryText: outputText,
|
|
977
|
+
headSha: pr.head.sha,
|
|
978
|
+
targetBranch: pr.head.ref,
|
|
803
979
|
});
|
|
804
|
-
|
|
980
|
+
await context.octokit.issues.createComment({
|
|
805
981
|
owner,
|
|
806
982
|
repo: repoName,
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
parents: [pr.head.sha],
|
|
983
|
+
issue_number: pullNumber,
|
|
984
|
+
body: result.message,
|
|
810
985
|
});
|
|
811
|
-
|
|
812
|
-
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (identifier === "rerun_sandbox") {
|
|
989
|
+
const prResponse = await context.octokit.pulls.get({
|
|
813
990
|
owner,
|
|
814
991
|
repo: repoName,
|
|
815
|
-
|
|
816
|
-
sha: newCommit.sha,
|
|
992
|
+
pull_number: pullNumber,
|
|
817
993
|
});
|
|
994
|
+
const pr = prResponse.data;
|
|
995
|
+
let probes;
|
|
996
|
+
let verificationGoal = "Re-run autonomous sandbox verification with a fresh container";
|
|
997
|
+
let testCommand;
|
|
998
|
+
let branch = pr.head.ref || "main";
|
|
999
|
+
try {
|
|
1000
|
+
const job = getSandboxJobByCheckRun({
|
|
1001
|
+
owner,
|
|
1002
|
+
repo: repoName,
|
|
1003
|
+
checkRunId: checkRun.id,
|
|
1004
|
+
});
|
|
1005
|
+
if (job) {
|
|
1006
|
+
if (job.probesJson) {
|
|
1007
|
+
try {
|
|
1008
|
+
probes = JSON.parse(job.probesJson);
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
// ignore bad json
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (job.verificationGoal)
|
|
1015
|
+
verificationGoal = job.verificationGoal;
|
|
1016
|
+
if (job.testCommand)
|
|
1017
|
+
testCommand = job.testCommand;
|
|
1018
|
+
if (job.branch)
|
|
1019
|
+
branch = job.branch;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
catch (jobErr) {
|
|
1023
|
+
app.log.warn({ jobErr }, "Could not load prior sandbox job for rerun");
|
|
1024
|
+
}
|
|
1025
|
+
// Best-effort recovery of objective/test command from prior check summary
|
|
1026
|
+
// (probe matrix rows are not reconstructed here; probes come from sandbox_jobs).
|
|
1027
|
+
if ((!probes || probes.length === 0) && checkRun.output?.summary) {
|
|
1028
|
+
const summary = checkRun.output.summary;
|
|
1029
|
+
const goalMatch = summary.match(/\*\*Objective:\*\*\s*(.+)/);
|
|
1030
|
+
if (goalMatch)
|
|
1031
|
+
verificationGoal = goalMatch[1].trim();
|
|
1032
|
+
const cmdMatch = summary.match(/\*\*Baseline Command:\*\*\s*`([^`]+)`/);
|
|
1033
|
+
if (cmdMatch)
|
|
1034
|
+
testCommand = cmdMatch[1];
|
|
1035
|
+
}
|
|
818
1036
|
await context.octokit.issues.createComment({
|
|
819
1037
|
owner,
|
|
820
1038
|
repo: repoName,
|
|
821
1039
|
issue_number: pullNumber,
|
|
822
|
-
body:
|
|
1040
|
+
body: `🔄 **hq-jr** re-dispatching Tier 3 sandbox verification for PR #${pullNumber} (requested by @${sender}).`,
|
|
1041
|
+
});
|
|
1042
|
+
const authToken = await resolveInstallationToken(context.octokit);
|
|
1043
|
+
executeSandboxCheckRun({
|
|
1044
|
+
octokit: context.octokit,
|
|
1045
|
+
owner,
|
|
1046
|
+
repo: repoName,
|
|
1047
|
+
pullNumber,
|
|
1048
|
+
headSha: pr.head.sha,
|
|
1049
|
+
branch,
|
|
1050
|
+
verificationGoal,
|
|
1051
|
+
testCommand,
|
|
1052
|
+
probes,
|
|
1053
|
+
authToken,
|
|
1054
|
+
}).catch((sandboxErr) => {
|
|
1055
|
+
app.log.warn({ sandboxErr, pullNumber }, "Rerun sandbox verification failed");
|
|
823
1056
|
});
|
|
824
1057
|
}
|
|
825
1058
|
});
|