Skip to content

fix(github): fix response bugs and add missing endpoint coverage - #5471

Merged
waleedlatif1 merged 3 commits into
stagingfrom
validate/github-integration
Jul 7, 2026
Merged

fix(github): fix response bugs and add missing endpoint coverage#5471
waleedlatif1 merged 3 commits into
stagingfrom
validate/github-integration

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Fixed unauthenticated internal sub-fetch in pr.ts (list PR files) that 401'd on private repos and burned the anonymous rate limit
  • Fixed hardcoded/placeholder output fields in add_labels, delete_comment, delete_file
  • merge_pr now correctly handles 409 (sha mismatch) as a failure, not just 405
  • request_reviewers.reviewers loosened to optional to support team-only review requests
  • Added items schema to get_commit parents array output
  • Added 4 new tools (+ v2 variants), each verified against live GitHub REST docs: github_get_readme, github_create_pr_review, github_get_latest_release, github_list_tags
  • Ran a full /validate-integration pass plus an independent parallel re-verification (API alignment, internal wiring, backwards compatibility) — no backwards-incompatible changes, no orphaned/missing registry entries

Type of Change

  • Bug fix
  • New feature (new tools)

Testing

Tested manually. Verified every changed/added endpoint against live GitHub REST API docs across 3 independent verification passes. bun run lint and tsc --noEmit both clean.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

- Fix unauthenticated internal sub-fetch in pr.ts (list_pr_files) that
  401'd on private repos and burned anon rate limits
- Fix hardcoded/placeholder output fields in add_labels, delete_comment,
  delete_file
- Handle 409 (sha mismatch) in merge_pr in addition to 405
- Loosen request_reviewers.reviewers to optional (team-only reviews)
- Add items schema to get_commit parents array output
- Add github_get_readme, github_create_pr_review,
  github_get_latest_release, github_list_tags (+ v2 variants)
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 7, 2026 3:38pm

Request Review

@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes affect PR merge/review flows and authenticated GitHub calls; bugs fixed reduce false success and auth failures, but workflows that relied on incorrect success flags or required reviewers may behave differently.

Overview
Fixes several GitHub tool response and API-call bugs and expands the integration with four new operations wired through the block, registry, and v1/v2 tools.

Bug fixes: pr.ts now sends the user token on the follow-up PR files fetch and surfaces failures instead of silently using unauthenticated requests (private repos / rate limits). add_labels, delete_comment, and delete_file populate metadata from request params instead of placeholders; delete_comment ties success to HTTP 204. merge_pr treats 409 (stale head SHA) as failure alongside 405. request_reviewers makes reviewers optional and only includes non-empty reviewer/team arrays in the POST body. get_commit documents parent objects in output schema.

New capabilities: Tools (and GitHub block operations) for create PR review, get README (decoded content), get latest release, and list tags, each with v2 variants and types/registry exports.

Reviewed by Cursor Bugbot for commit f754759. Configure here.

Comment thread apps/sim/tools/github/pr.ts
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes several bugs in existing GitHub tools (unauthenticated sub-fetch, hardcoded output fields, missing 409 handling in merge_pr) and adds four new tools (github_get_readme, github_create_pr_review, github_get_latest_release, github_list_tags) each with v1 and v2 variants.

  • Bug fixes: pr.ts now passes auth headers for the internal files sub-fetch, add_labels.ts/delete_file.ts now populate output fields from params, merge_pr.ts handles 409 (SHA mismatch) as a failure, and request_reviewers.ts no longer requires reviewers when team_reviewers is supplied.
  • New tools: All four new tools include response.ok guards (addressing issues flagged in a prior review pass) and follow the v1/v2 output pattern consistently used across the codebase.
  • Registry & block config: All new tools are wired into registry.ts and the block UI in github.ts with correct switch-case and tool-access entries.

Confidence Score: 5/5

This PR is safe to merge — all bug fixes address real defects (unauthenticated sub-fetches, hardcoded outputs, missed 409 status), and the four new tools follow the same defensive patterns already established in the codebase.

Every changed code path has been hardened with proper auth forwarding, response.ok guards, and correct output mapping. The new tools are wired up consistently (registry, block config, exports, types) with no orphaned or missing entries. No regressions were identified against existing tool contracts.

No files require special attention — the most complex change (pr.ts dual-fetch with auth) is straightforward and well-guarded.

Important Files Changed

