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 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,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 isRemediateCommand = lower.includes("fix") ||
441
- lower.includes("patch") ||
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
- 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({
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
- pull_number: pullNumber,
946
+ username: sender,
758
947
  });
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({
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
- content: Buffer.from(testCode).toString("base64"),
784
- encoding: "base64",
957
+ issue_number: pullNumber,
958
+ body: `⛔ **Access Denied:** Only repository collaborators with write or admin permissions can trigger sandbox check actions.`,
785
959
  });
786
- const { data: baseCommit } = await context.octokit.git.getCommit({
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
- commit_sha: pr.head.sha,
966
+ pull_number: pullNumber,
790
967
  });
791
- const { data: newTree } = await context.octokit.git.createTree({
968
+ const pr = prResponse.data;
969
+ const outputText = checkRun.output?.summary || "";
970
+ const result = await commitReproTestFromSummary({
971
+ octokit: context.octokit,
792
972
  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
- ],
973
+ repoName,
974
+ pullNumber,
975
+ sender,
976
+ summaryText: outputText,
977
+ headSha: pr.head.sha,
978
+ targetBranch: pr.head.ref,
803
979
  });
804
- const { data: newCommit } = await context.octokit.git.createCommit({
980
+ await context.octokit.issues.createComment({
805
981
  owner,
806
982
  repo: repoName,
807
- message: `test: add synthesized adversarial reproduction test for PR #${pullNumber} [skip ci]`,
808
- tree: newTree.sha,
809
- parents: [pr.head.sha],
983
+ issue_number: pullNumber,
984
+ body: result.message,
810
985
  });
811
- addRemediationCommit(newCommit.sha);
812
- await context.octokit.git.updateRef({
986
+ return;
987
+ }
988
+ if (identifier === "rerun_sandbox") {
989
+ const prResponse = await context.octokit.pulls.get({
813
990
  owner,
814
991
  repo: repoName,
815
- ref: `heads/${targetBranch}`,
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: `✅ **Reproduction Test Committed:** \`${testFileName}\` has been committed directly to branch \`${targetBranch}\` (Commit: \`${newCommit.sha.slice(0, 7)}\`) by @${sender}.`,
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
  });