feat: request bot as PR reviewer via GraphQL botIds#11
Conversation
The REST API `POST /pulls/{n}/requested_reviewers` silently ignores bot
accounts — GitHub returns 200 but never adds the bot. The existing
workflow step was dead code. Replace it with the GraphQL `requestReviews`
mutation's `botIds` field, which is the only API that supports requesting
bots as PR reviewers.
The action now self-requests before posting its review: discovers its
identity via `viewer { login }`, resolves the bot's node ID via REST
`GET /users/{slug}[bot]`, and calls `requestReviews` with `botIds`.
Best-effort — failures are logged but never block the review pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 9 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In
requestBotReview, consider extendingbotNodeIdSchema(e.g., to include and validatetype === "Bot") so that you fail fast if the username resolves to a non-bot account rather than just relying on the login naming convention. - To reduce repeated REST calls when the action runs multiple times on the same bot, consider caching the resolved bot
node_idin memory withincreateGithubClientsorequestBotReviewcan skip theusers.getByUsernamelookup after the first success.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `requestBotReview`, consider extending `botNodeIdSchema` (e.g., to include and validate `type === "Bot"`) so that you fail fast if the username resolves to a non-bot account rather than just relying on the login naming convention.
- To reduce repeated REST calls when the action runs multiple times on the same bot, consider caching the resolved bot `node_id` in memory within `createGithubClient` so `requestBotReview` can skip the `users.getByUsername` lookup after the first success.
## Individual Comments
### Comment 1
<location path="src/github/__tests__/client.test.ts" line_range="291-300" />
<code_context>
+ it("discovers the bot identity via REST and requests review via GraphQL", async () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test case for when the `requestReviews` GraphQL mutation itself fails
The new tests cover discovery well, but they’re missing the case where the `requestReviews` mutation (second GraphQL call using `botIds`) rejects. Please add a test where:
- the first `graphql` returns a valid `viewer.login`,
- `getByUsername` returns a valid bot matching `botNodeIdSchema`, and
- the second `graphql` result contains `{ error: new Error("requestReviews failed") }`.
Then assert that `requestBotReview` rejects with that error, so the current behavior (error propagation to `orchestrate`) is documented and protected against future changes.
Suggested implementation:
```typescript
describe("requestBotReview", () => {
it("discovers the bot identity via REST and requests review via GraphQL", async () => {
const stub = makeOctokitStub({
graphqlResponses: [
{ data: { viewer: { login: "umm-actually" } } },
{ data: { requestReviews: { pullRequest: { id: "PR_kwDOMock7" } } } },
],
getByUsernameResponses: [
{
data: {
node_id: "BOT_kgDOEewBdQ",
login: "umm-actually[bot]",
},
},
],
});
// Existing expectations/assertions for the happy path go here
await requestBotReview(stub, {
owner: "acme",
repo: "widgets",
pullNumber: 7,
});
});
it("propagates errors from the requestReviews GraphQL mutation", async () => {
const error = new Error("requestReviews failed");
const stub = makeOctokitStub({
graphqlResponses: [
// First GraphQL call: discover viewer login
{ data: { viewer: { login: "umm-actually" } } },
// Second GraphQL call: requestReviews fails
{ error },
],
getByUsernameResponses: [
{
data: {
node_id: "BOT_kgDOEewBdQ",
login: "umm-actually[bot]",
},
},
],
});
await expect(
requestBotReview(stub, {
owner: "acme",
repo: "widgets",
pullNumber: 7,
}),
).rejects.toBe(error);
});
```
- Ensure the arguments passed to `requestBotReview` (`owner`, `repo`, `pullNumber`) match its actual signature and any existing tests; adjust them if your current test suite uses different names (e.g. `pullRequestNumber`).
- If your `makeOctokitStub` uses a different shape for error responses (e.g. `{ error: { message: string } }` or throws directly), adapt the `graphqlResponses` entry for the second call to match that convention and adjust the expectation (e.g. `rejects.toThrow("requestReviews failed")` instead of `rejects.toBe(error)`).
- If there are existing expectations in the original “discovers the bot identity…” test after the snippet you shared, keep them intact below the `await requestBotReview(...)` call, or move the `await` to wherever it currently is so you don’t lose any assertions.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Node.js 20 is deprecated; the v2 action targets Node 20 and triggers a runner warning. v3.2.0 targets Node 22. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
aliasunder
left a comment
There was a problem hiding this comment.
Phase 1: PR Review
5 findings across 2 files + 1 doc.
Summary
| # | Dim | Severity | File | Description | Disposition |
|---|---|---|---|---|---|
| 1 | D1 | HIGH | src/github/client.ts:225 |
botIds is not a valid field on RequestReviewsInput — should be userIds |
Would fix (trivial) |
| 2 | D1 | HIGH | src/github/__tests__/client.test.ts:314 |
Test assertion checks botIds — should be userIds |
Would fix (trivial) |
| 3 | D1 | MEDIUM | src/github/client.ts:216 |
getByUsername REST error path is untested |
Would fix (trivial) |
| 4 | D4 | LOW | src/github/client.ts:204 |
botNodeIdSchema could validate type: "Bot" |
Would fix (trivial) |
| 5 | D6 | HIGH | AGENTS.md:22 |
Structure tree github/ description incomplete — missing "bot review request" |
Would fix (trivial) |
Finding 5 (body-only): AGENTS.md structure tree
The structure tree comment for github/ currently says "diff fetch, review posting" but the module now also handles bot review requesting via the new requestBotReview method. Update the comment to reflect the full scope:
src/github/ # GitHub I/O: event payload → PrContext, octokit wrappers (diff fetch, review posting, bot review request)
Finding 1: botIds is the critical finding
The requestReviews GraphQL mutation uses botIds as the input field name, but GitHub's GraphQL schema defines RequestReviewsInput with userIds and teamIds fields — not botIds. Bots are users in GitHub's model and must be requested via userIds. The mutation will fail at runtime with a GraphQL validation error.
Verdict: needs-changes — the botIds → userIds fix is required before this works at runtime.
🔍 ship-check · pr-review · deepseek/deepseek-v4-pro
aliasunder
left a comment
There was a problem hiding this comment.
Phase 2: Code Quality
3 findings across 2 files (all on pre-existing code in changed files — not diff-anchorable).
Findings
1. [comments] src/github/event.ts:56 — Redundant comment. The variable name prNumberOverride and the logger.info on the next line already say what's happening. Remove per AGENTS.md: "Comments explain non-obvious domain context; never restate what a self-documenting name already says."
if (prNumberOverride !== undefined) {
2. [structure] src/github/event.ts:85 — Use truthy check over explicit === undefined comparison. After Zod parsing, pull_request is Record<string, unknown> | undefined — an empty object {} is truthy, only undefined is falsy. AGENTS.md: "Truthy/falsy checks over explicit !== undefined comparisons — use if (value) not if (value !== undefined) unless distinguishing undefined from other falsy values actually matters for correctness."
if (!parsed.data.issue.pull_request) {
3. [structure] src/orchestrate.ts:249 — Use truthy check over explicit === undefined comparison. phase is ReviewPhase | undefined where ReviewPhase is an object type (always truthy). !phase is equivalent and preferred per AGENTS.md conventions.
if (!phase) {
By category
- Comments: 1 (redundant comment restating self-documenting code)
- Structure: 2 (explicit
=== undefinedover truthy/falsy checks)
No findings in CI/CD, config, or fixture files.
🔍 ship-check · code-quality · deepseek/deepseek-v4-pro
aliasunder
left a comment
There was a problem hiding this comment.
Phase 3: Test Audit
5 test quality findings, 1 soft coverage gap across 2 test files and 1 production file.
Findings breakdown
| File | Category | Issue |
|---|---|---|
client.test.ts:312 |
assertion | Substring match on deterministic query string |
orchestrate.test.ts:704 |
assertion | Loose matchers (toBeGreaterThanOrEqual, toBeTruthy) on known values |
orchestrate.test.ts:321 |
assertion | Substring toThrow("severity") on deterministic error message |
orchestrate.test.ts:333 |
assertion | Substring toThrow("phases") on deterministic error message |
orchestrate.test.ts:583 |
assertion | Decomposed toContain/not.toContain on changedPaths |
client.ts:216 |
soft gap | getByUsername REST call error propagation not tested |
Pre-existing issues (outside diff — noted in body only)
Three findings are on pre-existing lines outside the PR diff context. They cannot be posted as inline comments:
orchestrate.test.ts:321 — toThrow("severity") is a substring match on a deterministic error message. The exact message is unknown severity_threshold "invalid" — valid: critical | high | medium | low. Use exact match:
await expect(orchestrate(stubs.deps, logger)).rejects.toThrow(
'unknown severity_threshold "invalid" — valid: critical | high | medium | low',
)orchestrate.test.ts:333 — toThrow("phases") is a substring match on a deterministic error message. The exact message is unknown phases value "invalid" — V1 supports only "combined". Use exact match:
await expect(orchestrate(stubs.deps, logger)).rejects.toThrow(
'unknown phases value "invalid" — V1 supports only "combined"',
)orchestrate.test.ts:583 — Decomposed toContain/not.toContain checks on changedPaths elements don't catch extra or missing paths. Assert the full array shape with toEqual(expect.arrayContaining([...])) instead of individual toContain on each element.
Coverage gaps
One soft gap: the requestBotReview function in client.ts has three external calls that can throw — the viewer GraphQL query, the bot user REST lookup, and the requestReviews mutation. Only the viewer query error case is tested. The REST call error case (rate limit, network error) should be covered by a dedicated test.
Tests verified
All new tests for the requestBotReview feature are present and well-structured — the orchestrate.test.ts gains "calls requestBotReview with the PR node ID" and "continues when requestBotReview throws", and client.test.ts gains "discovers the bot identity", "logs a warning when the bot user response is malformed", and "propagates errors from the viewer query". The nodeId field was correctly added to all fixture contexts and expected assertions across all test files.
🔍 ship-check · test-audit · deepseek/deepseek-v4-pro
ship-check pipeline complete (comment mode)Pipeline ran on commit Results
False positive correctionsFindings #1 and #2 from Phase 1 are incorrect — The GraphQL mutation at Deferred: noneVerdict: shipNo bugs, no correctness issues, no blocking findings. All remaining findings are minor (pre-existing style conventions, assertion precision, and one soft coverage gap). 🤖 ship-check pipeline · deepseek/deepseek-v4-pro |
Add tests for requestReviews mutation error, getByUsername REST error, and non-Bot type rejection. Tighten botNodeIdSchema to validate type: "Bot" for defense-in-depth. Replace loose assertions (toContain, toBeTruthy, toBeGreaterThanOrEqual) with exact matches. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
umm-actually re-reviewed at 1 new finding(s) posted as inline comments (2 total across all reviews). umm-actually · deepseek/deepseek-v4-pro |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Sourcery review body finding (review #4689737018): validate Fixed — 🔍 ship-check · pr-monitor · Opus 4.6 (1M context) |
| const pullRequestEventSchema = z.object({ | ||
| pull_request: z.object({ | ||
| number: z.int().positive(), | ||
| node_id: z.string(), |
There was a problem hiding this comment.
[low/correctness] Verify node_id is present in all event payload shapes (confidence: high)
The pull_request_event_schema in event.ts now requires node_id in the event payload, but issue_comment events do not contain pull_request.node_id. When an issue_comment event resolves to needs_fetch and the caller fetches the PR via fetchPullRequest, the node_id field is populated from the REST response — this is correct. However, the Zod schema in event.ts for pull_request_event does not validate that node_id is present, but the fetchPullRequest Zod schema does. If the event payload's node_id is missing for pull_request/pull_request_target events, the Zod parse will fail and the event will be rejected as malformed — this is the intended behavior. No bug found.
Failure scenario: If a pull_request event payload were missing the node_id field, pullRequestEventSchema.safeParse would fail, causing the event to be rejected as kind: 'not_a_pr' with reason malformed pull_request payload. This is intentional and matches the existing behavior for other missing fields.
Suggested fix
The current handling is correct — no change needed.… comments
The dogfooded self-review runs on this PR exposed a live bug: GraphQL
viewer{login} on an app installation token already returns the bot user
(`<slug>[bot]`), so unconditionally appending the suffix built
`<slug>[bot][bot]` — a nonexistent user. That 404ed requestBotReview
(silently broken since #11) and made hasPriorBotReview never match its
own reviews, so re-runs took the first-run path and the summary comment
never appeared. Append the suffix only when absent.
Also, per CodeRabbit's security finding: fetchReviewComments now keeps
only the bot's own comments — anyone could paste an
`<!-- umm-actually:... -->` marker into a review comment and silently
suppress a real future finding at that location.
Plus review feedback: rerun summary wording now claims exactly what
totalCount counts (tracked inline findings — body-only and legacy
findings aren't parseable), detectRerun/fetchExistingAnchors take
(params, logger) per convention, exact whole-payload asserts on re-run
submitReview calls, and listReviewsCalls proofs so hasPriorBotReview
can't silently no-op.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… review blocks (#12) * fix: anchor key dedup uses line proximity instead of title hash The LLM generates different titles for the same conceptual finding across runs, so the sha256(title)-based anchor key never matched existing comments — every finding was treated as new. Replace with file:category:line keys and ±20-line proximity matching. Old-format (title-hash) anchors are gracefully skipped during parsing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: detect re-runs via prior bot review and track live comment positions Re-run detection previously inferred from inline anchors, which zero-findings and body-only first runs never leave behind — every subsequent run re-detected "first run" and posted a brand-new review instead of updating the summary comment. Detection now asks GitHub whether the bot already posted any review on the PR (hasPriorBotReview, reusing the viewer-login identity discovery), so re-runs always route to the summary upsert regardless of what earlier runs posted. Dedup positioning: fetchReviewComments now returns GitHub's live line (kept updated across pushes) plus original_line; extractAnchors prefers the live position over the line embedded at post time, so dedup follows code motion. With motion handled, LINE_PROXIMITY shrinks 20 → 5 — a wide window silently suppresses genuinely new findings in the same file+category, while a narrow one merely risks a visible duplicate. Also: dedup runs before the findings cap so duplicates don't consume slots, re-run reviews now report droppedByCap, and review-bot feedback addressed (AnchorEntry reuse, explicit callback names, immutable extractAnchors via flatMap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): memoize resolveBotLogin and DRY up error formatting Memoize the viewer GraphQL query inside the client factory so requestBotReview and hasPriorBotReview share the result instead of hitting the API twice per run. Replace the remaining inline error formatting in orchestrate step 3.5 with the describeError helper introduced by this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: truthy checks over explicit undefined comparisons Replace === undefined / !== undefined guards with truthy/falsy checks across comment-mapping.ts and orchestrate.ts, per project convention. All guarded values are objects or positive integers where 0/false/"" are never valid. Also simplify .filter(section !== "") to .filter(Boolean). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: cover botLoginCache memoization, hasPriorBotReview page cap, and tighten pagination assertions Add missing test coverage for the Phase 1 memoization of resolveBotLogin (verifies only one viewer query across requestBotReview + hasPriorBotReview) and the hasPriorBotReview page cap warning (mirrors the existing fetchReviewComments cap test). Tighten the pagination test from toMatchObject to full toEqual per project assertion conventions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: normalize bot login suffix; filter dedup anchors to bot-authored comments The dogfooded self-review runs on this PR exposed a live bug: GraphQL viewer{login} on an app installation token already returns the bot user (`<slug>[bot]`), so unconditionally appending the suffix built `<slug>[bot][bot]` — a nonexistent user. That 404ed requestBotReview (silently broken since #11) and made hasPriorBotReview never match its own reviews, so re-runs took the first-run path and the summary comment never appeared. Append the suffix only when absent. Also, per CodeRabbit's security finding: fetchReviewComments now keeps only the bot's own comments — anyone could paste an `<!-- umm-actually:... -->` marker into a review comment and silently suppress a real future finding at that location. Plus review feedback: rerun summary wording now claims exactly what totalCount counts (tracked inline findings — body-only and legacy findings aren't parseable), detectRerun/fetchExistingAnchors take (params, logger) per convention, exact whole-payload asserts on re-run submitReview calls, and listReviewsCalls proofs so hasPriorBotReview can't silently no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: stop guard-skipped comment runs from canceling in-flight reviews Any PR comment without the @umm review trigger phrase still creates a workflow run that joins the self-review concurrency group before the job guard skips it — and cancel-in-progress kills the legitimate review run already executing. Observed live: the pipeline's own reply comments canceled the self-review of the commit they were replying about. Route non-trigger comment runs into a per-run noop group instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: rename fetchReviewComments to fetchBotReviewComments The method filters to the bot's own comments since the anchor-spoofing fix — the name now says so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: compare multi-line comment anchors by range start, not end GitHub's live `line` on a multi-line review comment is the END of the range, while anchors embed the finding's `line` — the START. A wide finding (span > LINE_PROXIMITY) compared by its end line blew past the window and re-posted as a duplicate. Prefer start_line / original_start_line when present. Found by the bot reviewing its own dedup code on this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: suffix viewer login with [bot] only for Bot identities A PAT resolves viewer to a User whose bare login authors everything the action posts — appending [bot] would break author matching for PAT users. Query __typename and suffix only Bot viewers (still guarding against the already-suffixed installation-token form). Found by the bot's re-review of its own identity code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: parse anchor keys with a declarative regex Replace the lastIndexOf/slice index surgery in parseAnchorKey with a named-groups pattern — the regex states the key shape (file may contain colons, category never does, line is a positive integer) instead of making the reader simulate string offsets. Positivity moves into the pattern, dropping the separate integer check. Behavior unchanged; the malformed-key test table passes as-is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: single-path review architecture — quiet timeline, one status comment Every run now walks the same path; the first-run/re-run branch is gone. Dedup sources (the bot's inline comments with live positions, plus its beyond-diff issue comments by anchor line) are naturally empty on a first run, and the status comment's existence only picks the wording. Findings post without narrative wrappers: anchorable ones batch into a single review whose body is an invisible HTML marker (one notification, a bare "reviewed" timeline event, no prose blocks stacking per run); beyond-diff findings post as individual issue comments so each new finding is a visible event to PR watchers. All narration lives in the one always-upserted status comment — the run receipt, posted or updated on every completed run, findings or not. Deleted: hasPriorBotReview, detectRerun, the findings review body, the zero-findings review, buildReviewBody/buildRerunSummary and the 422 body-only fallback (anchor-rejected findings re-route to issue comments; other post failures self-heal — unposted findings carry no anchor, so the next run re-reports them). submitReview survives for skip notices only. Status anchor renamed to umm-actually-status. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: status comment reports what actually posted, not what was selected When a findings post fails (non-422), the findings stay unposted and self-heal next run — but the status comment claimed they were posted. Counts now come from the actual post outcomes, with an explicit "N finding(s) could not be posted — they will re-report on the next run" note, and the findings_count output follows suit. Found by the bot's first review under the new architecture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: author-filter the status comment upsert target upsertSummaryComment matched any comment containing the status anchor — a pasted marker from another author would hijack the update target (and the edit would 403; bots can't modify other users' comments). Match only the bot's own comments, same rule as the dedup sources. Found by the bot reviewing its own upsert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: status-comment detection and count accuracy from CodeRabbit round - Match the status marker only at the comment start (startsWith over includes) in both the upsert target search and the first-run detection — model-generated finding text quoting the marker mid-body could hijack the upsert or misclassify the run as a re-run. - Coalesce existing anchors by the proximity rule before counting: a fail-open repost leaves two anchors for one finding, and the tracked count reports findings, not comment anchors. - Assert exact page sequences in pagination tests — the stub serves responses by call count, so length checks passed even if page 1 were requested repeatedly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: harden anchor round-trip against model-controlled text - Reject file values that could break out of the HTML-comment anchor ("-->", newlines) at the schema boundary, where the structured-output retry ladder tells the model what to fix. Enforced via refine so the constraint stays out of the JSON schema sent to OpenRouter. - Extract only a trailing anchor: the genuine anchor is always the last thing the bot appends, so an anchor-shaped string quoted in model text can no longer poison dedup and suppress findings at arbitrary spots. - Clarify the findings_count output description: successfully posted this run, not generated — failed posts re-report next run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: drop dead usedFallbackBody from SubmitReviewResult Residue of this PR's removal of the 422 body-only fallback — submitReview now only posts skip notices and the field was hardcoded false, never read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
POST /pulls/{n}/requested_reviewersstep inself_review.yml— GitHub silently ignores bot accounts in thereviewersfield (returns 200 butrequested_reviewers: [])requestReviewsmutation'sbotIdsfield — the only API that supports requesting bots as PR reviewers (community discussion)viewer { login }→ REST/users/{slug}[bot]→ node ID) and requests itself before posting the review — best-effort, never blocks the pipelineRoot cause
The REST API and GraphQL
userIdsboth reject bot accounts. GraphQLrequestReviewshas a separatebotIdsparameter that the REST API doesn't surface. Without it, the bot appears under "+1 more reviewer" with "Reviewers whose approvals may not affect merge requirements."Changes
src/github/event.ts— addnodeIdtoPrContext(parsed from event payload and REST response)src/github/client.ts— addgraphql+users.getByUsernametoOctokitLike, newrequestBotReviewmethodsrc/orchestrate.ts— callrequestBotReviewafter PR context resolution (step 3.5), wrapped in try/catch.github/workflows/self_review.yml— remove dead REST reviewer-request stepTest plan
client.test.ts— happy path (viewer → REST lookup → GraphQL mutation), malformed bot response, viewer query errororchestrate.test.ts— requestBotReview called with PR node ID, pipeline continues when requestBotReview throwsnpm test— 263 passing (21 new)npm run lint— cleannpm run build— clean🤖 Generated with Claude Code