Filename Overview
apps/sim/tools/github/pr.ts Fixed unauthenticated files sub-fetch (adds auth headers) and adds response.ok guard + safe array fallback for the files response in both v1 and v2 tools.
apps/sim/tools/github/create_pr_review.ts New tool implementing the GitHub POST reviews endpoint; includes response.ok guard, correct body construction, and distinct v1/v2 output shapes.
apps/sim/tools/github/get_readme.ts New tool for fetching repository README; correctly base64-decodes content via Buffer, guards non-2xx responses, and exposes both simplified (v1) and raw (v2) output shapes.
apps/sim/tools/github/get_latest_release.ts New tool fetching the latest stable release; correctly guards response.ok and maps assets in v2 using the shared RELEASE_OUTPUT/RELEASE_ASSET_OUTPUT_PROPERTIES constants.
apps/sim/tools/github/list_tags.ts New tool listing repository tags; guards non-2xx responses, safely maps tag items, and provides structured v1/v2 outputs aligned with the GitHub Tags API schema.
apps/sim/tools/github/merge_pr.ts Added 409 (SHA mismatch) alongside 405 as an explicit failure path in both v1 and v2, with a descriptive fallback message for each status code.
apps/sim/tools/github/add_labels.ts Fixed hardcoded placeholder output fields: issue_number now reads from params, html_url is constructed from owner/repo/issue_number.
apps/sim/tools/github/delete_comment.ts Fixed success field to reflect actual delete status (response.status === 204) instead of always returning true, in both v1 and v2 tools.
apps/sim/tools/github/delete_file.ts Fixed path output field to use params.path instead of the commit tree SHA.
apps/sim/tools/github/request_reviewers.ts Loosened reviewers to optional and rewrote body builder to omit the field when no reviewers are provided, enabling team-only review requests.
apps/sim/tools/registry.ts All 8 new tool variants correctly imported and registered; no orphaned or missing entries.
apps/sim/blocks/blocks/github.ts Block config updated with UI parameters and switch-case entries for all four new operations; new global params (event, commit_id) added for PR review.
apps/sim/tools/github/types.ts Added type interfaces for all new params and responses; RequestReviewersParams.reviewers correctly marked optional; ReleaseResponse added to GitHubResponse union.
apps/sim/tools/github/get_commit.ts Added items schema to the parents array output; schema-only change, no behavioral impact.
apps/sim/tools/github/index.ts Correctly exports all new tool instances; import order follows the existing alphabetical pattern.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant prTool
    participant GH_PR as GitHub /pulls/{num}
    participant GH_Files as GitHub /pulls/{num}/files

    Client->>prTool: call with apiKey, owner, repo, pullNumber
    prTool->>GH_PR: GET (with Authorization header)
    GH_PR-->>prTool: PR JSON

    prTool->>GH_Files: GET (with Authorization header — fixed in this PR)
    alt filesResponse not OK
        GH_Files-->>prTool: 401/403/404 error
        prTool-->>Client: success: false, partial PR metadata
    else filesResponse OK
        GH_Files-->>prTool: files JSON array
        prTool-->>Client: success: true, full PR + files output
    end

    participant createPRReview
    participant GH_Review as GitHub /pulls/{num}/reviews
    Client->>createPRReview: call with event, body?, commit_id?
    createPRReview->>GH_Review: "POST {event, body?, commit_id?}"
    alt response not OK
        GH_Review-->>createPRReview: 422 (missing body for REQUEST_CHANGES)
        createPRReview-->>Client: success: false, error.message
    else OK
        GH_Review-->>createPRReview: review JSON
        createPRReview-->>Client: "success: true, {id, state, html_url, commit_id}"
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant prTool
    participant GH_PR as GitHub /pulls/{num}
    participant GH_Files as GitHub /pulls/{num}/files

    Client->>prTool: call with apiKey, owner, repo, pullNumber
    prTool->>GH_PR: GET (with Authorization header)
    GH_PR-->>prTool: PR JSON

    prTool->>GH_Files: GET (with Authorization header — fixed in this PR)
    alt filesResponse not OK
        GH_Files-->>prTool: 401/403/404 error
        prTool-->>Client: success: false, partial PR metadata
    else filesResponse OK
        GH_Files-->>prTool: files JSON array
        prTool-->>Client: success: true, full PR + files output
    end

    participant createPRReview
    participant GH_Review as GitHub /pulls/{num}/reviews
    Client->>createPRReview: call with event, body?, commit_id?
    createPRReview->>GH_Review: "POST {event, body?, commit_id?}"
    alt response not OK
        GH_Review-->>createPRReview: 422 (missing body for REQUEST_CHANGES)
        createPRReview-->>Client: success: false, error.message
    else OK
        GH_Review-->>createPRReview: review JSON
        createPRReview-->>Client: "success: true, {id, state, html_url, commit_id}"
    end
