When a Slack user kicks off a PR review with `@open-swe review <pr-url>`,
the reviewer agent now posts a one-line summary back to the Slack thread
when it finishes — either "No issues found" or "found N potential
issue(s)" with a link to the GitHub review.
The reviewer agent has no Slack tools by design, so the summary is sent
host-side from `publish_review` after the GitHub review POST succeeds.
The Slack channel/thread_ts is persisted on reviewer thread metadata at
trigger time and read back on publish. Re-reviews triggered by push
events stay silent in Slack to avoid noise on the original thread.
The reviewer agent was writing a 1–2 sentence "top-level take" as the
review body, which produced noisy paragraph-style summaries on PRs
("Reviewed the PR. The new ALLOWED_GITHUB_REPOS allowlist…"). Devin's
review comment is just a one-liner (`✅ No Issues Found` or
`**Devin Review** found N potential issue.`), and was preferred in the
internal A/B vs Graphite.
Drop the `summary` parameter from `publish_review`; render a fixed,
host-formatted body in `render_review_body` instead. Update the reviewer
prompt to forbid prose summaries.
* fix(reviewer): log every push/close early-return so 'silent ignore' is debuggable
Pushes to PRs that haven't had a first review fall through the watch
handler because the reviewer thread doesn't have kind=reviewer set.
Without log lines on the early-return paths, this scenario was
indistinguishable from 'webhook reached the handler at all' in the
hosted log stream.
Now every early-return logs at info or debug:
- info when a real PR exists but the reviewer thread isn't set up
(with a hint pointing at the trigger paths the user can use)
- info when the repo isn't in the reviewer allowlist
- debug for benign skips (non-branch refs, branch deletions,
already-reviewed head_sha)
* fix(reviewer): always post a summary review, even with no findings
The publish_review tool gated POSTing on `inline_comments or summary`,
so when the agent called publish_review() with no args on a clean PR
the result returned `success: true` but no GitHub review was posted —
the user got silence instead of a "no issues found" comment.
- Drop the gate so publish_review always POSTs.
- Friendlier no-findings render: `**No issues found.**` when the
findings list is empty, vs. `**No issues at or above \`<sev>\`
severity.**` with hidden count when only sub-threshold findings
exist. Agent summary renders below.
- Prompt now requires the agent to always pass a `summary` so the
body is meaningful; calls out specifically not to skip on a clean PR.
* feat: implement reviewer findings, publish_review, and watch mode
Build out the reviewer agent end-to-end against the design in
REVIEWER_DESIGN.md:
- Findings as first-class state on the reviewer thread metadata
(`agent/reviewer_findings.py`): Finding TypedDict with start_line/end_line
ranges, suggestion text for ```suggestion blocks, github_review_comment_id
for cross-run reconciliation, diff_hunk for UI rendering. Thread-level
metadata gets `kind=reviewer`, `pr`, `last_reviewed_sha`, `watch` so a
future frontend can list reviewer threads via the langgraph SDK.
- Diff utilities (`agent/reviewer_diff.py`): parse_unified_diff,
compute_diff_line_set for in-diff validation, extract_diff_hunk for
caching the hunk on a Finding, compute_diff_in_sandbox for SHA-to-SHA
diffs against the prepped repo.
- Tools: `add_finding` (validates against the diff line set so out-of-diff
ranges fail at creation, not at GitHub-publish), `update_finding`,
`list_findings`, `publish_review`. The reviewer agent's tool list is
swapped from `[]` (direct shell `gh api` calls) to these four.
- Publish path (`agent/reviewer_publish.py` + `agent/tools/publish_review.py`):
one POST /reviews call with body + inline comments + ```suggestion blocks,
per-comment IDs stored back on findings, GraphQL `resolveReviewThread`
fired for findings transitioning open->resolved on a re-review.
- Reviewer graph: deterministic clone-or-fetch + checkout in the factory
before the agent's first model call (warm- and cold-path symmetric);
computed diff and in-diff line set passed via runnable config; system
prompt rewritten for the single-evolving-findings model, severity ladder,
in-diff-only discipline, and watch-mode reconciliation flow.
- Watch mode in webapp.py: `push` event + `pull_request` closed/reopened
added to supported events. New `process_github_push_event` resolves the
open PR for the pushed branch, gates on the reviewer thread's `watch`
flag, builds a re-review configurable, and triggers a run on the same
canonical thread. `process_github_pr_close` toggles watch on
closed/reopened. `set_reviewer_thread_metadata` is called on first
review to install `kind=reviewer` + PR identity + watch=True.
- Eval harness: target.py now extracts `add_finding` calls (mapped to the
legacy {file, line, body, severity} shape the judge expects) and passes
the right configurable so the prep step has base/head SHAs.
- Tests: new unit suites for findings helpers, diff parsing, finding tools,
publish rendering + GraphQL resolve, and watch-mode webhook handlers
(push triggers re-review only when watching, idempotent on unchanged
head SHA, PR close disables watch). Updated existing reviewer-webhook
tests to mock `set_reviewer_thread_metadata`.
- REVIEWER_EVAL_PLAN.md removed per user request; folded relevant context
into REVIEWER_DESIGN.md.
* fix(reviewer): correct git diff flags, scope, dedup, and review-comments URL
Address PR #1253 review findings:
- compute_diff_in_sandbox dropped the invalid `--no-prefix=false` flag
(`option no-prefix takes no value` — every prep run was failing
silently and the agent saw an empty diff).
- compute_diff_in_sandbox grew a `merge_base` flag. First-review path
now uses three-dot `base...head` (the merge-base diff GitHub renders
on Files-changed) so we don't pick up changes that landed on the base
branch after the PR diverged. Re-review delta keeps two-dot
`last_reviewed_sha..head` since that's exactly the new commits.
- publish_review skips findings that already carry
`github_review_comment_id`. Without this, watched re-reviews
re-posted every previously surfaced finding, and only the most-recent
duplicate's id would later resolve when the issue got addressed.
- fetch_review_comments URL now includes `{pull_number}` —
`/repos/{owner}/{repo}/pulls/{pr_number}/reviews/{review_id}/comments`
is the canonical endpoint; the old form 404s, so comment ids were
never stored and watch-mode resolution couldn't run.
Three new tests cover: three-dot vs two-dot wiring, no `--no-prefix`
flag in the executed command, and that publish_review does not re-post
findings whose `github_review_comment_id` is set.
* fix(reviewer): default publish cap from 15 to 4
A clean PR with one critical issue padded out by three lower-severity
findings is fine; fifteen is review spam. The agent can override per
call when a PR genuinely warrants more.
* feat: only post Slack 'Working on it!' on first thread mention
* feat: randomize Slack trace reply phrase
Pick from a small list of friendly phrases instead of always saying
'Working on it!' so the bot feels less robotic. Explicit messages (e.g.
'Taking a look...' from PR review path) are unaffected.
* adjust phrases
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
The Slack mention path already routes mid-run messages through the
store-based queue + check_message_queue_before_model middleware (the
same path Linear uses), so multitask_strategy="enqueue" on runs.create
was a leftover no-op — that line is only reached when is_thread_active
returned False. The busy-path payload was also hardcoding image_urls=[]
which silently dropped any images attached to mid-run Slack mentions;
forward the resolved image_urls so the middleware can rebuild image
blocks like Linear does.
* Use gh for reviewer inline comments
* Trigger reviewer on PR review requests
* Format reviewer webhook test
* Add GitHub repo allowlist
* Separate reviewer repo allowlist
* only implement allowlist for reviewer
* feat: add reviewer graph + eval target wiring
- New `reviewer` graph (`agent/reviewer.py`) registered in langgraph.json
alongside the main `agent` graph. Reuses the same sandbox lifecycle,
GH proxy auth, and middleware primitives from `agent.server`, but with
a narrower tool set, a reviewer-specific system prompt, no
commit/push, and the `task` (subagent) tool stripped via
`_ToolExclusionMiddleware` so review stays in one context.
- New `github_comment` tool: agents call it once per issue with
`(file, line, body, severity)` and the eval scores those calls
against golden comments.
- `ensure_no_empty_msg` middleware (the no_op nudge) is intentionally
*not* on the reviewer's stack — that middleware exists to enforce the
main agent's "always finalize via Slack/Linear/PR" contract, which
the reviewer doesn't have. The main agent's behavior is unchanged.
- `evals/reviewer/target.py`: send PR info as a user message, extract
every `github_comment` tool call (multiple expected per review) into
the run output.
- `evals/reviewer/judge.py`: per-example evaluator now returns a list
of metrics under `{"results": [...]}` so LangSmith averages each
numeric key (f1/precision/recall/tp/fp/fn) across the experiment in
the UI. Dropped the broken `aggregate_pr` summary evaluator that
reached for an attribute that doesn't exist on `RunTree`.
- `evals/reviewer/run_eval.py`: `--limit` now slices the dataset via
`client.list_examples(limit=N)` since `aevaluate` doesn't accept
`max_examples`.
- Makefile: `dev` and `run` targets now use `uv run` so they work
without an activated venv.
* resolve comments
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Stopgap for Vercel preview deploys rejecting open-swe[bot] PRs: even with
git config --global applied at sandbox setup, commits are landing with
open-swe <open-swe@langchain.dev> — likely from local repo config or shell
session env that overrides our global config.
Adding git config inside the cloned repo (local config) beats global and
ensures every commit picks up the bot's noreply email regardless of what
else is set in the sandbox.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: re-apply git identity on every agent invocation, source from env
Cached/reconnected sandboxes weren't getting `git config --global` re-applied
(the call was gated on new-sandbox creation), so commits picked up whatever
identity the sandbox happened to have — sometimes an email not associated
with any GitHub account, which Vercel rejects on preview deploys.
Move the git config call out of the new-sandbox branch so it runs on every
get_agent, and source the values from OPEN_SWE_GIT_AUTHOR_NAME /
OPEN_SWE_GIT_AUTHOR_EMAIL env vars (defaulting to the existing bot identity)
so forks can override without code changes.
* cleanup
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: move github workflows to gh cli
Use LangSmith proxy auth to support gh-driven GitHub workflows while removing custom GitHub wrapper tools.
* docker ignore + snapshot and docker image updates
* updated image and instructions
* removing open_pr if needed after agent call
* feat: add edit_pull_request tool for editing PR titles and descriptions
* fix: patch auth flow in open PR middleware tests
* fix: support app token for editing PRs
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* feat: add release note section to PR body template
The langchainplus repo PR template includes a ## Release Note section,
but the agent hardcoded PR body template only had ## Description and
## Test Plan. This caused the release note section to be dropped from
agent-created PRs.
* docs: align PR body argument docs
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* fix: prevent futile retry loop when commit_and_open_pr fails with git/API errors
- Root cause: when git checkout or GitHub PR API fails, the tool returned a generic {"success": false} error with no signal that retrying is futile, causing the agent to loop 9-13+ times until hitting the 1000-step recursion limit
- Change: (1) git_checkout_branch now returns (bool, str) so the actual git error output is surfaced in the tool response; (2) checkout and PR creation failures now include "fatal": true and an explicit "Do not retry" message; (3) prompt.py COMMIT_PR_SECTION adds an explicit instruction to stop on fatal errors
- Verified: 109 unit tests pass, no regressions
* fix: skip PR safety net on fatal commit failures
* style(open_pr): ruff-format fatal retry skip condition
---------
Co-authored-by: LangSmith Forge <forge-agent@langsmith.ai>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
- Root cause: LLM occasionally generates strings like '1, 80' or '170, "limit": 60'
for integer fields, causing a Pydantic ValidationError and wasting an LLM turn
- Change: add SanitizeToolInputsMiddleware in agent/middleware/sanitize_tool_inputs.py
that extracts the leading integer from any string value in offset/limit before
the call reaches Pydantic validation; registered before ToolErrorMiddleware in server.py
- Verified: 14 unit tests covering all three production trace patterns pass
Co-authored-by: LangSmith Forge <forge-agent@langsmith.ai>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* fix: notify users via Slack when agent hits model call step limit
- Root cause: GraphRecursionError at 1000 steps bypassed all @after_agent
middleware including open_pr_if_needed, leaving users with no notification
- Change: Added ModelCallLimitMiddleware(run_limit=60) to intercept gracefully
before the hard recursion limit, and added notify_step_limit_reached
@after_agent middleware to post a Slack thread reply when the limit fires
- Verified: 107 existing tests pass, no regressions
* fix: harden step-limit Slack notification
Ensure the step-limit notification runs after the PR safety net and cover the new middleware behavior with focused unit tests.
---------
Co-authored-by: LangSmith Forge <forge-agent@langsmith.ai>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* fix: warn against using http_request for GitHub PR creation
- Root cause: http_request tool description and system prompt lacked explicit guidance not to use it for GitHub PR operations, causing the agent to fall back to it and receive 401 Unauthorized responses
- Change: added clear warnings to both the http_request docstring and the TOOL_USAGE_SECTION in prompt.py directing agents to use commit_and_open_pr instead
- Verified: docstring and prompt changes are minimal and scoped
* fix: clarify http_request PR guidance
---------
Co-authored-by: LangSmith Forge <forge-agent@langsmith.ai>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* fix: prevent scope creep — clarify lockfile handling and task scope guidance
- Root cause: CODING_STANDARDS_SECTION ambiguously said 'package manager files' (interpreted to include lockfiles like uv.lock, package-lock.json) and TASK_EXECUTION_SECTION scope guidance was too generic to prevent cross-language or cross-service modifications
- Change: clarify that only manifest files (pyproject.toml, package.json) need updating and auto-generated lockfiles must NOT be committed; add concrete examples to scope guidance (no JS when Python-only, no other services when one is targeted)
- Verified: 3 production traces showing users complaining about scope creep
* fix: clarify prompt lockfile guidance
---------
Co-authored-by: LangSmith Forge <forge-agent@langsmith.ai>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
- Root cause: create_github_pr found an existing PR on 422 but never
PATCHed it, so callers like commit_and_open_pr could not update the
PR body (e.g. adding "Closes AB-1159") on subsequent invocations.
- Change: after _find_existing_pr succeeds, call new _update_github_pr
helper which PATCHes /repos/{owner}/{repo}/pulls/{number} with the
requested title and body before returning pr_existing=True.
- Verified: self-evident API call addition; proof in production traces.
Co-authored-by: LangSmith Forge <forge-agent@langsmith.ai>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: return actionable error when git push fails due to workflows scope
- Root cause: push failure with "workflows scope" error returned a generic
"Git push failed: ..." message, causing the agent to retry 10+ times
- Change: detect "workflows" + "scope" in push output and return a clear
message instructing the agent to remove .github/workflows/ file changes
- Verified: unit tests cover both the workflow-scope path and the
non-workflow path to prevent regressions
* fix: detect github workflow permission push failures
* style: ruff-format checkout line in commit_and_open_pr
---------
Co-authored-by: LangSmith Forge <forge-agent@langsmith.ai>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
- Root cause: httpx.HTTPError handler returned (None, None, False) without
checking if the PR was already created on GitHub before the network error
- Change: added _find_existing_pr fallback in except block in agent/utils/github.py
- Verified: 5 production traces showed false failures where PR existed (pr_existing=True on retry)
Co-authored-by: LangSmith Forge <forge-agent@langsmith.ai>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* fix: stop agent retrying commit_and_open_pr on 403 permission denied
Detect 403/permission-denied push failures in commit_and_open_pr and
return a PERMANENT_FAILURE message so the LLM stops retrying. Also add
prompt-level guidance to the COMMIT_PR_SECTION reinforcing this. Add
unit tests covering both the 403 and non-403 push failure paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: stop safety net retrying permanent push failures
Skip the after-agent PR fallback when commit_and_open_pr reports a permanent GitHub push authorization failure, while preserving fallback behavior for recoverable failures.
---------
Co-authored-by: Claude Agent <agent@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* feat: add github CI check run tools for shepherding CI
Add get_pr_check_runs and rerun_failed_check_runs tools that authenticate
using the GitHub App installation token so the agent can query and retry
CI status on private repos without relying on GH_TOKEN or unauthenticated
http_request calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: handle paginated GitHub CI results
* refactor(github_ci): address review feedback
- Rename rerun_failed_check_runs -> rerun_failed_workflow_runs and clarify
in docstrings that the tool only retries GitHub Actions workflow runs
(not third-party CI checks surfaced by get_pr_check_runs).
- Skip action_required workflow runs when rerunning; those need manual
approval, not a rerun.
- Run rerun-failed-jobs requests concurrently via asyncio.gather instead
of sequentially.
- Fix latent pagination bug in _fetch_paginated_items where caller-supplied
params could overwrite per_page/page and break the end-of-pagination
check; reserved keys now always win and the threshold uses a PER_PAGE
constant.
- Set an explicit 30s httpx timeout so a hung GitHub call cannot stall
the agent loop.
- Restore alphabetical ordering of tools in agent/tools/__init__.py.
- Add tests for: a 500 surfaced on a later pagination page, and
action_required runs being filtered out of rerun candidates.
---------
Co-authored-by: Claude Agent <agent@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
When the agent is triggered by automated deployment monitoring (no Slack
thread, no Linear ticket, no GitHub issue), the prompt now explicitly
instructs it to skip the notification step rather than attempting
slack_thread_reply and failing.
Co-authored-by: Claude Agent <agent@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Calling get_github_token() without arguments always invoked LangGraph get_config internally,
which broke tests that only patch agent.middleware.open_pr.get_config and failed outside
runnable context.
Extend get_github_token with an optional runnable config mapping; the middleware passes
the config dict already resolved from get_config(). Request GitHub App installation tokens
only after detecting sandbox/repo changes worth publishing.
Fixes failing Agent unit tests in tests/test_open_pr_middleware.py.
* fix: safety net middleware always skipped due to key-existence check
The open_pr_if_needed after-agent middleware checked `if 'success' in pr_payload`
which evaluates True for BOTH success and failure responses from commit_and_open_pr
(all responses include the 'success' key). This meant the safety net never fired.
Fix: use `pr_payload.get('success')` to check the VALUE instead of key existence.
Evidence: 6+ production traces in last 24h where commit_and_open_pr returned
success=False but the safety net silently skipped (non-fast-forward push failures,
missing GitHub token, workflow permission errors, API 500 errors).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* update
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Palash Shah <palash@langchain.dev>
* feat: add get_pr_review_comments tool for authenticated GitHub API access
The agent was asking users to paste PR review comments because it had no
tool to fetch them with auth. This adds get_pr_review_comments, which uses
the GitHub App installation token to fetch all three comment types (thread
comments, inline review comments, review submissions) from private repos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* u
* u
---------
Co-authored-by: Forge Agent <agent@forge.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Palash Shah <palash@langchain.dev>
Co-authored-by: Palash Shah <35114859+Palashio@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Previous defaults (4 vCPU / 15 GiB) exceed the maximum sandbox size
the LangSmith API accepts, causing a 400 on every sandbox creation:
sandbox size 4 vCPU / 15360 MiB exceeds the maximum supported size;
must fit within one of: small (1 vCPU / 1792 MiB),
medium (1 vCPU / 3840 MiB), or large (2 vCPU / 7936 MiB)
Default to the "large" cap (2 vCPU / 7936 MiB) so deployments without
DEFAULT_SANDBOX_VCPUS / DEFAULT_SANDBOX_MEM_BYTES env overrides boot
into a working state.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: open PRs under user's name and add OpenSWE label
* feat: use user token for PR authorship, add OpenSWE label, and consolidate fallback logic
* linting
* fix: address review nits for PR authorship and labeling
Fix docstring casing, add debug logging for 422 existing-PR search
fallback, tighten test type annotations, and add missing HTTPError
fallback test.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: default to GPT-5.5 medium reasoning
Use OpenAI GPT-5.5 with medium reasoning as the default model and document the completion-token budget semantics for reasoning models.
* fix: use Responses API reasoning config
Pass GPT-5.5 reasoning settings through LangChain's Responses API parameter instead of the Chat Completions-only reasoning_effort field.
* feat: raise GPT-5.5 output budget
Set the default GPT-5.5 output token budget to the model maximum so long-running coding tasks have more room for reasoning and final responses.
* feat: align recursion limit with Deep Agents
Use Deep Agents' default recursion limit so longer coding runs have room to complete without Open SWE imposing a lower cap.
* chore: remove minimal effort level
* chore: reduce max tokens to 64_000
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: migrate LangSmith sandbox creation to snapshot API
Replaces the template-based sandbox flow (DEFAULT_SANDBOX_TEMPLATE_NAME /
DEFAULT_SANDBOX_TEMPLATE_IMAGE) with the new snapshot-based flow.
- New required env var DEFAULT_SANDBOX_SNAPSHOT_ID (UUID of a pre-built
LangSmith snapshot; build out-of-band via UI or SandboxClient.create_snapshot)
- Optional DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES overrides the root FS
size at boot (default 32 GiB)
- Startup-time validation via a FastAPI lifespan hook: the server refuses
to boot with a clear ValueError if SANDBOX_TYPE=langsmith and
DEFAULT_SANDBOX_SNAPSHOT_ID is unset, so failures surface in boot logs
rather than on the first thread
- Reconnect-to-existing-sandbox path unchanged
- Docs (INSTALLATION.md, CUSTOMIZATION.md) updated to describe the new
snapshot workflow
* fix: format create_sandbox_snapshot.py to pass ruff
---------
Co-authored-by: aran-yogesh <yogesh.mahendran@langchain.dev>