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 CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  ### Why hq-jr
17
17
 
18
- - **Sandbox proof.** Suspicious findings can be reproduced in an isolated container before the bot files a strong claim. Tier 3 / Antigravity is experimental.
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 memory tracks prior findings across pushes. When a break looks real, it can ask the experimental sandbox path to reproduce before speaking loudly. Tag `@hq-jr fix` if you want a remediation branch.
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. | Can reproduce in a sandbox before strong claims (Tier 3 experimental). |
41
- | **Memory** | Stateless; repeats nags after force-push. | SQLite WAL remembers prior findings. |
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
- - Treat as experimental until you validate it in your environment.
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` synthesizes a fix branch `hq-jr/fix-pr-<id>`. `@hq-jr fix and merge` when permissions allow. Requires collaborator `admin` or `write`.
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 { addRemediationCommit, hasRemediationCommit, acquireRunLock, releaseRunLock } from "./services/db.js";
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 review = await runDeepReview(reviewPrompt, { previousReview });
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 isRemediateCommand = lower.includes("fix") ||
441
- lower.includes("patch") ||
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
- if (identifier === "commit_repro_test") {
727
- let hasWrite = false;
728
- try {
729
- const { data: perm } = await context.octokit.repos.getCollaboratorPermissionLevel({
730
- owner,
731
- repo: repoName,
732
- username: sender,
733
- });
734
- hasWrite = perm.permission === "admin" || perm.permission === "write";
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
- pull_number: pullNumber,
948
+ username: sender,
758
949
  });
759
- const pr = prResponse.data;
760
- const targetBranch = pr.head.ref;
761
- const outputText = checkRun.output?.summary || "";
762
- const testCodeMatch = outputText.match(/```(?:rust|typescript|javascript|python)?\s*([\s\S]*?)```/);
763
- const testCode = testCodeMatch ? testCodeMatch[1].trim() : null;
764
- if (!testCode) {
765
- await context.octokit.issues.createComment({
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
- content: Buffer.from(testCode).toString("base64"),
784
- encoding: "base64",
959
+ issue_number: pullNumber,
960
+ body: `⛔ **Access Denied:** Only repository collaborators with write or admin permissions can trigger sandbox check actions.`,
785
961
  });
786
- const { data: baseCommit } = await context.octokit.git.getCommit({
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
- commit_sha: pr.head.sha,
968
+ pull_number: pullNumber,
790
969
  });
791
- const { data: newTree } = await context.octokit.git.createTree({
970
+ const pr = prResponse.data;
971
+ const outputText = checkRun.output?.summary || "";
972
+ const result = await commitReproTestFromSummary({
973
+ octokit: context.octokit,
792
974
  owner,
793
- repo: repoName,
794
- base_tree: baseCommit.tree.sha,
795
- tree: [
796
- {
797
- path: testFileName,
798
- mode: "100644",
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
- const { data: newCommit } = await context.octokit.git.createCommit({
982
+ await context.octokit.issues.createComment({
805
983
  owner,
806
984
  repo: repoName,
807
- message: `test: add synthesized adversarial reproduction test for PR #${pullNumber} [skip ci]`,
808
- tree: newTree.sha,
809
- parents: [pr.head.sha],
985
+ issue_number: pullNumber,
986
+ body: result.message,
810
987
  });
811
- addRemediationCommit(newCommit.sha);
812
- await context.octokit.git.updateRef({
988
+ return;
989
+ }
990
+ if (identifier === "rerun_sandbox") {
991
+ const prResponse = await context.octokit.pulls.get({
813
992
  owner,
814
993
  repo: repoName,
815
- ref: `heads/${targetBranch}`,
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: `✅ **Reproduction Test Committed:** \`${testFileName}\` has been committed directly to branch \`${targetBranch}\` (Commit: \`${newCommit.sha.slice(0, 7)}\`) by @${sender}.`,
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
  });