Skip to content

feat: V1 I/O clients — github, openrouter, workspace context#3

Merged
aliasunder merged 9 commits into
mainfrom
feat/v1-io-clients
Jul 11, 2026
Merged

feat: V1 I/O clients — github, openrouter, workspace context#3
aliasunder merged 9 commits into
mainfrom
feat/v1-io-clients

Conversation

@aliasunder

@aliasunder aliasunder commented Jul 11, 2026

Copy link
Copy Markdown
Owner

PR 1 of 2 for the V1 review pipeline. Ships the three I/O client modules, fully tested and intentionally unwired — orchestrate.ts composes them in PR 2, which keeps this PR independently reviewable and shrinks the dogfood blast radius.

What's here

  • src/github/client.ts — octokit wrappers behind a minimal structural OctokitLike (stubs are plain objects, data: unknown forces Zod parsing). fetchPullRequest (Zod-validated PrContext), fetchDiff (diff media type; 406 → too_large), submitReview with the 422 body-only fallback: fires only when the rejected review carried inline comments, posts the caller-supplied fallbackBody, never a third attempt.
  • src/openrouter/client.ts — structured-output request (json_schema, strict: true) with a bounded retry ladder: 1 retry per model on transient/garbage failures (408/429/5xx/network, empty content, invalid JSON, schema mismatch), no retry on structural 4xx, immediate abort on 401/402/403 (the fallback model shares the key), ≤2 models, max 4 billed calls. Every attempt is recorded with usage — failed attempts were billed too. Accepted attempts missing usage.cost get one best-effort generations.getGeneration lookup that can never fail the run.
  • src/openrouter/cost-summary.ts — pure markdown table renderer for the job summary (row per attempt, total, n/a for unpriced).
  • src/context/ — workspace reads against real fs (tests run on a committed fixture tree, no fs mocking). readConventions (null when missing), readChangedFiles (diff-order token budgeting, full → diff-only flip, binary/unreadable degrade with a warning), findRelatedFiles (bounded reverse-import scan: 5k-file / 256KB caps, pruned dep/build dirs, .js.ts remap, barrel resolution, import-count ranking with test files last, cap 8). All paths resolve under the workspace root with a traversal guard — diff paths are PR-author-influenced.
  • src/main.ts — registers both credentials with the runner masker (core.setSecret) immediately after config parse; the JSON logger writes raw to stdout and bypasses ::add-mask:: otherwise.
  • Fixture tree includes real node_modules/ + dist/ decoy dirs (gitignore-negated) so the scan pruning is tested against the genuine article.

Verification

  • 166 tests pass (85 new), npm run lint, npm run build, npm run prettier:check, docker build . all green.
  • Mutation spot checks: dropping the 406 mapping, breaking the .js.ts remap, and removing the 401 abort each made exactly their covering tests fail, then were reverted.
  • No runtime surface to drive yet — these clients are dead code until PR 2 wires orchestrate.ts; live behavior is covered by the planted-bug E2E after PR 2.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added GitHub pull request retrieval, unified diff fetching (with too-large handling), and review submission.
    • Added structured AI review generation with JSON/schema validation, retry/fallback across models, and token/cost tracking.
    • Added workspace context collection (conventions, changed files, and related source discovery).
    • Added a formatted AI review cost summary table.
  • Bug Fixes
    • Register action secrets with the GitHub Actions masker before reporting failures.
    • Hardened workspace path safety (including symlink escape prevention) and improved diff/review fallback behavior.

PR 1 of 2 for the V1 pipeline (spec: vault plans/v1-pipeline-spec).
github/client.ts (diff fetch via media type, createReview with 422
body-only fallback), openrouter/client.ts (structured-output retry
ladder, max 4 calls, cost accounting) + cost-summary.ts, context/
(conventions reader, budgeted changed-file reads, bounded
reverse-import related-files scan). Dead code until orchestrate.ts
lands in PR 2. main.ts registers both credentials with the runner
masker via core.setSecret.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@aliasunder, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ea8fb119-f11b-4be8-8265-f13d84615f79

📥 Commits

Reviewing files that changed from the base of the PR and between 4ef75f3 and d5cb743.

📒 Files selected for processing (11)
  • src/context/__tests__/import-resolution.test.ts
  • src/context/__tests__/workspace.test.ts
  • src/context/import-resolution.ts
  • src/context/workspace.ts
  • src/github/__tests__/client.test.ts
  • src/github/client.ts
  • src/openrouter/__tests__/client.test.ts
  • src/openrouter/__tests__/cost-summary.test.ts
  • src/openrouter/client.ts
  • src/openrouter/cost-summary.ts
  • src/review/prompt.ts
