Repository navigation
Conversation
…utofix, and skipped checks
- hooks/index.js: subprocess killed by a signal sets result.status to null, so
process.exitCode was assigned null (→ 0) and the gate silently passed.
Fix: `process.exitCode = result.status ?? (result.signal ? 1 : 0)`.
- review-gate.js autofix loop: when pushBranch returns ok=false the loop
previously broke and fell through to the generic "Auto-fix did not clear them"
message, hiding the real cause. Fix: throw with the push failure details.
- review-gate.js allAccountedFor: SKIPPED checks were not counted in either
formula, so a PR where every non-pending check is success/waived/skipped
never satisfied allAccountedFor and polled until the 30-min timeout.
Fix: add count('skipped') to both the baseline and normal paths.
Tests: added signal-kill exit test in hook-help.test.js; added skipped-check
(with and without GitHub verdict) and push-failure tests in review-gate.test.js;
updated the prior skipped+billing-waiver timeout test to assert the now-correct
green result.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d-changed guard on non-Lens path - commandForFix now passes the fix prompt via stdin (input: + stdio 'pipe') for both claude and codex, matching the review path. The prompt no longer appears in the process argument vector and cannot exceed ARG_MAX. - runPrReview captures the PR head SHA before runProviderReviewPayload so the mismatch guard in postGithubReview fires on all paths, not just the PR Lens path where reviewedHeadSha was previously set. - Tests updated for the new commandForFix shape and stdio/input contract. - New test: commandForFix delivers the prompt via stdin for both providers. - New integration test: runPrReview aborts posting when the PR head advances between the pre-review snapshot and the post. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…posure
- review-bot-watch.sh: add sanitize_untrusted_str() to strip control
chars and cap length on PR-author-controlled fields; wrap pr_title
and head_branch in a clearly delimited UNTRUSTED PR DATA block so the
agent treats them as data, not instructions
- review-bot-watch.sh: add ${2:-base} default to sanitize_slug fallback
parameter so the function is safe under set -u when called without a
second argument
- sync-frontend-mirror.sh: replace token-in-remote-URL push with
GIT_CONFIG_COUNT/KEY/VALUE http.extraHeader so the credential never
appears in argv or the git remote URL
- sync-frontend-mirror.sh: add --token-file <path> option; print a
deprecation warning when --token is used so callers can migrate to
env-var or file-based credential delivery
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n; sanitize pr_url - fetchHeadSha in runPrReview was called unconditionally before the provider run, adding an extra gh-pr-view call on every non-posting invocation. Tests whose fake runner identified the provider call as "first call that is not gh-pr-diff" now picked up the fetchHeadSha call instead, getting timeout=60000 instead of the expected 900000/42000. Fix: guard the call with options.post so it only fires on the posting path where the mismatch guard is actually used. The head-changed integration test (pr-review.test.js:743) continues to pass because it invokes with --post. - scripts/sync-frontend-mirror.sh: token-file stripping now removes both \r and \n so Windows-line-ending token files do not leave a trailing CR in the header. - templates/scripts/review-bot-watch.sh: sanitize pr_url through sanitize_untrusted_str (same as pr_title) before embedding it in the prompt to close a prompt-injection vector via a crafted PR URL. Fixes 4 new test failures introduced by commit 9f5f933: - the provider spawn is bounded (pr-review-timeout.test.js) - an explicit timeoutMs wins over the default (pr-review-timeout.test.js) - a garbage override falls back to the default (pr-review-timeout.test.js) - disabled integration does not invoke PR Lens (pr-lens.test.js) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-safety-bugs-in-hooks-and-review-2026-10-27-12-17
…supported) GitHub's HTTPS Git transport expects the token as a Basic-auth password (x-access-token:<token> base64-encoded), not as a Bearer token. The previous Authorization: Bearer header causes push failures with git's http.extraHeader. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…review race Move fetchHeadSha above fetchPrDiff so the head snapshot always precedes the diff fetch. If the PR advances between these two calls, the mismatch guard in postGithubReview detects it and rejects the stale finding set rather than posting it against the new commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n post path fetchHeadSha returns '' on a gh error. Without this guard the posting guard in postGithubReview receives an empty reviewedHeadSha, which disables the concurrent-push check entirely. Throw early so callers never post a review that cannot be anchored to a known head SHA. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-safety-bugs-in-hooks-and-review-2026-10-27-12-17
The guard added in fb35833 threw unconditionally when fetchHeadSha returned '' on the --post path. fetchHeadSha also returns '' when GitHub auth is not available (gh pr view exits non-zero), so the throw fired before the githubAuthAvailable fallback at line 1000 could catch the no-auth case and write the markdown artifact instead. Fix: gate the throw on githubAuthAvailable — only abort when auth IS reachable but the SHA still could not be captured. When auth is unavailable, proceed past the check; the existing fallback writes the artifact and exits 0 as intended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed its own header Use the github.com-scoped extraheader key and reset it with an empty value first; git sends every value of that multi-valued key otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ble, MSS hints, lightweight head-poll - claude install: gate the success banner on the real MCP outcome; when installMcpServer returns ownership-unavailable, emit a logWarn explaining the file conflict and what to do, rather than printing the ✅ "wiring is in place" line that implies full success. - review-gate merge-blocked: map raw mergeStateStatus enums to actionable hints in the thrown error — DIRTY→resolve conflicts (gx pr sync / merge base), BEHIND→update branch, BLOCKED→required reviews/conversations, UNSTABLE→wait or check failing checks. - waitForPullRequestHead: default getStatus now calls pr.getPrHeadOid (headRefOid-only query) instead of pr.getPullRequestStatus (full statusCheckRollup) so each 2-second poll is a lightweight gh pr list --json headRefOid fetch rather than a full status snapshot. - Adds pr.getPrHeadOid lightweight helper and exports it from pr.js. - Focused tests: banner success/warning paths; DIRTY/BEHIND/UNSTABLE MSS hint text in runReviewGate error messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-safety-bugs-in-hooks-and-review-2026-10-27-12-17
…hreads
Add `gx pr respond --pr <n>` (src/pr-respond.js + wired into
src/cli/commands/pr.js) which:
- Fetches unresolved, non-outdated review threads whose first comment is
CRITICAL/HIGH (gitguardex review markers) or from a human reviewer who
submitted a CHANGES_REQUESTED review.
- Converts threads to findings and runs the existing runReviewFix path in
the PR's local agent/* worktree.
- On a successful fix: pushes the commit, re-runs gx pr-review (--post),
then replies + resolves only the threads whose files changed.
- Refuses forks, non-agent/* branches, missing/dirty worktrees.
- Supports --dry-run (prints what would be fixed without touching anything).
Export worktreePathForBranch from src/agents/inspect.js (previously private).
Wire opt-in auto-respond into templates/scripts/review-bot-watch.sh behind
GUARDEX_REVIEW_AUTO_RESPOND=1: calls gx pr respond for each agent/* PR seen
in the poll loop (both on new-SHA dispatch and on unchanged-SHA polling).
Add 30 focused tests in test/pr-respond.test.js covering body-parsing
helpers, thread filtering, refusal logic, dry-run, and GraphQL
mutation wrappers via fake gh runners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
3 finding(s) — 🟠 1 high · 🟡 2 medium
⛔ Merge gate: blocked — 1 blocking finding(s) (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟠 high | src/pr-respond.js:508 |
Changing a file causes every blocking thread on that file to be resolved without confirming each defect was fixed. |
| 🟡 medium | src/pr-respond.js:123 |
Filtering reviews to CHANGES_REQUESTED includes historical requests that a reviewer has subsequently approved or dismissed. |
| 🟡 medium | src/pr-respond.js:499 |
Diagnostic logging corrupts the CLI's JSON output. When re-review throws, this console.log writes a plain-text prefix before cmdRespond |
Provider codex · commit 02d9d8e · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| const skippedReply = []; | ||
| for (let index = 0; index < threads.length; index += 1) { | ||
| const thread = threads[index]; | ||
| if (!changedFiles.has(thread.path)) { |
There was a problem hiding this comment.
Warning
🟠 HIGH · correctness
Changing a file causes every blocking thread on that file to be resolved without confirming each defect was fixed. When two threads concern different bugs in the same file, fixing only one clears both conversations; resolution also proceeds when re-review fails. This can remove blocking feedback while the reported defect remains.
| 'query($owner:String!,$name:String!,$pr:Int!){', | ||
| 'viewer{login}', | ||
| 'repository(owner:$owner,name:$name){pullRequest(number:$pr){', | ||
| 'reviews(states:[CHANGES_REQUESTED],last:20){nodes{author{login}}}', |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
Filtering reviews to CHANGES_REQUESTED includes historical requests that a reviewer has subsequently approved or dismissed. The resulting reviewer set does not represent the latest review state, so auto-respond can modify code for feedback that no longer blocks the PR. Fetch the latest review state per author and select only currently active change requests.
| ); | ||
| } catch (error) { | ||
| // Non-fatal: fix is already pushed. Log but don't fail the whole respond. | ||
| console.log(`${TOOL_PREFIX} re-review failed (fix is pushed): ${error.message}`); |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
Diagnostic logging corrupts the CLI's JSON output. When re-review throws, this console.log writes a plain-text prefix before cmdRespond serializes the result, making --json output invalid; reply and resolution failures have the same problem. Send these diagnostics to stderr at all three logging sites.
Auto-formatted by `npx biome format --write`; no logic change. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pproved-after-CR Covers the new latest-review-state logic in fetchBlockingThreads: a reviewer who issued CHANGES_REQUESTED then APPROVED should not keep a thread blocking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
3 finding(s) — 🟠 1 high · 🟡 2 medium
⛔ Merge gate: blocked — 1 blocking finding(s) (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟠 high | src/pr-respond.js:542 |
Changing a file automatically resolves every blocking thread in that file without confirming each defect was fixed. |
| 🟡 medium | src/pr-respond.js:182 |
A subsequent COMMENTED or PENDING review incorrectly clears an active change request. |
| 🟡 medium | src/pr-respond.js:127 |
Review thread coordinates are used without checking which side of the diff they address. |
Provider codex · commit 68fd91a · abide: skipped (no rubric (.abide/rubric.json); compile it first)
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
2 finding(s) — 🟠 1 high · 🟡 1 medium
⛔ Merge gate: blocked — 1 blocking finding(s) (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟠 high | src/pr-respond.js:526 |
A completed re-review does not prove that a thread's defect was fixed. |
| 🟡 medium | src/pr-respond.js:400 |
The local worktree is selected without verifying that its HEAD matches the PR head. |
Provider codex · commit 68fd91a · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| skippedReply.push(thread.id); | ||
| continue; | ||
| } | ||
| if (reReviewResult === null) { |
There was a problem hiding this comment.
Warning
🟠 HIGH · correctness
A completed re-review does not prove that a thread's defect was fixed. This condition permits automatic resolution whenever the review returns any non-null result and the file changed, even if the review still reports the original HIGH/CRITICAL finding or another thread in the same file was the only issue addressed. This can clear blocking conversations while their defects remain.
Why this matters
The added tests do not exercise this resolution path.
| } | ||
|
|
||
| // 2. Resolve the local worktree for the branch. | ||
| const { worktreePath, worktreeFound } = worktreePathForBranch(repoRoot, branch); |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
The local worktree is selected without verifying that its HEAD matches the PR head. A clean worktree can be behind after another contributor pushes; the responder then applies current review locations to stale code and commits a fix that the subsequent non-fast-forward push rejects. Fetch the PR head SHA and require or safely establish matching local HEAD before running the fix provider.
…tate reviewer check; HEAD sync guard Review findings addressed (pass 2/2): HIGH (line 526): thread resolution now cross-checks the re-review findings set. A thread is only resolved when re-review returns no remaining blocking finding on that file, so two bugs on the same file do not both get dismissed after fixing only one. Builds on the earlier null guard (re-review failure = no resolution). MEDIUM (line 399): verify worktree HEAD matches origin before running the fix provider. After a successful git fetch, compare HEAD to refs/remotes/origin/<branch>; refuse if they differ. Skipped when fetch fails (no remote / offline), so test repos without a remote still pass. MEDIUM (line 123): already landed in b61550c; this commit carries the biome format-only delta left over from the previous auto-commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
2 finding(s) — 🟠 1 high · 🟡 1 medium
⛔ Merge gate: blocked — 1 blocking finding(s) (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟠 high | src/pr-respond.js:572 |
Human review threads can be resolved without their requested changes being implemented. |
| 🟡 medium | src/pr-respond.js:126 |
The review-state lookup silently excludes reviewers outside the latest 50 reviews. |
Provider codex · commit 5654100 · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| skippedReply.push(thread.id); | ||
| continue; | ||
| } | ||
| if (reReviewRemainingPaths.has(thread.path)) { |
There was a problem hiding this comment.
Warning
🟠 HIGH · correctness
Human review threads can be resolved without their requested changes being implemented. Touching the file and receiving no HIGH/CRITICAL findings from a fresh general review does not verify each original request. If two human threads concern the same file and only one is fixed, both can be automatically resolved, clearing unresolved review conversations.
Why this matters
Require confirmation against each original finding before resolving its thread, and cover partial fixes with a regression test.
| 'query($owner:String!,$name:String!,$pr:Int!){', | ||
| 'viewer{login}', | ||
| 'repository(owner:$owner,name:$name){pullRequest(number:$pr){', | ||
| 'reviews(last:50){nodes{author{login}state}}', |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
The review-state lookup silently excludes reviewers outside the latest 50 reviews. A reviewer with an older active CHANGES_REQUESTED review disappears from humanReviewers after enough subsequent reviews, so their unresolved threads are ignored and the command can incorrectly report no blocking threads. Paginate the review connection or explicitly refuse incomplete review history, as the thread lookup already does.
…d review history
Review findings addressed (pass 3/3):
HIGH (line 572): human reviewer threads are no longer auto-resolved. Only
gitguardex-bot threads (structured findings targeted by the fix provider)
are resolved automatically. Human threads receive a reply but stay open for
the reviewer to confirm their specific request was addressed.
MEDIUM (line 126): add pageInfo{hasNextPage} to the reviews query. If the PR
has more than 50 reviews the oldest fall off and a CHANGES_REQUESTED reviewer
could be silently excluded; now throws with an actionable message rather than
auto-fixing with incomplete reviewer state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
2 finding(s) — 🟠 1 high · 🟡 1 medium
⛔ Merge gate: blocked — 1 blocking finding(s) (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟠 high | src/pr-respond.js:439 |
A failed fetch bypasses the stale-worktree safeguard and still allows automatic edits. |
| 🟡 medium | src/pr-respond.js:201 |
The review-history truncation check uses the wrong pagination direction. |
Provider codex · commit 67439ae · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| // excluded. Refuse rather than auto-fix with incomplete information. | ||
| const rawReviews = payload?.data?.repository?.pullRequest?.reviews; | ||
| if (rawReviews?.pageInfo?.hasNextPage) { | ||
| throw new Error( |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
The review-history truncation check uses the wrong pagination direction. Because reviews are fetched with last:50, omitted older reviews are indicated by hasPreviousPage, while hasNextPage remains false. An active CHANGES_REQUESTED reviewer whose later interactions were only comments can therefore disappear from the computed blocking-reviewer set, silently skipping their unresolved threads.
| // If we can reach origin, verify the local HEAD matches to prevent applying the fix | ||
| // to a stale worktree (a diverged push would fail non-fast-forward, but checking here | ||
| // gives a clearer error sooner). | ||
| const fetchResult = gitRun(worktreePath, ['fetch', 'origin', branch], { allowFailure: true }); |
There was a problem hiding this comment.
Warning
🟠 HIGH · correctness
A failed fetch bypasses the stale-worktree safeguard and still allows automatic edits. Git fetch can fail independently of the GitHub API, leaving current review coordinates applied to an older local checkout; the later push rejection occurs only after the provider has modified and committed files. Refuse the run when the remote head cannot be verified.
| const fetchResult = gitRun(worktreePath, ['fetch', 'origin', branch], { allowFailure: true }); | |
| if (fetchResult.status !== 0) { | |
| return { | |
| status: 'skipped', | |
| reason: 'could not fetch the PR branch from origin; cannot verify worktree HEAD' | |
| }; | |
| } | |
| if (fetchResult.status === 0) { |
…ion direction for reviews Review findings addressed (pass 4/4): HIGH (line 439): if git fetch fails and origin is configured, refuse the run rather than proceeding with potentially stale code. If origin is absent (local-only repo), the check is skipped as before. Tests unaffected because test repos have no remote origin. MEDIUM (line 201): 'last:50' fetches the most recent 50 reviews; older ones fall off the front of the connection, so the correct pagination flag is hasPreviousPage (not hasNextPage). Added test to cover the throw. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
1 finding(s) — 🟡 1 medium
✅ Merge gate: pass — no blocking findings (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟡 medium | src/pr-respond.js:453-456 |
The freshness check reads a tracking ref that the fetch may not update. |
Provider codex · commit 1068a90 · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| ).trim(); | ||
| const remoteHead = String( | ||
| gitRun(worktreePath, ['rev-parse', `refs/remotes/origin/${branch}`], { allowFailure: true }) | ||
| .stdout || '' |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
The freshness check reads a tracking ref that the fetch may not update. When origin's configured fetch refspec excludes this branch, git fetch origin <branch> updates FETCH_HEAD while an existing origin/ ref remains stale. If the worktree matches that stale ref, this check accepts outdated code and applies review suggestions using coordinates from a newer PR revision.
| ).trim(); | |
| const remoteHead = String( | |
| gitRun(worktreePath, ['rev-parse', `refs/remotes/origin/${branch}`], { allowFailure: true }) | |
| .stdout || '' | |
| const remoteHead = String( | |
| gitRun(worktreePath, ['rev-parse', 'FETCH_HEAD'], { allowFailure: true }) | |
| .stdout || '' | |
| ).trim(); |
…NDING filter tests MEDIUM (line 453): git fetch origin <branch> always writes FETCH_HEAD with the remote SHA regardless of whether the tracking ref is updated — use FETCH_HEAD instead of refs/remotes/origin/<branch> so the comparison is always against what was actually just fetched. Also includes 2 auto-generated tests covering the COMMENTED/PENDING state filter in the latest-review-state computation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
2 finding(s) — 🟠 2 high
⛔ Merge gate: blocked — 2 blocking finding(s) (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟠 high | src/pr-respond.js:50 |
The responder never verifies that the local commit matches the PR head. |
| 🟠 high | src/pr-respond.js:587 |
Thread resolution fails open when re-review returns no findings array. |
Provider codex · commit 92cc97e · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| function fetchPrMeta(pr, repoRoot, runner = run) { | ||
| const result = runner( | ||
| GH_BIN, | ||
| ['pr', 'view', String(pr), '--json', 'headRefName,isCrossRepository'], |
There was a problem hiding this comment.
Warning
🟠 HIGH · correctness
The responder never verifies that the local commit matches the PR head. Metadata contains only the branch name, while the later freshness check compares against origin, which can differ from the repository selected by gh. A matching branch in another remote can therefore receive fixes derived from unrelated PR coordinates, and the responder can subsequently resolve threads on the original PR.
| ['pr', 'view', String(pr), '--json', 'headRefName,isCrossRepository'], | |
| ['pr', 'view', String(pr), '--json', 'headRefName,isCrossRepository,headRefOid,headRepository'], |
| const thread = threads[index]; | ||
| if (!changedFiles.has(thread.path)) { | ||
| skippedReply.push(thread.id); | ||
| continue; |
There was a problem hiding this comment.
Warning
🟠 HIGH · correctness
Thread resolution fails open when re-review returns no findings array. The preceding code treats a missing findings array as zero remaining blockers, but this guard rejects only null. A skipped or otherwise incomplete review result can therefore allow unresolved blocking threads to be automatically marked resolved without confirmation.
| continue; | |
| if (!reReviewResult || !Array.isArray(reReviewResult.findings)) { |
…/PENDING, reReviewRemainingPaths Fixes three correctness issues surfaced by the AI code review: HIGH: thread resolution was too broad — a file being touched was sufficient to resolve all its blocking threads even when the re-review still reported blocking findings for that file. Added `reReviewRemainingPaths` Set: only resolve threads whose file has no remaining HIGH/CRITICAL findings in the re-review result. MEDIUM: COMMENTED and PENDING reviews were overwriting CHANGES_REQUESTED in the latest-review-state map, silently clearing a reviewer's block. These two states carry no approval/dismissal semantics and are now skipped when building the map. MEDIUM: LEFT-side diff threads reference deleted lines that no longer exist in the current file; forwarding those coordinates to the fix provider could apply suggestions to unrelated surviving code. Added `diffSide` to the GraphQL query and filter LEFT-side threads before returning. Bonus: added a `fetchResult.status === 0` guard around the HEAD/origin sync check so it is skipped for repos without a real origin remote (keeps tests clean). Tests: +3 tests for COMMENTED-does-not-clear-CR, LEFT-side exclusion, and reviewer-approved-after-CR. All 1576 tests pass (0 failures, 5 known skips). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update the fetchPrMeta test to include headRefOid in the gh pr view stub and assert it is returned, covering the PR head SHA verification path added in the previous commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
1 finding(s) — 🟡 1 medium
✅ Merge gate: pass — no blocking findings (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟡 medium | test/pr-respond.test.js:765-768 |
The fixture cannot create the agent worktree. If checkout succeeds, the branch is already checked out in repoDir, so the subsequent |
Provider codex · commit cbbb329 · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| cp.spawnSync('git', ['-C', repoDir, 'checkout', '-b', 'agent/empty-fix', '--track'], { | ||
| encoding: 'utf8', | ||
| env: { ...process.env, GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH: '1' } | ||
| }); |
There was a problem hiding this comment.
Important
🟡 MEDIUM · tests
The fixture cannot create the agent worktree. If checkout succeeds, the branch is already checked out in repoDir, so the subsequent worktree add refuses it; if --track fails, the branch does not exist. Both command failures are ignored, and prRespond returns skipped instead of no-threads. The dry-run fixture has the same problem.
| cp.spawnSync('git', ['-C', repoDir, 'checkout', '-b', 'agent/empty-fix', '--track'], { | |
| encoding: 'utf8', | |
| env: { ...process.env, GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH: '1' } | |
| }); | |
| cp.spawnSync('git', ['-C', repoDir, 'branch', 'agent/empty-fix'], { | |
| encoding: 'utf8' | |
| }); |
…eads tests Use 'git branch <name>' instead of 'git checkout -b <name> --track' so the branch is not checked out in the main working tree. 'git worktree add' refuses branches already checked out elsewhere, so the old setup silently failed and the tests passed only by accident (prRespond returned 'skipped' instead of the asserted 'dry-run'/'no-threads'). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
1 finding(s) — 🟡 1 medium
✅ Merge gate: pass — no blocking findings (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟡 medium | templates/scripts/review-bot-watch.sh:451 |
Unchanged human review threads trigger the fix provider on every poll cycle. |
Provider codex · commit aa8f085 · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| # Even when the SHA hasn't changed, auto-respond handles any new review | ||
| # comments that arrived since the last dispatch. | ||
| if [[ "$AUTO_RESPOND" == "1" ]]; then | ||
| try_respond_pr "$pr" "$head_branch" |
There was a problem hiding this comment.
Important
🟡 MEDIUM · performance
Unchanged human review threads trigger the fix provider on every poll cycle. prRespond deliberately leaves human threads unresolved, but fetchBlockingThreads keeps selecting them after a fix and reply. With auto-respond enabled, waiting for reviewer confirmation repeatedly invokes the provider and can produce duplicate replies or additional commits without new reviewer input.
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
1 finding(s) — 🟡 1 medium
✅ Merge gate: pass — no blocking findings (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟡 medium | test/pr-respond.test.js:814 |
The successful auto-fix workflow has no test coverage. These tests stop at refusal, dry-run, or no-threads paths, so they never exercise |
Provider codex · commit aa8f085 · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| cp.spawnSync('git', ['-C', repoDir, 'worktree', 'remove', wtDir, '--force'], { | ||
| encoding: 'utf8' | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Important
🟡 MEDIUM · tests
The successful auto-fix workflow has no test coverage. These tests stop at refusal, dry-run, or no-threads paths, so they never exercise committing, pushing, re-reviewing, or the conditions that automatically resolve GitHub threads. A regression could silently resolve an unfixed blocking thread without failing this suite.
Why this matters
Add workflow tests covering successful resolution, failed re-review, remaining blocking findings, untouched files, and human threads remaining unresolved.
…d re-processing on every poll fetchBlockingThreads was re-queuing human reviewer threads on every poll cycle because prRespond deliberately leaves them unresolved (human must confirm). This caused duplicate replies and spurious fix commits on subsequent auto-respond runs. Fix: add latestComment:comments(last:1) alias to the GraphQL thread query so we can inspect the most recent comment without fetching the full thread. When the latest comment is from the trusted bot author and contains MARKER, the thread is skipped — the bot already replied and is waiting for the reviewer. When the reviewer replies after the bot, the latest comment flips back to the reviewer and the thread is re-queued normally. Tests: 36/36 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
5 finding(s) — 🟠 2 high · 🟡 1 medium · 🔵 2 low
⛔ Merge gate: blocked — 2 blocking finding(s) (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟠 high | src/pr-respond.js:193 |
Any GitHub user who submits a CHANGES_REQUESTED review is treated as a trusted human reviewer, so their free-text comment is fed straight |
| 🟠 high | templates/scripts/review-bot-watch.sh:451 |
Auto-respond runs on every poll cycle even when the head SHA is unchanged, so the same still-open threads are re-fixed over and over. |
| 🟡 medium | test/pr-respond.test.js:883 |
The riskiest part of prRespond has no tests: the post-fix path (fixed, push-failed, the re-review gating, and the reply/resolve loop). |
| 🔵 low | src/pr-respond.js:26-28 |
readProtectedBranches and FINDING_MARKER_PREFIX are imported but never used. |
| 🔵 low | src/finish/review-gate.js:606 |
The BEHIND hint hardcodes 'gx pr sync', while the DIRTY hint right above it uses ${TOOL_NAME}. |
Provider claude · commit d015626 · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| const state = String(review?.state || ''); | ||
| // COMMENTED and PENDING do not represent a review decision — skip them so a | ||
| // comment-only interaction cannot clear an active CHANGES_REQUESTED state. | ||
| if (login && state && state !== 'COMMENTED' && state !== 'PENDING') { |
There was a problem hiding this comment.
Warning
🟠 HIGH · security
Any GitHub user who submits a CHANGES_REQUESTED review is treated as a trusted human reviewer, so their free-text comment is fed straight into the fix provider's prompt. The provider's output is then committed and pushed automatically. On a public repo, non-collaborators can leave CHANGES_REQUESTED reviews, and with GUARDEX_REVIEW_AUTO_RESPOND=1 the watch loop runs this unattended.
Why this matters
That is a prompt-injection path to arbitrary code being written and pushed to agent/* branches. Fix: add authorAssociation to the reviews{nodes{...}} selection, and only add a reviewer to latestStateByReviewer when the association is OWNER, MEMBER or COLLABORATOR, or when the reviewer has write permission.
| # Even when the SHA hasn't changed, auto-respond handles any new review | ||
| # comments that arrived since the last dispatch. | ||
| if [[ "$AUTO_RESPOND" == "1" ]]; then | ||
| try_respond_pr "$pr" "$head_branch" |
There was a problem hiding this comment.
Warning
🟠 HIGH · correctness
Auto-respond runs on every poll cycle even when the head SHA is unchanged, so the same still-open threads are re-fixed over and over. Several outcomes leave threads open and eligible again: provider no-op, a thread whose path was not in changedFiles, re-review still flagging the file, a failed resolve, and bot threads (which, unlike human threads, have no 'already replied' check).
Why this matters
Each repeat costs a provider run, and when it does change something it adds a push and a pr-review --post. That push produces a new SHA, so on the next cycle process_one_pr reviews and posts again, duplicating the re-review respond already posted. Without persisted per-thread or per-SHA attempt state, this can loop indefinitely, burning provider quota and piling up commits and review threads. Fix: record which thread IDs and head SHA were last attempted in the state file, and skip them until the SHA or the thread's latest comment changes.
| const { gitRun, readProtectedBranches, currentBranchName } = require('./git'); | ||
| const { runReviewFix } = require('./review-fix'); | ||
| const { runPrReview, MARKER, FINDING_MARKER_PREFIX } = require('./pr-review'); |
There was a problem hiding this comment.
Note
🔵 LOW · maintainability
readProtectedBranches and FINDING_MARKER_PREFIX are imported but never used. They will fail a no-unused-vars lint rule and suggest a protected-branch check that does not actually happen.
| const { gitRun, readProtectedBranches, currentBranchName } = require('./git'); | |
| const { runReviewFix } = require('./review-fix'); | |
| const { runPrReview, MARKER, FINDING_MARKER_PREFIX } = require('./pr-review'); | |
| const { gitRun, currentBranchName } = require('./git'); | |
| const { runReviewFix } = require('./review-fix'); | |
| const { runPrReview, MARKER } = require('./pr-review'); |
| const mss = (ci.pr && ci.pr.mergeStateStatus) || 'BLOCKED'; | ||
| const mssHints = { | ||
| DIRTY: `resolve conflicts first (${TOOL_NAME} pr sync, or merge the base branch)`, | ||
| BEHIND: 'update the branch to head of base (gx pr sync)', |
There was a problem hiding this comment.
Note
🔵 LOW · maintainability
The BEHIND hint hardcodes 'gx pr sync', while the DIRTY hint right above it uses ${TOOL_NAME}. The two will drift if the CLI name differs or changes.
| BEHIND: 'update the branch to head of base (gx pr sync)', | |
| BEHIND: `update the branch to head of base (${TOOL_NAME} pr sync)`, |
| cp.spawnSync('git', ['-C', repoDir, 'worktree', 'remove', wtDir, '--force'], { | ||
| encoding: 'utf8' | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Important
🟡 MEDIUM · tests
The riskiest part of prRespond has no tests: the post-fix path (fixed, push-failed, the re-review gating, and the reply/resolve loop). Nothing checks that human threads are replied to but never resolved. Nothing checks that threads are left open when re-review throws or still reports a blocking finding on the same path.
Why this matters
Nothing checks that threads on files outside changedFiles are skipped, or that a push failure returns fixSha. The local-HEAD vs headRefOid staleness refusal is also untested. Add runner/deps-injected tests that stub runReviewFix/runPrReview results and assert on resolveThread/replyToThread calls.
… — prompt-injection label, auto-respond backoff, unused imports, BEHIND hint
HIGH: threadToFinding now prefixes human reviewer message bodies with "reviewer: "
so the fix provider treats them as data rather than instructions, preventing
prompt-injection via CHANGES_REQUESTED review comments.
HIGH: review-bot-watch.sh try_respond_pr now accepts a `current_sha` arg and
maintains LAST_RESPOND_SHA / RESPOND_SKIP_REMAINING per PR. After a successful
respond call, the next RESPOND_BACKOFF_POLLS cycles (default 4) are skipped when
the SHA is unchanged — avoiding repeated fix-provider invocations for threads
that are already handled. Backoff resets immediately on SHA change or failure.
LOW: removed unused imports `readProtectedBranches` and `FINDING_MARKER_PREFIX`
from src/pr-respond.js.
LOW: fixed hardcoded "gx pr sync" in BEHIND hint in src/finish/review-gate.js
to use the ${TOOL_NAME} constant, matching the DIRTY hint style.
Biome formatting applied to all changed JS files (format:check:changed).
Tests: 36/36 pr-respond + 62/62 review-gate pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
3 finding(s) — 🟠 1 high · 🟡 2 medium
⛔ Merge gate: blocked — 1 blocking finding(s) (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟠 high | src/pr-respond.js:198 |
Any GitHub user who submits a CHANGES_REQUESTED review can make the auto-fix provider edit, commit and push to the agent branch. |
| 🟡 medium | templates/scripts/review-bot-watch.sh:431-435 |
Any failure of pr respond clears the backoff, so permanent refusals and provider failures re-run on every poll. |
| 🟡 medium | test/pr-respond.test.js:11 |
The riskiest part of prRespond has no tests: the post-fix reply/resolve path and the HEAD-staleness refusal. |
Provider claude · commit 1e186ca · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| } | ||
| } | ||
| // Only treat a reviewer as "blocking" when their most recent review is CHANGES_REQUESTED. | ||
| const humanReviewers = new Set( |
There was a problem hiding this comment.
Warning
🟠 HIGH · security
Any GitHub user who submits a CHANGES_REQUESTED review can make the auto-fix provider edit, commit and push to the agent branch. humanReviewers is built from every review author, with no check of the reviewer's relationship to the repository. On a public repo, anyone with read access can submit a 'Request changes' review.
Why this matters
Their inline comment body then becomes a 'high' finding that drives the codex/claude fix provider in the local worktree, and the result is pushed to origin. This also runs unattended every poll when GUARDEX_REVIEW_AUTO_RESPOND=1. The reviewer: prefix in threadToFinding is not a real boundary against prompt injection. Add authorAssociation to the reviews selection in the query (line 133). Then only count reviewers whose association is OWNER, MEMBER or COLLABORATOR (or who have write permission) before treating their threads as actionable.
| else | ||
| echo "[review-bot-watch] respond: PR #${pr} skipped or failed (exit=${respond_exit})" >&2 | ||
| # Clear backoff on failure so we retry next cycle. | ||
| RESPOND_SKIP_REMAINING["$pr"]=0 | ||
| fi |
There was a problem hiding this comment.
Important
🟡 MEDIUM · performance
Any failure of pr respond clears the backoff, so permanent refusals and provider failures re-run on every poll. Exit 1 covers more than transient errors. It also covers 'skipped' (no local worktree, dirty worktree, HEAD mismatch, more than 100 threads), 'fix-failed' and 'push-failed'. Because the failure branch resets RESPOND_SKIP_REMAINING to 0, each agent PR re-invokes gx pr respond every interval.
Why this matters
For 'fix-failed' that means paying for a full fix-provider run every 5+ seconds on the same unchanged SHA. Arm the same SHA-keyed backoff on failure. A new push still resets it immediately.
| else | |
| echo "[review-bot-watch] respond: PR #${pr} skipped or failed (exit=${respond_exit})" >&2 | |
| # Clear backoff on failure so we retry next cycle. | |
| RESPOND_SKIP_REMAINING["$pr"]=0 | |
| fi | |
| else | |
| echo "[review-bot-watch] respond: PR #${pr} skipped or failed (exit=${respond_exit})" >&2 | |
| # Back off on failure too: permanent refusals (no worktree, fork, dirty tree) | |
| # and provider failures would otherwise re-invoke the fix provider every cycle. | |
| # A new head SHA still resets the backoff immediately. | |
| if [[ -n "$current_sha" ]]; then | |
| LAST_RESPOND_SHA["$pr"]="$current_sha" | |
| RESPOND_SKIP_REMAINING["$pr"]="$RESPOND_BACKOFF_POLLS" | |
| fi | |
| fi |
| seedCommit | ||
| } = require('./helpers/install-test-helpers'); | ||
| const cp = require('node:child_process'); | ||
| const prRespond = require('../src/pr-respond'); |
There was a problem hiding this comment.
Important
🟡 MEDIUM · tests
The riskiest part of prRespond has no tests: the post-fix reply/resolve path and the HEAD-staleness refusal. The suite covers parsing, filtering, refusals, dry-run and no-threads.
Why this matters
It never exercises the 'fixed' branch, so nothing pins these behaviours: a thread is resolved only when its file changed and the re-review reports no blocking finding on that path; all threads stay open when the re-review throws; human threads get a reply but are never resolved; push-failed returns fixSha. The headRefOid mismatch refusal is also untested. A regression in any of these would silently resolve unfixed threads or push without verification. Add prRespond tests that inject runReviewFix / runPrReview via deps and a runner that records the reply and resolve mutations.
…rators (authorAssociation) On public repositories any authenticated GitHub user can submit a CHANGES_REQUESTED review, which would previously allow them to trigger the fix provider. This commit adds a `TRUSTED_REVIEW_ASSOCIATIONS` guard (OWNER/MEMBER/COLLABORATOR): a reviewer is only added to `humanReviewers` when (a) their latest review decision is CHANGES_REQUESTED AND (b) at least one of their reviews carried a trusted association. External contributors (NONE, CONTRIBUTOR, FIRST_TIME_CONTRIBUTOR, FIRST_TIMER, MANNEQUIN) are excluded even when they submit a formal CHANGES_REQUESTED review. Implementation: - Added `TRUSTED_REVIEW_ASSOCIATIONS` constant - GraphQL query now fetches `authorAssociation` on each review node - Review loop tracks trusted associations separately in `trustedReviewerAssocs` - `humanReviewers` filter now requires both CHANGES_REQUESTED AND a trusted assoc Tests: added 'excludes human threads from external contributors (NONE/CONTRIBUTOR association)'; updated all existing human-reviewer fixtures with explicit authorAssociation. 37/37 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
6 finding(s) — 🟡 5 medium · 🔵 1 low
✅ Merge gate: pass — no blocking findings (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟡 medium | src/pr-respond.js:652 |
Threads are resolved even when the re-review was never posted to the PR. |
| 🟡 medium | src/pr-respond.js:614 |
The re-review runs right after git push, without waiting for GitHub's PR head to reach the fix commit. |
| 🟡 medium | src/pr-respond.js:305-307 |
Follow-up comments on human reviewer threads never reach the fix provider. |
| 🟡 medium | templates/scripts/review-bot-watch.sh:433-434 |
Every failed respond attempt clears the backoff, so persistent failures re-run on every poll cycle. |
| 🟡 medium | templates/scripts/review-bot-watch.sh:412-415 |
Auto-respond has no cap on fix rounds per PR. Each successful respond pushes a new SHA. |
| 🔵 low | src/cli/commands/pr.js:470 |
The skipped-thread summary misstates why threads were left open. |
Provider claude · commit 2e377a1 · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| skippedReply.push(thread.id); | ||
| continue; | ||
| } | ||
| if (!reReviewResult || !Array.isArray(reReviewResult.findings)) { |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
Threads are resolved even when the re-review was never posted to the PR. Only a thrown error (null result) or a missing findings array keeps threads open. If runPrReview returns posted: false (for example, GitHub auth was unavailable) or a provider returns an empty findings array, the bot still replies and resolves threads with no published evidence that the fixed diff was examined.
Why this matters
review-gate.js explicitly refuses this case in requirePostedReview, because an unposted or empty review looks exactly like a clean pass.
| if (!reReviewResult || !Array.isArray(reReviewResult.findings)) { | |
| if (!reReviewResult || !reReviewResult.posted || !Array.isArray(reReviewResult.findings)) { |
| // 7. Re-run pr-review with --post so the updated diff is reviewed again. | ||
| let reReviewResult; | ||
| try { | ||
| reReviewResult = runPrReview( |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
The re-review runs right after git push, without waiting for GitHub's PR head to reach the fix commit. GitHub updates headRefOid asynchronously, so runPrReview({ pr }) can fetch and post a review of the pre-fix diff. That stale review then drives thread resolution. Real fixes may be reported as still blocking, and a stale clean verdict can resolve threads.
Why this matters
review-gate.js guards this same transition with waitForPullRequestHead after every push. Call it, or an equivalent poll against fixSha, before re-reviewing, and treat a timeout as 'leave threads open'.
| const rawMessage = isBot | ||
| ? messageFromBody(body) | ||
| : `reviewer: ${body.replace(/\s+/g, ' ').trim()}`; |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
Follow-up comments on human reviewer threads never reach the fix provider. fetchBlockingThreads re-queues a human thread when the reviewer replies after the bot (for example, 'Actually, also handle the timeout case'), but threadToFinding builds the message only from comments.nodes[0]. The provider re-applies the original request and ignores the follow-up.
Why this matters
The bot then replies 'Applied fix' and stops re-queuing, which buries the reviewer's actual new request. Include the latest comment when its author is the same trusted reviewer. Do not include it for arbitrary repliers, so untrusted text stays out of the prompt.
| # Clear backoff on failure so we retry next cycle. | ||
| RESPOND_SKIP_REMAINING["$pr"]=0 |
There was a problem hiding this comment.
Important
🟡 MEDIUM · performance
Every failed respond attempt clears the backoff, so persistent failures re-run on every poll cycle. Respond exits 1 for deterministic refusals: no local worktree (common for agent PRs created on another machine), dirty worktree, HEAD mismatch, more than 50 reviews or more than 100 threads. It also exits 1 for fix-failed.
Why this matters
With the backoff cleared, the watcher repeats several gh/GraphQL calls per PR per interval indefinitely. On fix-failed it re-invokes the paid fix provider every cycle on the same SHA. Arm the backoff on failure too. A new SHA still resets it through the else branch above.
| # Clear backoff on failure so we retry next cycle. | |
| RESPOND_SKIP_REMAINING["$pr"]=0 | |
| # Back off on failure too: deterministic refusals (no local worktree, dirty | |
| # worktree) and provider failures would otherwise re-run every poll. A new | |
| # head SHA still resets the backoff above. | |
| RESPOND_SKIP_REMAINING["$pr"]="$RESPOND_BACKOFF_POLLS" |
| else | ||
| # SHA changed (or first run): reset backoff for this PR. | ||
| LAST_RESPOND_SHA["$pr"]="${current_sha:-}" | ||
| RESPOND_SKIP_REMAINING["$pr"]=0 |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
Auto-respond has no cap on fix rounds per PR. Each successful respond pushes a new SHA. The next poll sees a changed SHA, resets the backoff, re-reviews via process_one_pr, and responds again. If the provider keeps producing fixes that the review still flags, the loop runs unbounded, with provider cost and push noise on every cycle. The review gate bounds this with maxFixRounds.
Why this matters
Track a per-PR respond-round counter here and stop after a configurable limit (for example, GUARDEX_RESPOND_MAX_ROUNDS).
| `PR #${opts.pr}: fixed ${result.changedFiles.length} file(s), pushed ${result.fixSha ? result.fixSha.slice(0, 8) : ''}, resolved ${result.resolved.length}/${result.threads.length} thread(s).`, | ||
| ); | ||
| if (result.skippedReply.length > 0) { | ||
| logInfo(` ${result.skippedReply.length} thread(s) untouched (path not in changed files).`); |
There was a problem hiding this comment.
Note
🔵 LOW · maintainability
The skipped-thread summary misstates why threads were left open. skippedReply also includes threads whose file changed but the re-review was unavailable or still blocking, and human-reviewer threads that were replied to but intentionally left unresolved. Labeling all of them 'untouched (path not in changed files)' misleads the operator about what happened.
| logInfo(` ${result.skippedReply.length} thread(s) untouched (path not in changed files).`); | |
| logInfo(` ${result.skippedReply.length} thread(s) left open (file unchanged, re-review unconfirmed/still blocking, or awaiting human reviewer).`); |
…tead of clearing to zero Clearing RESPOND_SKIP_REMAINING to 0 on every failure meant that permanent failures (worktree absent, fork PR, non-agent branch) retried on every single poll cycle — the same behaviour the backoff was introduced to prevent. Use a 1-poll backoff on failure: transient errors (network blip, dirty worktree) are retried on the next cycle, permanent errors back off by one poll each time rather than hammering continuously. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NagyVikt
left a comment
There was a problem hiding this comment.
🛡️ GitGuardex code-assist
4 finding(s) — 🟡 3 medium · 🔵 1 low
✅ Merge gate: pass — no blocking findings (blocks on high/critical).
| Severity | Location | Finding |
|---|---|---|
| 🟡 medium | templates/scripts/review-bot-watch.sh:388-391 |
declare -A runs at top level on every start, so the watcher crashes on bash 3.2 (macOS /bin/bash) even when auto-respond is off. |
| 🟡 medium | src/pr-respond.js:614-624 |
The re-review runs right after git push without waiting for GitHub to report the new PR head, so it can review and post against the stale |
| 🟡 medium | src/pr-respond.js:601 |
The 'fixed' path has no test coverage: push, re-review, reply, and the rules for resolving or skipping threads. |
| 🔵 low | src/cli/commands/pr.js:470 |
This summary line gives the wrong reason for most skipped threads. |
Provider claude · commit 773cbbf · abide: skipped (no rubric (.abide/rubric.json); compile it first)
| # backoff resets immediately when the SHA changes or when respond exits non-zero | ||
| # (so a transient failure is retried on the next cycle). | ||
| declare -A LAST_RESPOND_SHA | ||
| declare -A RESPOND_SKIP_REMAINING |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
declare -A runs at top level on every start, so the watcher crashes on bash 3.2 (macOS /bin/bash) even when auto-respond is off. bash 3.2 rejects declare -A with "invalid option". Under the script's set -e, that error kills the watcher for every user, not only those who set GUARDEX_REVIEW_AUTO_RESPOND=1. PR numbers are integers, so plain indexed arrays work here and need no associative arrays.
Why this matters
The comment on lines 388-389 also says the backoff resets on a non-zero exit, but try_respond_pr actually arms a 1-poll backoff.
| # backoff resets immediately when the SHA changes or when respond exits non-zero | |
| # (so a transient failure is retried on the next cycle). | |
| declare -A LAST_RESPOND_SHA | |
| declare -A RESPOND_SKIP_REMAINING | |
| # backoff resets immediately when the SHA changes; a non-zero respond exit arms a | |
| # 1-poll mini-backoff so transient failures retry soon without hammering every cycle. | |
| # PR numbers are integers, so plain indexed arrays work here and stay compatible | |
| # with bash 3.2 (macOS /bin/bash), which has no `declare -A`. | |
| LAST_RESPOND_SHA=() | |
| RESPOND_SKIP_REMAINING=() |
| reReviewResult = runPrReview( | ||
| { | ||
| pr, | ||
| target: worktreePath, | ||
| provider: options.provider || 'codex', | ||
| model: options.model, | ||
| post: true, | ||
| timeoutMs: options.timeoutMs | ||
| }, | ||
| deps | ||
| ); |
There was a problem hiding this comment.
Important
🟡 MEDIUM · correctness
The re-review runs right after git push without waiting for GitHub to report the new PR head, so it can review and post against the stale pre-fix diff. review-gate.js waits with waitForPullRequestHead before re-reviewing for exactly this reason ("Refusing to re-review a stale PR diff"). Without that wait, the re-review can post duplicate findings for code that is already fixed.
Why this matters
Thread resolution on lines below is then decided by a review of the wrong commit, which can leave fixed threads open or resolve threads on the wrong evidence. Wait for the PR head to equal the pushed fix SHA, and treat a timeout like a failed re-review, before calling runPrReview.
| } | ||
|
|
||
| // fixResult.status === 'fixed' | ||
| const changedFiles = new Set(fixResult.changedFiles); |
There was a problem hiding this comment.
Important
🟡 MEDIUM · tests
The 'fixed' path has no test coverage: push, re-review, reply, and the rules for resolving or skipping threads. This is the riskiest logic in the PR. It decides when threads get resolved: a bot thread is resolved only if its file changed and the re-review has no blocking finding on that path. Human threads get a reply but are never resolved. A failed re-review leaves all threads open.
Why this matters
A push failure returns 'push-failed' with fixSha. test/pr-respond.test.js covers only the refusal, dry-run, and no-threads paths. Add runner/deps fakes for runReviewFix and runPrReview that assert which thread IDs land in resolved and which land in skippedReply.
| `PR #${opts.pr}: fixed ${result.changedFiles.length} file(s), pushed ${result.fixSha ? result.fixSha.slice(0, 8) : ''}, resolved ${result.resolved.length}/${result.threads.length} thread(s).`, | ||
| ); | ||
| if (result.skippedReply.length > 0) { | ||
| logInfo(` ${result.skippedReply.length} thread(s) untouched (path not in changed files).`); |
There was a problem hiding this comment.
Note
🔵 LOW · maintainability
This summary line gives the wrong reason for most skipped threads. skippedReply also holds human-reviewer threads that were fixed and replied to, threads still flagged by the re-review, and threads left open because the re-review failed. Labelling them all "path not in changed files" sends the operator to the wrong cause.
| logInfo(` ${result.skippedReply.length} thread(s) untouched (path not in changed files).`); | |
| logInfo(` ${result.skippedReply.length} thread(s) left open (file not changed, re-review still blocking or failed, or awaiting human reviewer).`); |
… fixtures) Inline object literals added in the authorAssociation commit exceeded biome's line limit. Formatted to keep CI's format:check:changed green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
What
A collection of gate-safety, security, and UX fixes for
gx claude install,gx pr-review, and the mirror push path, plus a newgx pr respondsub-command for auto-fixing blocking review threads.Why / Fixes
feat(claude-install,review-gate)):gx claude installprinted a green success banner even when ownership was unavailable; now shows an honest warning. Adds lightweight head-poll and actionablemergeStateStatushints for common blocked states (BLOCKED, DIRTY, BEHIND).fix(gate-safety)): Signal-killed hook exits (exit 128+N), push failures in the autofix path, and skipped checks were previously silently treated as success; they now surface as gate failures.fix(pr-review-correctness)): Fix prompt was passed on the command line (leaks intopsoutput); now delivered via stdin. Head-changed guard enforced on non-Lens path.fix(security)): Review-bot prompt injection hardened; mirror token no longer exposed in git remote URL or log output.fix(mirror)×2): Bearer auth is rejected by GitHub HTTPS Git; switched to Basic. Subsequent fix: suppress the mirror token whenactions/checkoutalready persisted its ownAuthorizationheader.fix(pr-review)×3): Capture head SHA before fetching diff to close stale-review race; abort before diff fetch when SHA capture fails; skip SHA abort when GitHub auth is unavailable (local-only runs).gx pr respond(feat(pr-respond)): Reads open review threads, generates fix commits via Claude, and re-runs the gate.Tests
test/claude-install.test.js,test/pr-respond.test.js, andtest/review-gate.test.js.npm run format:check:changedandnpx biome lintboth clean.🤖 Generated with Claude Code