Loading

Reviews (3): Last reviewed commit: "fix(github): guard new tools against non..." | Re-trigger Greptile

Comment thread apps/sim/tools/github/delete_comment.ts
Comment thread apps/sim/tools/github/pr.ts
- pr.ts: surface a real failure instead of silently returning
  success:true with an empty files array when the files sub-fetch
  fails; unify the sub-fetch Accept header with the rest of the file
- delete_comment: success now tracks the actual deletion outcome
  instead of being hardcoded true
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/tools/github/list_tags.ts

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b321202. Configure here.

list_tags, get_readme, get_latest_release, and create_pr_review
(v1 + v2) now check response.ok before parsing the payload as
success data, returning success:false with a real error message
instead of crashing on .map() or silently returning undefined
fields when GitHub returns a 404/422/etc.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f754759. Configure here.

@waleedlatif1
waleedlatif1 merged commit a1e6744 into staging Jul 7, 2026
18 checks passed
@waleedlatif1
waleedlatif1 deleted the validate/github-integration branch July 7, 2026 15:56
icecrasher321 added a commit that referenced this pull request Jul 29, 2026
The previous note contrasted these five with "the user-facing tools added
alongside them", implying this branch added both. It did not: the branch
never touches blocks/blocks/github.ts, and github_create_pr_review came from
#5471, which predates staging. The real contrast is with every user-facing
GitHub tool in the registry.

Also records the governance consequence, which was the part actually worth
writing down: the permission-group deny list is built from tools.access, so
an admin cannot deny these from the UI, and the allowedIntegrations gate
keys on block type while Babysit calls them with a tool id alone.

Co-Authored-By: Claude <noreply@anthropic.com>
BillLeoutsakosvl346 added a commit that referenced this pull request Jul 29, 2026
…b tools, sandbox lifetime (#5962)

* feat(pi): optional multi-provider web search for the coding agent

Adds a search provider dropdown (Exa, Serper, Parallel, Firecrawl) to the Pi
block, off by default. The selected provider's key comes from the block field or
Workspace Settings → BYOK; a Sim-hosted key is never spent, so a missing key
fails the run with a setup message instead of quietly billing Sim.

Search is available in all three modes. Local Dev and Review Code register a
host-side tool that goes through the existing provider tools, while Create PR has
no host in the loop and gets a generated Pi extension in the sandbox. Both paths
derive their requests from one normalizer and are held together by a parity test,
since the sandbox copy cannot import Sim's code.

Results are normalized to title, URL, snippet, and publication date, capped per
field and per envelope, marked untrusted in the prompt, and limited to 20
searches per run so a tool loop cannot drain the workspace's quota.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): drop the banned JSON round-trip from the search parity test

`check:utils` bans `JSON.parse(JSON.stringify(...))`. The round-trip was
normalizing the host body to its wire form, which buys nothing here: the bodies
are plain JSON and `toEqual` already ignores undefined members.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(agents): add reviewed-development skill and the babysit implementation plan

Carries the plan and review protocol into the repository so a cloud agent
working from the remote can read them. Temporary: the plan is removed before
this branch goes for review.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(pi): babysit foundations — shared PR/push extraction, GitHub tools, sandbox lifetime

Stage 1 of the Babysit plan (.agents/plans/pi-babysit-mode.plan.md), sections 0,
3, and 7 plus their tests. The Babysit mode itself lands in stage 2.

Section 0 — shared extraction and push hardening:
- Move PREPARE_SCRIPT, PUSH_SCRIPT, and the finalize path/size constants from
  cloud-backend.ts into cloud-shared.ts.
- Move Review Code's PR snapshot helpers into pi/github-pr.ts, generalized off
  PiCloudReviewRunParams to a plain PullRequestCoordinates, and split the raw
  fetch from the "must be open" wrapper so a mode that has to report a closed PR
  gracefully can build on the raw form.
- Harden the one token-bearing command: GIT_CONFIG_NOSYSTEM/GIT_CONFIG_GLOBAL on
  its env, git by absolute path, and an explicit HEAD:refs/heads/$BRANCH refspec.
  The clone script now emits a .git/config digest marker as its last line for
  every mode; only Babysit will verify it.

Section 3 — five GitHub tools, registered but not wired into the block dropdown:
github_list_review_threads, github_reply_review_thread,
github_resolve_review_thread, github_status_check_rollup (GraphQL) and
github_job_logs (REST). Plus a nullable repo_full_name on the PR reader's branch
parse, declared on its own output rather than the shared BRANCH_REF_OUTPUT.

Section 7 — CreateSandboxOptions.lifetimeMs threaded to E2B's timeoutMs, clamped
below the one-hour Hobby ceiling and lowerable by PI_SANDBOX_LIFETIME_MS. Daytona
is deliberately untouched. The per-command Pi timeout is capped at the lifetime.

* fix(tools): correct the rollup CheckRun selection and stop leaking the token on redirect

Two defects found in review, both verified against the live GitHub API.

GraphQL's CheckRun has no `output` object — that is REST's shape. It exposes
`title`, `summary`, and `text` as flat nullable fields, confirmed by introspecting
the schema. The old selection made every github_status_check_rollup call fail with
"Field 'output' doesn't exist on type 'CheckRun'", delivered as an HTTP 200 errors
payload that no fixture-based test could catch. The corrected query was run against
a real PR and returns 18 check runs plus a Vercel status context, with title and
summary null on every Actions run exactly as the plan predicted.

github_job_logs redirects to third-party blob storage, and Sim's tool fetch follows
redirects itself rather than through the fetch spec, so it replayed the GitHub token
to that host. Tools can now declare `stripAuthOnRedirect`, and the log reader does.

Also from review: pin Review Code's "must be open" guard with a test now that it
lives in its own function, drop the stream reader in github_job_logs since the
executor already hands transformResponse a capped buffer, and correct three doc
comments that overstated what they guaranteed.

* test(tools): cover the stripAuthOnRedirect plumbing end to end

Asserting the flag on the tool config alone would not catch a regression in
formatRequestParams or in the executor's call into secureFetchWithPinnedIP, so
pin what the fetch layer actually receives, in both the opted-in and the default
case.

* fix(pi): correct the push-hardening claim, reserve finalize time, tighten isRequired

Three review findings, each verified rather than taken on faith.

PUSH_SCRIPT's comment claimed GIT_CONFIG_NOSYSTEM and GIT_CONFIG_GLOBAL close
config-driven URL rewriting. They do not: reproduced locally on git 2.43, a
repository-local url.*.insteadOf still rewrites the push URL and sends the token's
userinfo to another host. That is the scope a root agent in the checkout can
actually write, and it stays open until a mode verifies the config digest — which
is Babysit, per the plan. The comment now says that instead of the opposite.

PI_TIMEOUT_MS capped the Pi command at the whole sandbox lifetime, so the sandbox
always died first and the stated benefit — a clean timeout instead of an opaque
SDK error — could never happen. It now reserves the clone and finalize budgets it
shares the sandbox with, leaving the host time to commit and push whatever the
agent produced.

isRequired is Boolean! on both CheckRun and StatusContext (confirmed by schema
introspection), so the nullable parse modelled a value GitHub cannot send and left
stage 2 a tri-state to handle. It is required now, and an absent value fails loudly
rather than reading as "not required", which would let a failing required check
stop blocking the green verdict.

Also: a github-pr.test.ts pinning that the raw fetchPrSnapshot does not throw on a
closed PR (the entire reason for the wrapper split, previously untested), a
cloud-shared.test.ts for the timeout reserve and the digest line, the E2B lifetime
ceiling documented next to E2B_PI_TEMPLATE_ID as section 7 asks, and the new
registry tests no longer leaving their fake tools registered.

* fix(pi): reserve both finalize budgets in the Pi command timeout

Create PR dispatches two commands at FINALIZE_TIMEOUT_MS, not one — the commit
and the push — so reserving a single budget left the push unbudgeted and the
worst case overshot the sandbox lifetime by exactly that amount. Losing the
sandbox during the push is the most expensive moment to lose it: the work is
committed and unpushed, which is the outcome the reserve exists to prevent.

The comment no longer claims more than the arithmetic delivers. What is reserved
is each command's timeout ceiling rather than its measured elapsed time, so this
is a budget that adds up, not a guarantee. Two other comments described Babysit
verifying the config digest in the present tense, when no mode verifies it yet.

Also drops the digest-line test: it asserted a string constant contains its own
substrings, while cloud-backend.test.ts already pins the property that matters —
the marker being the clone's last line, after the remote rewrite.

* docs(pi): stop describing Babysit's digest check in the present tense

Three comments still read as statements of current behavior: the Create PR push
test's note beside the assertion that proves no verification happens, github-pr's
module doc naming a second consumer that does not exist yet, and the timeout
floor claiming a short-lifetime run was doomed regardless when the reserved
ceilings are pessimistic enough that it may well finish.

* fix(pi): scope the sandbox lifetime cap to E2B and harden the job-log path

Deriving PI_TIMEOUT_MS from the E2B lifetime applied it to every provider, so a
Daytona Create PR run lost its ~90-minute agent turn to a ceiling Daytona does
not have — it stops on inactivity instead. The reserve now only applies when the
provider imposes an absolute lifetime.

A configured PI_SANDBOX_LIFETIME_MS below the clone and finalize reserves left no
positive remainder for the turn, so E2B could reap the sandbox before the push.
Such a value is raised to a floor rather than rejected: a module-scope throw on a
config typo would take down every path that imports this, not just Pi.

github_job_logs returns its response body verbatim, so unlike its siblings that
parse a typed shape, a coordinate carrying URL syntax turned a bearer-authenticated
request into a general read. Path segments are now escaped and the job id checked.

Also corrects the plan's rollup field path: GraphQL's CheckRun has no output
object, and isRequired is Boolean!, so stage 2 needs neither the nested path nor
an unknown-required branch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add Pi Babysit mode

* fix(pi): wait on required checks before optional failures

* fix(pi): preserve Daytona babysit budget

* fix(pi): bound babysit round setup

* fix(pi): keep babysit sandboxes active

* fix(pi): report babysit round state accurately

* fix(pi): classify babysit finalize failures

* fix(pi): preserve babysit partial state

* fix(pi): retain post-push check state

* fix(pi): retain pending rereview state

* fix(pi): normalize empty sandbox provider

* Move Babysit into Create PR

* Fix Babysit wait-only budgeting

* Wait for reviews before skipped-thread exit

* Polish Babysit reviewer field spacing

* Fix duplicated Internet Search section from staging merge

The staging merge landed the branch's Internet Search section and
staging's reviewed replacement side by side, leaving two `### Internet
Search` headings and two `#internet-search` anchors. The branch's copy
also still claimed a Settings > BYOK fallback for the search key, which
staging deliberately removed.

Keep staging's section and fold the branch's only new fact — that the
Babysit continuation sandbox carries both keys — into its warning callout.

* fix(pi): correct Babysit check, budget, and push-guard accuracy

Correctness:

- The `.github/` push refusal compared raw `git diff --name-only` output,
  which git C-quotes for non-ASCII paths. `.github/workflows/évil.yml`
  arrived as `".github/workflows/\303\251vil.yml"` — leading quote included
  — so neither `.github` nor `.github/` matched and the file pushed. Both
  name-listing diffs now pin `core.quotePath=false`; Create PR's does too so
  `changedFiles` reports real names.
- `CANCELLED` and `STALE` were counted as non-failing conclusions, so a
  cancelled required check produced `checksGreen: true` and `stopReason:
  'clean'` on a PR branch protection still blocks. Both now fall through to
  failing, matching every other unknown conclusion.
- The per-round check bound counted optional checks, so a repo with a wide
  optional matrix ended the run at `bounds_exceeded` before a single review
  thread was addressed. Only required-check overflow is fatal now; the rest
  trims, required checks first, and reports what was left out. This also
  makes the prompt's existing slice reachable.
- A re-review request that posted nothing still re-armed `requestedAt` with
  `landed: false`, leaving the loop waiting on a review nobody had asked
  for — burning the remaining lifetime on a billed idle sandbox before
  reporting `awaiting_review` on a clean PR. The previous request now stands.
- Prompt bounds threw where every other bound in the file trims, ending a
  busy PR's run on round one after the PR and its review comments were
  already posted. They now drop trailing entries and note the omission.
- `babysitMode` used a strict boolean compare; a `switch` input arriving as
  the string 'true' silently opened a draft PR and skipped Babysit while the
  editor showed it enabled. Matches `wait-handler`'s coercion now.
- The fork check compared head against the block's typed owner/repo, so a
  renamed repository — which GitHub serves through a 301 while reporting the
  canonical name — was reported as a fork. Compares head against base now.
- Each round's diff is capped like Create PR's. The cumulative guard measures
  the net change, so a round that reverts an earlier addition passed it while
  contributing a full-size diff.
- Reviewer mentions must start with `@`. Each entry becomes its own issue
  comment re-posted every round, so a comma inside one left prose on the PR.

Efficiency:

- Thread and check reads per poll now run together; neither consumes the
  other's result and both are paginating loops.
- `babysitReviewLandedSince` takes the `latestReview` the caller already
  fetched instead of re-listing the PR for it.
- Actions log reads fan out in small batches rather than one at a time.

Cleanup:

- `BabysitFinalizeError` was a twin of `BabysitGitHubError`; folded together.
- Replaced the hand-inlined copies of `threadsAreClean`.
- Dropped `MEMORY_MODES`, a duplicate of `AUTHORING_MODES`, and the
  `parsePiMode` tombstone for a mode that never shipped.

Adds regression tests for the quoted `.github` path, cancelled/stale required
checks, and the renamed-repository snapshot.

* fix(pi): budget Babysit against the run's real deadline

Three fixes that each needed to land a level below where the symptom showed.

Execution deadline. Babysit planned its wait loop against
`getMaxExecutionTimeout()`, which unconditionally returns the enterprise
async ceiling (90 min) with no regard for the run's plan or sync/async mode
— the sync ceiling is 300s on free, 3000s on pro. A synchronously triggered
run was therefore killed mid-loop with the PR already opened, its review
comments already posted, and none of the rounds/stopReason outputs produced.

Rather than thread a numeric deadline through ExecutionContext — five entry
points that would each have to remember it, and nothing to catch the one that
forgot — `createTimeoutAbortController` now records the deadline against the
signal it creates, and `getRemainingExecutionMs(signal)` reads it back. The
number cannot disagree with the timer that enforces it, because both are
established in the one place the timeout exists, and every entry point
already hands the executor that signal. `undefined` means unknown, not
unlimited: Babysit keeps the old ceiling as its fallback for an untimed run.

Job logs. The executor caps a response at 10 MB and throws rather than
truncating, so a verbose CI job produced no diagnostic at all — and GitHub
Actions reports null title/summary on every check run, leaving the agent a
bare URL it has no tool to follow. The tool now sends `Range: bytes=-N` so
the storage host returns only the tail. Since a suffix range is a request and
not a guarantee, a 200 still takes the local slice.

That made the old output dishonest, so the contract changed while the tool is
still new and has one consumer: `totalCharacters` (which a ranged read cannot
know) is replaced by `totalBytes`, sourced from the `Content-Range` total and
null when unreported. A ranged body is trimmed at its first line break, since
the byte window cuts mid-line and can split a multi-byte character.

Generated docs. `github.mdx` was missing the `repo_full_name` the PR reader
now returns, and regenerating deleted the head/base rows instead of adding it:
`scripts/generate-docs.ts` only expands a spread at depth 0 of a const, and
`PR_BRANCH_REF_OUTPUT` spread inline under `properties`. Restructured into a
named properties const in types.ts, next to the shapes it belongs with, which
the generator resolves the same way it already resolves BRANCH_REF_OUTPUT.

Only the github.mdx hunk is committed. The generator is lossy elsewhere —
it drops 126 lines of trigger configuration from jira.mdx — which is
pre-existing drift for whoever owns that surface, not this branch.

* fix(pi): refuse any Git-quoted path before the Babysit push

`core.quotePath=false` stopped Git escaping non-ASCII bytes, but it closed one
instance rather than the class. Git still quotes any path it cannot state on a
single line — one containing a newline, a double quote, a backslash, or a tab —
and such a path arrives with a leading `"`, so the `.github/` prefix test does
not match it and the refusal is bypassed exactly as before.

Refuse any quoted path outright. Unescaping instead would put a second
implementation of Git's quoting rules on the security-relevant side of the
push, and these characters have no legitimate place in a source path.

* fix(pi): harden the Babysit push against a repository the agent controls

Every host-side guard before the push was computed by a git invocation the
agent could influence from inside `.git/`, which is never committed and so is
covered by neither the config digest nor any content check.

- Diff measurement ignored `.git/info/attributes`. One line saying `* -diff`
  reduced a 500 KB change to `Binary files differ`: verified locally as 119
  bytes reported against a real 500,147, so the 200 KB cumulative bound never
  fired, and the `diff` block output — the only record of what an unattended,
  review-comment-steered agent pushed — recorded the same nothing. Both diffs
  now run `--text --no-ext-diff --no-textconv` with an empty
  `core.attributesFile`.
- `rev-list --count` and `merge-base --is-ancestor` answered about replaced
  history. A `refs/replace/*` mapping makes a five-commit chain report one,
  while the pushed SHA stays real. `GIT_NO_REPLACE_OBJECTS` is now set for
  both the prepare and push commands.
- The push asserted HEAD's *shape* — right branch, one commit past the pin, a
  descendant — every part of which survives `commit --amend` to a different
  tree. So the bounds, the quoted-path refusal, the `.github/` refusal and the
  reported diff could all describe a commit other than the one pushed. The
  validated SHA is now passed in and the refspec names it.
- The digest comparison resolved `cat`, `sha256sum` and `cut` through `$PATH`
  in the same script that invokes git absolutely for exactly that reason, and
  was handed the expected digest in its own environment — so a shim could
  print it. The utilities are now absolute.
- The clone left the token in `.git/config` when the pinned-SHA assertion
  failed, because `set -e` aborted before `remote set-url`. Reordered.
- `changedFiles` reached the block output unscrubbed on the Babysit path,
  where Create PR already scrubs it. Names are agent-chosen.

These reduce exposure; they do not bound it. E2B runs each command as
`bash -l`, so a login-shell profile the agent writes executes ahead of the
next host command with the token in its environment. Removing the token from
the sandbox entirely means pushing host-side through the Git Data API, which
the ≤50-file / ≤200 KB bound already makes practical.

* fix(pi): correct switch coercion for draft and tidy Babysit reporting

- `draft` had the same string-coercion bug that `babysitMode` was fixed for one
  line above it. A switch arrives as `'true'`/`'false'` when its value came
  through a variable reference, an API trigger payload, or a legacy serialized
  workflow, and `inputs.draft !== false` read `'false'` as truthy — opening a
  draft PR against the user's explicit setting. Both now go through one
  `isSwitchEnabled` helper that handles either polarity and takes the field's
  default, because the bug is opposite on each.
- `mergePhaseDiffs` joined two separately-capped diffs without re-capping, so
  the combined output could reach twice MAX_DIFF_BYTES.
- The cancellation poller's `logger.warn` was the one message in these files
  emitted unscrubbed. A Redis poll error is unlikely to carry a run credential,
  but a uniform invariant is easier to keep than a per-call-site argument.
- Renamed `waitWithSandboxKeepalive` to `waitWithSandboxProbe`. E2B's `timeoutMs`
  counts down from create and is reset only by `Sandbox.setTimeout`, never by
  running a command, so `true` every four minutes proves liveness and buys no
  time. The old name invited raising the round wait on the assumption that waits
  extend the sandbox, which would let E2B reap it mid-wait.
- Dropped a `{@link}` to a symbol in another module that was never imported.

* docs(tools): record why the Babysit GitHub tools are registry-only

The same branch added four user-facing GitHub tools through the full
block-exposure recipe (v2 variant, tools.access, dropdown, subBlocks) and
five internal ones through none of it. The distinction is deliberate — the
five are called by the Pi Babysit handler via executeTool, which resolves
against the registry rather than any block's access list — but nothing in CI
encodes it, and `check-block-registry.ts` silently skips ids it cannot find.

Worth stating because the trap is non-obvious: `GitHubV2Block` builds its
access list by appending `_v2` to every entry, so adding one of these to
`tools.access` without first adding a v2 variant would point the block at an
id that does not exist.

* docs(pi): document the clean stop reason and Babysit's fixed bounds

The FAQ told readers to inspect `stopReason`, but the reference list never
named `clean` — the one value that means the PR actually reached the goal
state — and omitted `closed_or_merged`, `fork_pr`, and `check_read_failed`.

Also records the bounds that were previously undiscoverable, split by how
each one actually behaves: the reviewer-mention limits reject the block
before the run starts, the 30-thread limit trims a round, and only the
failing-check and cumulative-change limits produce `bounds_exceeded`.

Corrects step 6, which claimed Babysit reruns CI. It never does — the push
is what re-triggers checks.

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

* docs(tools): correct and widen the registry-only note

The previous note contrasted these five with "the user-facing tools added
alongside them", implying this branch added both. It did not: the branch
never touches blocks/blocks/github.ts, and github_create_pr_review came from
#5471, which predates staging. The real contrast is with every user-facing
GitHub tool in the registry.

Also records the governance consequence, which was the part actually worth
writing down: the permission-group deny list is built from tools.access, so
an admin cannot deny these from the UI, and the allowedIntegrations gate
keys on block type while Babysit calls them with a tool id alone.

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

* fix(pi): size the sandbox to the run's own execution timeout

The Pi sandbox lifetime was a global constant while the execution timeout is
per-plan and per-mode, varying 18x (a free sync run gets 5 minutes, an async
run 90). Every Pi sandbox asked E2B for the same sub-hour ceiling, so a
five-minute run whose web process died left a sandbox billing for an hour.
PI_SANDBOX_LIFETIME_MS could not close the gap: its floor is 31 minutes.

resolvePiRunLifetimeMs lowers the provider ceiling to whatever the run's own
deadline leaves, read from the signal that enforces it. Untimed runs and
Daytona are unchanged, so no path gets a longer lifetime than before.

The turn cap had to move with it. PI_TIMEOUT_MS reserved the clone and both
finalize budgets out of the ceiling as a module constant; leaving it there
while shrinking the lifetime would re-open the exact bug its docs describe —
the sandbox dying first, taking the agent's finished work with it unpushed.
It is now resolvePiTimeoutMs(lifetimeMs), and each backend resolves the
lifetime once and feeds both, so the two cannot disagree.

Two things this surfaced:

Babysit had to read context.signal, not the cancellation signal it uses
everywhere else. createCancellationSignal returns a fresh controller that
only forwards aborts, so the deadline lookup answers "unknown" through it and
would have silently left the longest-lived mode on the ceiling. Covered by a
test that fails against the wrong signal.

The E2B adapter tested lifetimeMs for truthiness, so a run resolving to zero
would have had the key dropped and been handed the SDK's five-minute default
- longer than it asked for, on the run least entitled to it.

Options precede the callback in withPiSandbox so that adding one did not
re-indent every caller's sandbox body.

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

* fix(pi): raise the sandbox ceiling to the longest execution we allow

The ceiling was pinned just under E2B's one-hour *Hobby* session limit. Sim is
on Professional, where the limit is 24 hours, so the cap was enforcing a
restriction no plan imposes — and it sat below the 90-minute async execution
ceiling, which made the sandbox the binding constraint. A long Babysit run
could be handed a 90-minute budget and still lose its sandbox at 59.

Derived from getMaxExecutionTimeout rather than given a number of its own, so
the sandbox always outlasts the longest run the platform permits and an
operator who raises the async timeout does not have to know this file exists.
The provider session limit stays as a clamp, so the derivation can never ask
E2B for a lifetime it will refuse.

Effect: ceiling 59 -> 90 min, and the agent turn it funds 29 -> 60 min, since
resolvePiTimeoutMs reserves the clone and both finalize budgets out of it.
Runs with a shorter deadline are unaffected — resolvePiRunLifetimeMs already
lowers the ceiling to whatever the run itself has left.

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

* docs(pi): describe the deadline-sized sandbox, not a fixed hour

Both facts in this paragraph were stale: the lifetime is no longer a single
sub-hour constant (it tracks the run's own remaining execution time), and the
ceiling was justified by E2B's Hobby limit on an account that is on
Professional.

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

* fix(pi): share the sandbox sizing and lift E2B off the base default

The two Pi images had drifted on exactly the axis their shared module exists to
prevent. Daytona asked for 4 CPU / 8 GB; the E2B template asked for nothing and
inherited its base default of 2 vCPU / 512 MB. That is a 16x memory gap between
the provider Pi normally runs on and the one it fails over to, so a failover
could be OOM-killed doing work that had just succeeded.

512 MB is too small independently of the drift: the Pi CLI is a Node process
holding an LLM context, running beside a clone of the user's repository, and
Node is OOM-killed rather than degraded at that ceiling — which reaches the
user as an opaque agent failure.

CPU and memory now come from pi-sandbox-packages.ts alongside the package
lists. Disk stays in the Daytona renderer: its 10 GB per-sandbox cap is a hard
provider limit with no E2B equivalent, so it is the one dimension where the
images legitimately differ.

E2B fixes resources at template build time, so this takes effect only when
build-pi-e2b-template.ts is re-run — nothing builds these images in CI.

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

* chore(pi): remove internal planning files

* chore(pi): remove generated review commands

* fix(pi): align babysit toggle visibility

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Bill Leoutsakos <billleoutsakos@Mac.localdomain>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Claude <noreply@anthropic.com>
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