📝 Walkthrough

Walkthrough

Adds workspace context discovery, validated GitHub and OpenRouter clients, structured review retries and cost reporting, bootstrap secret masking, and fixtures and tests covering these behaviors.

Changes

Workspace context assembly

Layer / File(s) Summary
Import resolution contract and tests
src/context/import-resolution.ts, src/context/__tests__/import-resolution.test.ts
Extracts import specifiers and resolves relative JavaScript and TypeScript path candidates.
Budgeted workspace reader
src/context/workspace.ts, src/context/__tests__/workspace.test.ts
Reads conventions and changed files, enforces budgets and workspace confinement, scans bounded source files, and ranks related importers.
Workspace discovery fixtures
.gitignore, fixtures/workspace/...
Adds source, importer, registry, convention, and pruning fixtures for context-reader tests.

GitHub review client

Layer / File(s) Summary
GitHub client operations
src/github/client.ts, src/github/__tests__/client.test.ts, fixtures/pull.get.json
Adds validated pull-request and diff retrieval plus review submission with body-only fallback for rejected inline comments.

OpenRouter structured review

Layer / File(s) Summary
Structured review client
src/openrouter/client.ts, src/openrouter/__tests__/client.test.ts, fixtures/openrouter.*.json
Adds schema-constrained requests, response validation, retries, model fallback, failure aggregation, and generation-cost lookup.
Cost summary rendering
src/openrouter/cost-summary.ts, src/openrouter/__tests__/cost-summary.test.ts
Formats attempt token and cost data into Markdown with aggregate totals and unpriced indicators.

Bootstrap and project structure

Layer / File(s) Summary
Configuration secret masking
src/main.ts
Masks parsed GitHub and OpenRouter credentials during bootstrap.
Active module documentation
AGENTS.md
Updates the documented status and responsibilities of the GitHub, OpenRouter, and context modules.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: new V1 I/O clients for GitHub, OpenRouter, and workspace context.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v1-io-clients

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • Consider extracting the duplicated HTTP error status helpers (e.g., errorStatus in GitHub client and errorStatusCode in OpenRouter client) into a shared utility to keep error handling consistent across I/O clients.
  • The workspace scanning code mixes path.join with posix-style paths (posix.join, posix.extname); if Windows runners are a target, it may be worth standardizing on one path convention or clearly documenting the assumptions to avoid subtle separator issues.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider extracting the duplicated HTTP error status helpers (e.g., `errorStatus` in GitHub client and `errorStatusCode` in OpenRouter client) into a shared utility to keep error handling consistent across I/O clients.
- The workspace scanning code mixes `path.join` with `posix`-style paths (`posix.join`, `posix.extname`); if Windows runners are a target, it may be worth standardizing on one path convention or clearly documenting the assumptions to avoid subtle separator issues.

## Individual Comments

### Comment 1
<location path="src/openrouter/__tests__/client.test.ts" line_range="270-280" />
<code_context>
+    ])
+  })
+
+  it("aborts the ladder after exactly one call on a 401", async () => {
+    const stub = makeSdkStub({
+      sendResponses: [{ error: makeStatusError(401) }],
+    })
+    const { client } = makeClient(stub)
+
+    await expect(client.requestReview(requestParams)).rejects.toThrow(
+      "OpenRouter auth/credit error — aborting without fallback: openai/gpt-5-mini: api_error (HTTP 401: HTTP 401)",
+    )
+    expect(stub.sendCalls).toHaveLength(1)
+  })
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for all auth/credit abort status codes (401/402/403), not just 401

The client treats 401, 402, and 403 as abort statuses via `ABORT_STATUSES`, but this test only covers 401. To fully validate that the ladder aborts for all configured auth/credit failures, please parameterize this test (or add separate ones) for 401, 402, and 403 and assert that no fallback model call is made in each case. This helps catch regressions if the abort set or error handling changes and retries are accidentally reintroduced for 402/403.

```suggestion
  it.each([401, 402, 403])(
    "aborts the ladder after exactly one call on an auth/credit error status %d",
    async (status) => {
      const stub = makeSdkStub({
        sendResponses: [{ error: makeStatusError(status) }],
      })
      const { client } = makeClient(stub)

      await expect(client.requestReview(requestParams)).rejects.toThrow(
        `OpenRouter auth/credit error — aborting without fallback: openai/gpt-5-mini: api_error (HTTP ${status}: HTTP ${status})`,
      )
      expect(stub.sendCalls).toHaveLength(1)
    },
  )
```
</issue_to_address>

### Comment 2
<location path="src/openrouter/__tests__/client.test.ts" line_range="362-385" />
<code_context>
+    expect(result.attempts[0]?.costUsd).toBe(0.0399)
+  })
+
+  it("degrades to a null cost with a warning when the generation lookup fails", async () => {
+    const noCostResult = {
+      id: "gen-no-cost",
+      model: "openai/gpt-5-mini",
+      choices: [{ message: { content: JSON.stringify(acceptedReview) } }],
+      usage: { promptTokens: 12000, completionTokens: 800, cost: null },
+    }
+    const stub = makeSdkStub({
+      sendResponses: [{ value: noCostResult }],
+      generationResponses: [{ error: makeStatusError(500) }],
+    })
+    const { client, logger } = makeClient(stub)
+
+    const result = await client.requestReview(requestParams)
+
+    expect(result.attempts[0]?.costUsd).toBeNull()
+    expect(logger.messages).toContainEqual({
+      level: "warn",
+      message: "generation cost lookup failed",
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for the "unexpected generation response shape" warning path

You already test the successful lookup and the failure that logs `"generation cost lookup failed"`. There’s a third branch in `lookupGenerationCost` where the lookup succeeds but the response fails schema validation, logging `"unexpected generation response shape"`. Please add a test that stubs `generationResponses` with an invalid payload and asserts that `costUsd` stays null and the expected warning is logged, so all branches are covered.

```suggestion
  it("degrades to a null cost with a warning when the generation lookup fails", async () => {
    const noCostResult = {
      id: "gen-no-cost",
      model: "openai/gpt-5-mini",
      choices: [{ message: { content: JSON.stringify(acceptedReview) } }],
      usage: { promptTokens: 12000, completionTokens: 800, cost: null },
    }
    const stub = makeSdkStub({
      sendResponses: [{ value: noCostResult }],
      generationResponses: [{ error: makeStatusError(500) }],
    })
    const { client, logger } = makeClient(stub)

    const result = await client.requestReview(requestParams)

    expect(result.attempts[0]?.costUsd).toBeNull()
    expect(logger.messages).toContainEqual({
      level: "warn",
      message: "generation cost lookup failed",
      data: { error: "HTTP 500" },
    })
  })

  it("degrades to a null cost with a warning when the generation lookup returns an unexpected shape", async () => {
    const noCostResult = {
      id: "gen-no-cost",
      model: "openai/gpt-5-mini",
      choices: [{ message: { content: JSON.stringify(acceptedReview) } }],
      usage: { promptTokens: 12000, completionTokens: 800, cost: null },
    }
    const stub = makeSdkStub({
      sendResponses: [{ value: noCostResult }],
      // Return a successful response, but with an invalid payload shape
      generationResponses: [{ value: { data: { foo: "bar" } } }],
    })
    const { client, logger } = makeClient(stub)

    const result = await client.requestReview(requestParams)

    expect(result.attempts[0]?.costUsd).toBeNull()
    expect(
      logger.messages.some(
        (msg) => msg.level === "warn" && msg.message === "unexpected generation response shape",
      ),
    ).toBe(true)
  })

  it("skips the cost lookup entirely when the sdk has no generations surface", async () => {
```
</issue_to_address>

### Comment 3
<location path="src/openrouter/__tests__/cost-summary.test.ts" line_range="14-23" />
<code_context>
+describe("renderCostSummary", () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for the zero-attempts case to pin down summary behavior

Existing tests cover several non-empty attempt scenarios, but not `attempts: []`. Since this function may be called defensively, please add a test for an empty attempts array that verifies the header, table, and total line remain well-formed (e.g., `Total cost: n/a`) and that no errors are thrown or malformed markdown is produced.

Suggested implementation:

```typescript
describe("renderCostSummary", () => {
  it("renders zero attempts with well-formed summary", () => {
    const summary = renderCostSummary({
      attempts: [],
      modelUsed: "openai/gpt-5-mini",
    })

    expect(summary).toBe(
      [
        "### umm-actually cost summary",
        "",
        "| Model | Attempts | Prompt tokens | Completion tokens | Cost (USD) |",
        "| --- | --- | --- | --- | --- |",
        "| openai/gpt-5-mini | 0 | 0 | 0 | n/a |",
        "",
        "Total cost: n/a",
      ].join("\n"),
    )
  })

  it("renders a single accepted attempt with its total", () => {

```

If the actual markdown format produced by `renderCostSummary` differs (e.g., different header text, column order, or total line wording), adjust the expected array lines to match the existing tests’ formatting conventions while still asserting:
1. The header line is present.
2. The table header is present and well-formed.
3. The model row reflects zero attempts/tokens and an `n/a` cost (or equivalent representation).
4. The total line expresses that cost is not applicable (e.g., `Total cost: n/a`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/openrouter/__tests__/client.test.ts Outdated
Comment thread src/openrouter/__tests__/client.test.ts Outdated
Comment thread src/openrouter/__tests__/cost-summary.test.ts
@aliasunder

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/github/__tests__/client.test.ts (1)

101-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding a test for null body mapping.

prResponseSchema accepts body: z.string().nullable(), but no test exercises the null-body path. If the .nullable() modifier were removed, no test would fail. Adding a case with "body": null in the fixture or a dedicated it() would guard this regression.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github/__tests__/client.test.ts` around lines 101 - 110, Add coverage for
the nullable pull request body accepted by prResponseSchema by adding a
fetchPullRequest test fixture with body set to null, and assert the response
succeeds with the body preserved as null. Keep the existing missing-fields
rejection test unchanged.

Source: Coding guidelines

src/openrouter/client.ts (1)

332-371: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add backoff between retries, especially for 429 rate-limit errors.

The retry loop immediately re-calls attemptOnce after a retryable failure with no delay. For HTTP 429 (rate limit), an immediate retry is almost certain to receive another 429, wasting the retry budget. Consider adding exponential backoff with jitter before the next iteration when attemptResult.retryable is true and the loop will continue.

♻️ Suggested backoff insertion
         if (!attemptResult.retryable) break
+
+        if (attemptNumber < MAX_ATTEMPTS_PER_MODEL) {
+          const baseMs = 1000 * 2 ** (attemptNumber - 1)
+          const jitterMs = Math.random() * 500
+          await new Promise((resolve) => setTimeout(resolve, baseMs + jitterMs))
+        }
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openrouter/client.ts` around lines 332 - 371, In the retry loop around
attemptOnce, add a delay before continuing after a retryable failure, only when
another attempt will run. Use exponential backoff with jitter, increasing by
attemptNumber and preferably honoring any available rate-limit or Retry-After
information, while preserving immediate exit for non-retryable and abort
outcomes.
src/openrouter/__tests__/cost-summary.test.ts (1)

85-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer exact toBe assertions over toContain for deterministic output.

Tests 3 and 4 use toContain to check only the total cost line, but the full markdown output is deterministic. Using toBe with the complete expected string would catch unexpected formatting changes in these scenarios. As per coding guidelines, "Prefer exact assertions and assert whole deterministic values rather than substrings or loose matchers."

♻️ Suggested exact assertions for tests 3 and 4
   it("sums costs across attempts when every attempt is priced", () => {
     const fallbackAttempt: ModelAttempt = {
       model: "anthropic/claude-haiku-4.5",
       outcome: "accepted",
       promptTokens: 11000,
       completionTokens: 600,
       costUsd: 0.01,
       errorSummary: null,
     }

     const summary = renderCostSummary({
       attempts: [
         { ...acceptedAttempt, outcome: "schema_mismatch" },
         fallbackAttempt,
       ],
       modelUsed: "anthropic/claude-haiku-4.5",
     })

-    expect(summary).toContain("Total cost: $0.052100")
-    expect(summary).not.toContain("(some attempts unpriced)")
+    expect(summary).toBe(
+      [
+        "### umm-actually cost summary",
+        "",
+        "Model used: anthropic/claude-haiku-4.5",
+        "",
+        "| attempt | model | outcome | prompt tokens | completion tokens | cost |",
+        "| --- | --- | --- | --- | --- | --- |",
+        "| 1 | openai/gpt-5-mini | schema_mismatch | 12000 | 800 | $0.042100 |",
+        "| 2 | anthropic/claude-haiku-4.5 | accepted | 11000 | 600 | $0.010000 |",
+        "",
+        "Total cost: $0.052100",
+      ].join("\n"),
+    )
   })

   it("renders an n/a total when no attempt carries a cost", () => {
     const unpricedAttempt: ModelAttempt = {
       ...acceptedAttempt,
       costUsd: null,
     }

     const summary = renderCostSummary({
       attempts: [unpricedAttempt],
       modelUsed: "openai/gpt-5-mini",
     })

-    expect(summary).toContain("Total cost: n/a")
+    expect(summary).toBe(
+      [
+        "### umm-actually cost summary",
+        "",
+        "Model used: openai/gpt-5-mini",
+        "",
+        "| attempt | model | outcome | prompt tokens | completion tokens | cost |",
+        "| --- | --- | --- | --- | --- | --- |",
+        "| 1 | openai/gpt-5-mini | accepted | 12000 | 800 | n/a |",
+        "",
+        "Total cost: n/a",
+      ].join("\n"),
+    )
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openrouter/__tests__/cost-summary.test.ts` around lines 85 - 101, Update
the deterministic output assertions in the cost-summary tests around
renderCostSummary, especially the cases asserting “Total cost: $0.052100” and
“Total cost: n/a,” to use exact toBe comparisons against the complete expected
markdown output. Preserve the existing test inputs and verify the entire
rendered summary rather than only matching substrings.

Source: Coding guidelines

src/context/__tests__/workspace.test.ts (1)

69-75: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use exact error-message assertions instead of toThrow(string) (substring match).

.rejects.toThrow(string) checks that the message includes the substring, not that it equals it — a regression that changes the message format while keeping the substring would still pass. As per coding guidelines, tests should "Prefer exact assertions and assert whole deterministic values rather than substrings or loose matchers." Both escape-path tests (readConventions at Line 74, readChangedFiles at Line 162) rely on this loose match.

♻️ Proposed fix: assert the exact thrown error
     await expect(
       contextReader.readConventions({ conventionsFile: "../outside.md" }),
-    ).rejects.toThrow("path escapes the workspace: ../outside.md")
+    ).rejects.toThrow(new Error("path escapes the workspace: ../outside.md"))
     await expect(
       contextReader.readChangedFiles({
         changedPaths: ["../../etc/passwd"],
         budgetTokens: 10_000,
       }),
-    ).rejects.toThrow("path escapes the workspace: ../../etc/passwd")
+    ).rejects.toThrow(new Error("path escapes the workspace: ../../etc/passwd"))

Also applies to: 154-163

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/__tests__/workspace.test.ts` around lines 69 - 75, Update the
rejection assertions in the readConventions and readChangedFiles tests to verify
the complete deterministic error message exactly, replacing the substring-based
rejects.toThrow(string) matcher while preserving the existing expected messages.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@fixtures/workspace/src/barrel-consumer.ts`:
- Line 1: Update the relative import in the barrel-consumer fixture to
explicitly reference the compiled JavaScript module by adding the .js extension
to the "./lib" specifier, while preserving the existing LIB_NAME import.

In `@src/context/workspace.ts`:
- Around line 123-160: Update readChangedFiles to stat each resolved changed
path before calling readFileOrNull, mirroring the MAX_SCAN_BYTES guard used by
scanWorkspaceSourceFiles. For files exceeding the size limit, add the existing
diff-only PromptFile entry and continue without reading their contents; preserve
current unreadable, binary, and token-budget handling for files within the
limit.

---

Nitpick comments:
In `@src/context/__tests__/workspace.test.ts`:
- Around line 69-75: Update the rejection assertions in the readConventions and
readChangedFiles tests to verify the complete deterministic error message
exactly, replacing the substring-based rejects.toThrow(string) matcher while
preserving the existing expected messages.

In `@src/github/__tests__/client.test.ts`:
- Around line 101-110: Add coverage for the nullable pull request body accepted
by prResponseSchema by adding a fetchPullRequest test fixture with body set to
null, and assert the response succeeds with the body preserved as null. Keep the
existing missing-fields rejection test unchanged.

In `@src/openrouter/__tests__/cost-summary.test.ts`:
- Around line 85-101: Update the deterministic output assertions in the
cost-summary tests around renderCostSummary, especially the cases asserting
“Total cost: $0.052100” and “Total cost: n/a,” to use exact toBe comparisons
against the complete expected markdown output. Preserve the existing test inputs
and verify the entire rendered summary rather than only matching substrings.

In `@src/openrouter/client.ts`:
- Around line 332-371: In the retry loop around attemptOnce, add a delay before
continuing after a retryable failure, only when another attempt will run. Use
exponential backoff with jitter, increasing by attemptNumber and preferably
honoring any available rate-limit or Retry-After information, while preserving
immediate exit for non-retryable and abort outcomes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ab0ff55a-d5e4-4068-9c74-40a5bdde42ca

📥 Commits

Reviewing files that changed from the base of the PR and between a75661b and e95f06a.

⛔ Files ignored due to path filters (3)
  • fixtures/workspace/dist/stale.js is excluded by !**/dist/**
  • fixtures/workspace/node_modules/decoy/index.js is excluded by !**/node_modules/**
  • fixtures/workspace/src/data.bin is excluded by !**/*.bin
📒 Files selected for processing (35)
  • .gitignore
  • AGENTS.md
  • fixtures/openrouter.chat-result.json
  • fixtures/openrouter.invalid-json.json
  • fixtures/openrouter.schema-mismatch.json
  • fixtures/pull.get.json
  • fixtures/workspace/AGENTS.md
  • fixtures/workspace/src/__tests__/greeter.test.ts
  • fixtures/workspace/src/barrel-consumer.ts
  • fixtures/workspace/src/caller.ts
  • fixtures/workspace/src/consumer.ts
  • fixtures/workspace/src/greeter.ts
  • fixtures/workspace/src/hub.ts
  • fixtures/workspace/src/importers/importer-1.ts
  • fixtures/workspace/src/importers/importer-2.ts
  • fixtures/workspace/src/importers/importer-3.ts
  • fixtures/workspace/src/importers/importer-4.ts
  • fixtures/workspace/src/importers/importer-5.ts
  • fixtures/workspace/src/importers/importer-6.ts
  • fixtures/workspace/src/importers/importer-7.ts
  • fixtures/workspace/src/importers/importer-8.ts
  • fixtures/workspace/src/importers/importer-9.ts
  • fixtures/workspace/src/lib/index.ts
  • fixtures/workspace/src/registry.ts
  • src/context/__tests__/import-resolution.test.ts
  • src/context/__tests__/workspace.test.ts
  • src/context/import-resolution.ts
  • src/context/workspace.ts
  • src/github/__tests__/client.test.ts
  • src/github/client.ts
  • src/main.ts
  • src/openrouter/__tests__/client.test.ts
  • src/openrouter/__tests__/cost-summary.test.ts
  • src/openrouter/client.ts
  • src/openrouter/cost-summary.ts

Comment thread fixtures/workspace/src/barrel-consumer.ts
Comment thread src/context/workspace.ts
aliasunder and others added 5 commits July 11, 2026 11:18
The traversal guard in createContextReader was lexical only — readFile
follows symlinks, so a PR could commit a link targeting files outside
the workspace or inside .git/ (checkout token in .git/config under
persist-credentials) and have the content pulled into the LLM prompt,
which is echoed into a public review. Resolve real paths and re-check
containment before reading: conventions reads throw on escape,
changed-file reads degrade to diff-only with a warning. In-workspace
symlinks (AGENTS.md -> CLAUDE.md) stay readable. The related-files scan
was already safe (lstat-semantics dirent checks skip symlinks).

Guardrail tests mutation-checked: all three fail with the containment
check removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBJw5bhDigxu8HM2sPPZVf
- extractImportSpecifiers + isMissingFileError: multiline expression
  bodies wrapped in block bodies per AGENTS.md
- readFileOrNull -> readScannedFileOrNull inside the factory: the silent
  catch now warns with path + error so scan exclusions are auditable
- changed-file unreadable warn now carries the error detail
- summarizeAttempts: map callback with ternary-in-interpolation
  extracted into describeAttempt
- cost summary: nested ternary-in-template pulled into unpricedSuffix
- BFS queue mutation in scanWorkspaceSourceFiles gets its justifying
  comment, matching the file's sequential-state comment pattern

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBJw5bhDigxu8HM2sPPZVf
- github/client: toStrictEqual on the zero-comment review (toEqual cannot
  tell an absent `comments` key from `comments: undefined`); 422-fallback
  test now asserts the whole call array (first call shape + no third
  attempt); new test for rethrowing a status-less error
- cost-summary: replace toContain with full-output toBe on the two
  deterministic renders
- workspace: name directory pruning as a spec behavior (covers the
  previously untested hidden-directory branch); cover the unreadable
  scanned-file warn path via chmod 000
- openrouter/client: cover status-less network error retry, >200-char
  error summary truncation, unexpected generation response shape, and
  missing usage block; hoist the triplicated no-cost chat result into a
  factory

Four mutations verified: each failed exactly its covering test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBJw5bhDigxu8HM2sPPZVf
…related-file NUL guard, deleted-file log level

- import-resolution: match side-effect static imports (import "./x.js") —
  the doc claimed static imports were matched, but the regex required
  from/import(/require( and missed the side-effect form
- import-resolution: add index.tsx/index.jsx to directory-index candidates,
  matching the .tsx/.jsx suffixes extensionless resolution already accepts
- workspace: exclude related-file candidates carrying a NUL byte — the same
  binary rule readChangedFiles applies
- workspace: a missing changed file (deleted in the PR) logs at info, not
  warn — deletions are routine, warn implied breakage
- cost-summary: doc said one row per billed attempt; every attempt gets a
  row, including unbilled network failures

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBJw5bhDigxu8HM2sPPZVf
…, exact assertions

- readChangedFiles stats before reading so an oversized changed file
  (lockfile, bundle) is demoted to diff-only without an unbounded read
- abort-ladder test parameterized across 401/402/403
- zero-attempts cost summary pinned
- null PR body mapping pinned
- workspace escape tests assert exact error messages

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBJw5bhDigxu8HM2sPPZVf
@aliasunder

Copy link
Copy Markdown
Owner Author

ship-check — non-thread bot findings & deferred items

Dispositions for review-body findings that have no inline thread, plus items the ship-check pipeline deferred to the author.

Sourcery overall comments (review body)

  • Extract shared HTTP error-status helper (errorStatus / errorStatusCode): Intentional — the two helpers read different properties from different SDK error shapes (octokit duck-types status; OpenRouterError exposes statusCode). A shared utility would need the property name as a parameter, an abstraction that removes no real duplication. Kept separate per the repo's utils-admission bar.
  • path.join vs posix mixing in workspace scanning: Intentional — this is a Docker container action, which GitHub Actions only runs on Linux runners; Windows is not a target. posix.* is used for workspace-relative paths (matching git-diff path shape), native path for filesystem access. On Linux they agree.

CodeRabbit nitpicks (review body)

  • Null-body test for prResponseSchema: Fixed in d78be98 — test added pinning body: null preservation.
  • Backoff between retries (429): Intentional for V1 — the retry ladder (≤4 calls, one retry per model, fast fallback to the second model) is the settled design; on a 429 the ladder's response is to move models, not wait. Recorded for revisit if live E2E shows rate-limit churn.
  • toContain → exact toBe in cost-summary tests: Fixed in cbacb7f (test-audit pass) — both assertions are now full-render toBe.
  • Substring toThrow in workspace escape tests: Fixed in d78be98 — all three escape assertions now match the exact error.

Deferred by ship-check to the author

  1. workspace.ts walk error policy (needs design decision): an unexpected readdir/stat error in scanWorkspaceSourceFiles fails the whole review, while the module elsewhere degrades per-file. Deliberately unresolved — a wrong workspaceRoot should fail loudly, and the expected-vs-unexpected line belongs with orchestrate.ts's error policy (PR 2, alongside skip-surfacing-on-PR).
  2. Scan reads skip realPathIfSafe (needs design decision): changed-file and conventions reads re-check containment on real paths; reverse-import scan reads don't. Verified not exploitable today (Dirent.isFile() is false for symlinks, so none enter the scan; checkout is static during a run). Pure defense-in-depth question — recommendation is to add the guard (cost is negligible), pending author's call.
  3. No test for main.ts core.setSecret wiring (pre-existing gap): main.ts has never had a test file; the stub it surrounds is replaced by orchestrate.ts in PR 2. Suggest covering it there.

🔍 ship-check · pr-monitor · claude-fable-5

Comment thread src/github/client.ts
aliasunder and others added 3 commits July 11, 2026 15:50
expectTypeOf<ReturnType<typeof getOctokit>>().toExtend<OctokitLike>() —
an @actions/github upgrade that drifts the SDK shape now fails tsc and
the suite instead of surfacing at runtime in PR 2's wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBJw5bhDigxu8HM2sPPZVf
…eJsonOrNull

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBJw5bhDigxu8HM2sPPZVf
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBJw5bhDigxu8HM2sPPZVf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant