refactor: adopt upstream domain reorg (8356eb34) — fork content, upstream layout (#204)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Format check (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Unit tests (push) Has been cancelled
CI / Playwright E2E (push) Has been cancelled
CI / Docker build smoke (push) Has been cancelled
CI / Triage ledger up to date (push) Has been cancelled
CI / ui bun.lock in sync (push) Has been cancelled

* refactor: move docs/resources/assets to domain layout

Part of the domain-reorg adoption (build plan step C1): fork content,
upstream layout. Moves INSTALLATION.md/CUSTOMIZATION.md under docs/,
static/ under assets/, and default_prompt.md under agent/resources/
(packaged via agent/resources/__init__.py), then switches prompt.py's
loader to importlib.resources with an explicit DEFAULT_PROMPT_PATH
override, matching upstream's hunk. README and CUSTOMIZATION.md links
updated for the new paths; wheel build verified to still ship
agent/resources/default_prompt.md.

* refactor: consolidate reviewer modules into agent/review/

Part of the domain-reorg adoption (build plan step C2): fork content,
upstream layout. Nine 1:1 module moves (reviewer_diff/eval_store/
findings/groups/publish/reconcile/trace_context + review_style_
collector/guidance) into agent/review/, with internal relative
imports re-wired to the new package depth. agent/review/__init__.py
mirrors upstream's thin re-export shim (one of the 21 verified "A"
structural adds).

Rewrote the 38 grep hits across importer files (agent/{analyzer,
ci_autofix,reviewer,webapp}.py, agent/dashboard/*, agent/middleware/
settle_review_check.py, agent/tools/*, agent/utils/github_feedback.py,
agent/webhooks/github.py, evals/reviewer/*, and the reviewer test
suite) to point at agent.review.*; 4 of the 38 hits were name
collisions (list_reviewer_findings, reviewer_outcomes,
_reviewer_thread_id, reviewer_thread_id — not the moved modules) and
were left untouched. tests/test_github_checks.py's module-alias
import (`from agent import reviewer_publish`) follows upstream's own
`from agent.review import publish as reviewer_publish` pattern so
downstream `reviewer_publish.*` call sites needed no changes.
agent/reviewer.py and agent/webapp.py stay in place per the hard
rule (fork content, import-only rewire) and are not part of this
package.

Gates: ruff check + ruff format --check, pytest --co -q (1637
collected), full unit suite (1637 passed), and the reviewer/findings
suite in isolation (pytest -k "review or finding", 421 passed).

* refactor: adopt graphs/runtime/providers shims + retarget langgraph.json

Part of the domain-reorg adoption (build plan step C3): fork content,
upstream layout. Adds agent/graphs/{agent,analyzer,chat,reviewer,
scheduler}.py as thin re-export shims delegating to the existing fork
graph factories (agent.server/analyzer/chat/reviewer/scheduler), plus
agent/providers/__init__.py re-exporting agent.utils.model's
make_model/provider_model_kwargs/fallback_model_id_for surface —
verbatim upstream content, verified each import resolves against fork
modules with no name changes needed.

agent/runtime/{constants,execution}.py deviate from upstream's
verbatim shim bodies: rather than duplicating DEFAULT_LLM_MODEL_ID/
DEFAULT_LLM_MAX_TOKENS/DEFAULT_RECURSION_LIMIT/MODEL_CALL_RECURSION_LIMIT
and graph_loaded_for_execution's logic (upstream's shims assume
agent/server.py already had these extracted into runtime/ modules,
which is out of this commit's scope — server.py is untouched), they
import the fork's existing agent.server attributes directly. This
keeps the values/logic single-sourced instead of forking a second
copy that could drift.

agent/runtime/sandbox.py's delegation targets also differ from
upstream: fork's sandbox lifecycle helpers are private
(_get_cached_sandbox_backend, _configure_git_identity,
_recreate_sandbox in agent/server.py) since the fork's sync
4-case `__creating__` sentinel design (AGENTS.md) never made them
public. get_cached_sandbox_backend() also drops upstream's
caller-supplied `reconnect` callback parameter — fork's
_get_cached_sandbox_backend is a plain cache lookup; reconnection is
handled internally by ensure_sandbox_for_thread/
check_or_recreate_sandbox, not via a passed-in callback. No other
signature changes.

Added fork-only agent/graphs/ci_monitor.py (delegates to
agent.ci_monitor:get_ci_monitor) for symmetry, since upstream deleted
its ci-autofix cluster and has no equivalent shim. langgraph.json's
five stock graph entrypoints plus the fork-only ci_monitor now all
point at agent.graphs.<name>; http.app stays agent.webapp:app
(unchanged, per plan).

Deliberately NOT included (owned by build plan step C4, the FastAPI
split, gated on /sh-security-review): agent/api/{__init__,app,
health}.py, agent/webhooks/common.py, and the three
agent/webhooks/{github,linear,slack}_routes.py files. Those aren't
thin structural shims like the 21-file list implies in isolation —
they carry the fork's actual webhook dispatch/verify logic split out
of the still-monolithic webapp.py, which hasn't happened yet.
Building them now against upstream's placeholder content would ship
incomplete auth surface that C4 would just discard and redo.

Pinned oven-sh/setup-bun's bun-version to 1.3.14 (the version
installed locally; ui/ has no .bun-version file or package.json
engines/packageManager field pinning one) across all three CI jobs
that install bun, removing the latest-resolution flake.

Gates: ruff check + ruff format --check (clean), pytest --co -q
(1637 collected, no import errors), a direct import smoke-test of
every new module's public symbols, and a make dev boot check —
langgraph dev registered all six graphs (agent, reviewer, analyzer,
chat, scheduler, ci_monitor) each importing from agent.graphs.*, and
loaded the custom app from agent.webapp:app, before the process was
killed. (The subsequent lifespan failure, "DEFAULT_SANDBOX_SNAPSHOT_ID
must be set when SANDBOX_TYPE=langsmith", is expected with no .env
secrets configured in this environment and unrelated to this commit.)

* refactor: split webapp.py into api/ + per-source webhook routes

Plan step C4 (docs/upstream-sync/domain-reorg/reorg-build-plan.md, approved
decisions 1-2): split the 2,590-line agent/webapp.py monolith into
agent/webhooks/common.py (shared verify/dispatch helpers), agent/api/app.py
(composition), agent/api/health.py (/health + /webhooks/run-complete), and
per-source {github,linear,slack,jira,confluence}_routes.py. Atlassian
Connect lifecycle + descriptor routes (/connect/*) fold into
confluence_routes.py; webapp.py becomes the upstream-shaped compatibility
shim (from .api.app import app). langgraph.json http.app stays
agent.webapp:app via the shim.

Fork content, upstream layout: linear/slack route files verified
content-identical to upstream 8356eb34 and taken verbatim; github_routes is
upstream + the fork's CI auto-fix trigger wiring; jira/confluence routes are
fork-only, transformed to the same common.X / service.X module-attribute
style. All signature verification (GitHub HMAC, Slack, Linear
timestamp-freshness, verify_jira_secret + opt-in HMAC/timestamp/IP
allowlist, Connect JWT/qsh), token-attribution gating, TID-COLLIDE-01 repo
binding, _is_repo_auto_review_enabled gates, and public-repo org gate move
unchanged.

Handlers rewired from webapp.X to common.X; test monkeypatch sites across
26 files + conftest.py + e2e/harness.py retargeted to
webhook_common/handler/route modules per upstream's pattern. Residual
agent.webapp importers: only the shim, langgraph.json http.app, Makefile
uvicorn target, and docs (doc-path updates land in C7).

Gates: ruff check + format, pytest --co, full unit (1637 passed), full
Playwright E2E vs real langgraph dev (9/9), residual-importer sweep.

* refactor: move tests into tests/<domain>/ layout

Applies the plan's C5 step: git mv every test per the domain-reorg
move-map (movemap-m50.txt) into tests/{agent,analyzer,auth,dashboard,
github,middleware,models,reviewer,sandbox,slack,tools,webhooks}/, plus
the 13 fork-only placements from the scoping report §2c (Atlassian
webhook tests -> tests/webhooks/, test_atlassian_connect.py and
test_auth_error_leak.py -> tests/auth/, jira/confluence util tests ->
tests/tools/, test_repo_binding_isolation.py -> tests/sandbox/,
bot-identity/autofix tests -> tests/github/).

Path-only move: the only content edits are parents[1] -> parents[2]
fixes in test_e2b_integration.py and test_daytona_integration.py,
required because their __file__-relative ROOT path gained one more
directory level in the move.

Monkeypatch retargets for these files were already completed in C4;
none remained outstanding here.

* refactor: move ui/src/{components,lib}/agents into features/ layout

Part of the domain-reorg adoption (build plan step C6): fork content,
upstream layout. git mv's all 80 pure-rename files from the move-map
(agent-scoped components/lib -> ui/src/features/{agents,automations,
reviews,settings}/...), including ported/ splitting into
features/agents/experiments/ (still-unintegrated desktop-host ports)
and features/agents/components/chat/ (CloudPromptBar, CodeBlock,
DiffView, Logo, Markdown, ReplyCard, ShellCommand, ToolExecution --
files actually wired into the live dashboard). Fork-diverged files
(PlanReview.tsx, WorkflowApprovalCard.tsx, AgentsSidebar.tsx,
SidebarFilterMenu.tsx, AgentGitPanel.tsx, AutomationEditor.tsx,
DiffView.tsx, CloudPromptBar.tsx) keep fork content -- verified via
diff that only import lines changed.

Rewrote @/components/agents and @/lib/agents imports across all 54
importer files plus the moved files' own internal imports (grep-driven,
85-entry alias map covering every old->new path pair). Two hazards
caught only by the build gate (not tsc, since both files sit under the
new experiments/** tsconfig exclude): ui/src/lib/notifications.ts had
a relative `./agents/types` import (no `@/` alias) that the grep missed;
and features/agents/experiments/index.ts's barrel re-export of the
messages module needed to switch from an `@/` alias to a relative
import (`../components/messages`) after landing inside the newly
tsconfig-excluded experiments/ directory -- vite-tsconfig-paths failed
to resolve it at build time even though tsc stayed silent.

AgentPromptBar.tsx (the 2-line CloudPromptBar re-export shim) moves to
features/agents/components/ with its export path retargeted at
CloudPromptBar's new chat/ location -- a D/A pair, not a content loss,
since the import-path edit drops it below git's rename-similarity
threshold. ported/index.ts is the same D/A story for the same reason.

Swapped the 8-file ported/ exclude lists in tsconfig.json and
eslint.config.js for the single `src/features/agents/experiments/**`
glob (upstream's simplification, no behavior change). Deleted
ui/pnpm-workspace.yaml (lockfile hygiene -- bun stays the toolchain).

Gates: bunx tsc --noEmit (clean), bun run build (clean after the two
notifications.ts / experiments-index.ts fixes above), bun run test
(33/33), and the full Playwright E2E suite against the real langgraph
dev server + built dashboard (9/9).

* docs: land reorg in ledger, fix path refs, ship plan artifacts (C7)

C7 of the domain-reorg adoption (build plan docs/upstream-sync/domain-reorg/
reorg-build-plan.md, step C7):

- CLAUDE.md / AGENTS.md: retarget architecture path references to the new
  layout — graph entrypoints via agent.graphs.* shims, the agent/api/ +
  agent/webhooks/*_routes.py FastAPI split (webapp.py now a shim),
  agent/review/ package, tests/<domain>/ test paths, and the new-graph/
  test conventions. README.md already pointed at docs/ (C1) — no change.
- triage.jsonl: flip 8356eb34 (#1726) to landed on branch
  refactor/domain-reorg-adoption; add re-triage notes to the 8 unblocked
  rows (#1732/#1761/#1744/#1748/#1742 clean, #1736/#1758/#1760 near-clean).
  triage.md regenerated via make triage-render.
- Ship the plan, scoping report, move-map artifacts, and the
  domain-reorg-adoption workflow so the exercise is reproducible.

Memory + Confluence handled out-of-band (not in this commit): project
memory updated with the new module layout; Confluence check found no IT
page documents the repo module map — no Confluence change required.
This commit is contained in:
Adam Moussa 2026-07-17 15:26:49 -04:00 • committed by GitHub
commit 298443ac8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
325 changed files with 7738 additions and 4026 deletions

View file

@ -0,0 +1,229 @@
export const meta = {
name: 'domain-reorg-adoption',
description: 'Execute the gate-approved C1-C7 domain-reorg build plan: moves, webapp split, security review, PR — no merge',
whenToUse: 'Run once, after Adam gives the go for the reorg build. Requires clean dev; plan + scoping artifacts live in docs/upstream-sync/domain-reorg/.',
phases: [
{ title: 'Preflight', detail: 'clean tree, artifacts present, branch created', model: 'haiku' },
{ title: 'C1 docs/resources', detail: 'doc moves + prompt loader + wheel check', model: 'sonnet' },
{ title: 'C2 review package', detail: '9 module moves + 38 importer rewrites', model: 'sonnet' },
{ title: 'C3 shims+manifest', detail: '21 shims, ci_monitor, langgraph.json, bun pin', model: 'sonnet' },
{ title: 'C4 webapp split', detail: 'the critical auth-surface split', model: 'fable' },
{ title: 'C4 security review', detail: 'detector fan-out + proof-or-kill verify' },
{ title: 'C5 test moves', detail: 'tests/<domain>/ mechanical moves', model: 'sonnet' },
{ title: 'C6 UI moves', detail: 'ui/src/features/ moves + import rewrites', model: 'sonnet' },
{ title: 'C7 docs/ledger/memory', detail: 'docs, triage ledger, memory, Confluence check', model: 'opus' },
{ title: 'PR', detail: 'push branch, open PR, final report — NO merge' },
],
}
// ---------------------------------------------------------------------------
// Shared context strings
// ---------------------------------------------------------------------------
const REPO = '/Users/adammoussa/Documents/repositories/seahaven/open-swe'
const SCRATCH = '/Users/adammoussa/Documents/repositories/seahaven/open-swe/docs/upstream-sync/domain-reorg'
const BRANCH = 'refactor/domain-reorg-adoption'
const COMMON = `
You are executing one commit of a gate-approved build plan in ${REPO} (branch ${BRANCH}).
Plan: ${SCRATCH}/reorg-build-plan.md — READ IT FIRST, plus the section of ${SCRATCH}/reorg-scoping-report.md relevant to your commit.
Move-map artifacts: ${SCRATCH}/movemap-m50.txt, ${SCRATCH}/cross.json, ${SCRATCH}/forkonly.json.
HARD RULES (violations = abort and report, do not improvise):
- Fork file contents, upstream layout. Upstream content ONLY for the 21 structural "A" shims listed in the scoping report.
- NEVER adopt upstream content for agent/webhooks/{github,linear,slack}.py, tests/conftest.py, tests/e2e/harness.py.
- Use "git mv" for moves so rename tracking survives. One commit for your cascade class only; commit message follows the repo's conventional style (see recent git log), body references the plan step.
- No pushes from your stage. No force operations. No edits outside your commit's scope. Do not touch docs/upstream-sync/* unless your stage says so.
- Gate ladder before committing: ruff check && ruff format --check, then uv run pytest --co -q, then the stage-specific gates. If a gate fails, fix within scope; if you cannot, ABORT: git reset --hard to the pre-stage SHA you recorded at start, and report the failure.
Return JSON: {ok, commit_sha, gates: {name: pass|fail|skipped}, notes, aborted_reason}`
const RESULT_SCHEMA = {
type: 'object',
properties: {
ok: { type: 'boolean' },
commit_sha: { type: 'string' },
gates: { type: 'object' },
notes: { type: 'string' },
aborted_reason: { type: 'string' },
},
required: ['ok', 'notes'],
}
const results = { commits: [], securityReview: null, pr: null }
function ensure(stage, r) {
if (!r || !r.ok) {
throw new Error(`${stage} failed: ${r ? r.aborted_reason || r.notes : 'agent returned null'}`)
}
results.commits.push({ stage, sha: r.commit_sha, gates: r.gates, notes: r.notes })
return r
}
// ---------------------------------------------------------------------------
// Preflight — cheap checks, haiku
// ---------------------------------------------------------------------------
phase('Preflight')
const pre = await agent(
`${COMMON}
STAGE: Preflight (no commit).
1. Verify ${REPO} is on dev and dev == origin/dev (fetch first is allowed here). Tree must be clean EXCEPT untracked files under docs/upstream-sync/domain-reorg/ and .claude/workflows/, which are expected (they are committed in C7).
2. Verify the plan file, scoping report, and all three move-map artifacts exist and are non-empty.
3. Verify branch ${BRANCH} does not already exist locally or on origin.
4. Create ${BRANCH} off dev. Record the base SHA.
Return JSON with ok, notes, and base_sha in notes.`,
{ label: 'preflight', phase: 'Preflight', model: 'haiku', effort: 'low', schema: RESULT_SCHEMA },
)
if (!pre || !pre.ok) throw new Error(`Preflight failed: ${pre ? pre.notes : 'null'}`)
log('Preflight OK — branch created')
// ---------------------------------------------------------------------------
// C1–C3: independent-of-C4 stages, sequential commits on the shared branch
// ---------------------------------------------------------------------------
phase('C1 docs/resources')
ensure('C1', await agent(
`${COMMON}
STAGE: C1 — docs/resources/assets (plan section "C1").
Moves: INSTALLATION.md and CUSTOMIZATION.md -> docs/, static/ -> assets/ (fix README refs), default_prompt.md -> agent/resources/ (+ __init__.py). Apply the importlib.resources loader hunk to the fork's prompt.py exactly as described in scoping §2b (upstream's hunk pattern, fork's file).
Extra gate: build the wheel (uv build or python -m build) and verify agent/resources/default_prompt.md is inside it; if missing, add the explicit hatchling include and note it.`,
{ label: 'C1', phase: 'C1 docs/resources', model: 'sonnet', schema: RESULT_SCHEMA },
))
phase('C2 review package')
ensure('C2', await agent(
`${COMMON}
STAGE: C2 — agent/review/ package (plan section "C2", scoping §2a/2b reviewer rows).
git mv the 9 reviewer/style modules per the move-map (reviewer_findings.py -> agent/review/findings.py etc.), replicate upstream's import-rewire pattern on FORK content for the R<100 ones, rewrite all 38 importer files (grep for reviewer_ and review_style_ imports to find them — do not trust the count blindly), create agent/review/__init__.py exporting the fork's existing public surface.
Extra gate: full reviewer/findings unit suites pass (uv run pytest tests/ -k "review or finding" plus any suite the moved modules own).`,
{ label: 'C2', phase: 'C2 review package', model: 'sonnet', schema: RESULT_SCHEMA },
))
phase('C3 shims+manifest')
ensure('C3', await agent(
`${COMMON}
STAGE: C3 — graphs/runtime/providers shims + langgraph.json + CI hardening (plan section "C3").
Adopt the 21 upstream "A" shim files verbatim-then-verify: for each, confirm every import it makes resolves against FORK modules (they delegate to agent.server etc.); fix delegation targets if fork names differ. Add fork-only agent/graphs/ci_monitor.py shim following the same pattern. Retarget langgraph.json graph entrypoints to agent.graphs.* (http.app stays agent.webapp:app). Pin bun-version in .github/workflows/ci.yml to the version in ui/ (check .bun-version / package.json engines; else current stable — note which).
Extra gate: timeout-bounded "make dev" boot check — all six graphs (agent, reviewer, analyzer, chat, scheduler, ci_monitor) register and the FastAPI app mounts; kill it after verifying startup logs.`,
{ label: 'C3', phase: 'C3 shims+manifest', model: 'sonnet', schema: RESULT_SCHEMA },
))
// ---------------------------------------------------------------------------
// C4 — the critical commit. Strongest model, high effort.
// ---------------------------------------------------------------------------
phase('C4 webapp split')
ensure('C4', await agent(
`${COMMON}
STAGE: C4 — FastAPI split (plan section "C4" + approved decisions 1-2). THE critical auth-surface commit.
Split the fork's 2,590-line agent/webapp.py into: agent/webhooks/common.py (shared verify/dispatch helpers), agent/api/app.py (composition), agent/api/health.py (/health + /webhooks/run-complete), and per-source github_routes.py / linear_routes.py / slack_routes.py / jira_routes.py / confluence_routes.py. Atlassian Connect lifecycle + descriptor routes (/connect/*) go INTO confluence_routes.py (approved decision 2). webapp.py becomes the compatibility shim re-exporting app (mirror upstream's shim shape).
Preserve EXACTLY: all signature-verification logic (GitHub HMAC, Slack, Linear, verify_jira_secret + optional HMAC/timestamp, Connect JWT/qsh), token-attribution gating, TID-COLLIDE-01 binding, the renamed _is_repo_auto_review_enabled gates, ci-autofix trigger wiring. Handlers keep module-attribute access style (common.X / service.X).
Retarget the ~240 monkeypatch.setattr(webapp, ...) sites across the 11 test files + tests/conftest.py + tests/e2e/harness.py per upstream's retarget pattern (webhook_common / handler modules) — find every site by grep, not by count.
Extra gates before committing: full unit suite; FULL E2E (Playwright + real langgraph dev server); residual-importer sweep — git grep for "agent.webapp"/"from agent import webapp" must return only the shim and intentional compat references (list what remains in notes).`,
{ label: 'C4', phase: 'C4 webapp split', model: 'fable', effort: 'high', schema: RESULT_SCHEMA },
))
log('C4 committed — entering security review (hard gate before C5)')
// ---------------------------------------------------------------------------
// C4 security review — detector fan-out (sonnet lenses) + proof-or-kill verifier (opus),
// with a bounded fix loop (fable) per the approved failure path (new commits, no rewrites).
// ---------------------------------------------------------------------------
phase('C4 security review')
const LENSES = [
{ key: 'authz', focus: 'broken object-level auth, missing access checks, webhook signature verification gaps (GitHub HMAC / Slack / Linear / Jira shared-secret+HMAC / Connect JWT+qsh), token-attribution gating, TID-COLLIDE-01 thread binding, the _is_repo_auto_review_enabled gates' },
{ key: 'injection', focus: 'untrusted webhook payload handling: parsing, header trust, SSRF-prone fetches, path handling in the moved dispatch code' },
{ key: 'logic', focus: 'multi-step invariants broken by the split: dispatch ordering, dedup/replay protection, race conditions across the new module boundaries, dropped code paths (diff every moved function against its pre-split source)' },
]
const FINDINGS_SCHEMA = {
type: 'object',
properties: { findings: { type: 'array', items: { type: 'object' } } },
required: ['findings'],
}
const VERDICT_SCHEMA = {
type: 'object',
properties: { confirmed: { type: 'array', items: { type: 'object' } }, unverified: { type: 'array', items: { type: 'object' } } },
required: ['confirmed', 'unverified'],
}
let reviewRound = 0
let confirmedHighs = []
while (true) {
reviewRound += 1
const c4diff = `the C4 split commit(s) on ${BRANCH} in ${REPO} (git show for each C4/C4a commit; compare moved code against pre-split agent/webapp.py at the merge base)`
const found = (await parallel(LENSES.map(l => () =>
agent(
`You are a hostile ${l.key} security auditor. Audit ONLY ${l.key} issues in ${c4diff}. Focus: ${l.focus}. Read the actual files at both revisions; for each checklist area either cite the specific safe line or file a finding {id,title,claimed_severity,file,line,data_flow,proof:{input,outcome},recommendation}. Findings must be about the SPLIT (regressions vs pre-split behavior), not pre-existing issues. Return {findings:[...]}.`,
{ label: `detect:${l.key}:r${reviewRound}`, phase: 'C4 security review', model: 'sonnet', schema: FINDINGS_SCHEMA },
)))).filter(Boolean).flatMap(r => r.findings)
if (!found.length) { results.securityReview = { rounds: reviewRound, confirmed: [], outcome: 'clean' }; break }
const verdict = await agent(
`You are a skeptical exploitation verifier — you did NOT find these and are rewarded for killing weak claims. For each candidate finding on ${c4diff}, demand a concrete proof-of-exploit (specific input -> specific bad outcome, consistent with the code at HEAD of ${BRANCH}). Default to unverified when uncertain. Candidates: ${JSON.stringify(found)}. Return {confirmed:[...], unverified:[...]} keeping severity only on confirmed items.`,
{ label: `verify:r${reviewRound}`, phase: 'C4 security review', model: 'opus', effort: 'high', schema: VERDICT_SCHEMA },
)
confirmedHighs = (verdict?.confirmed || []).filter(f => ['critical', 'high'].includes((f.claimed_severity || f.severity || '').toLowerCase()))
if (!confirmedHighs.length) {
results.securityReview = { rounds: reviewRound, confirmed: verdict?.confirmed || [], unverified: verdict?.unverified || [], outcome: 'pass' }
break
}
if (reviewRound >= 3) {
results.securityReview = { rounds: reviewRound, confirmed: confirmedHighs, outcome: 'BLOCKED' }
throw new Error(`Security review still has ${confirmedHighs.length} confirmed critical/high after ${reviewRound} rounds — plan failure path: fundamental-flaw handling, return to Adam. Branch left intact for inspection.`)
}
log(`Security round ${reviewRound}: ${confirmedHighs.length} confirmed critical/high — dispatching fix commit C4a`)
ensure(`C4a-r${reviewRound}`, await agent(
`${COMMON}
STAGE: C4a — address confirmed security-review findings as a NEW commit on ${BRANCH} (plan failure path: no history rewrite, no force-push). Findings with proofs: ${JSON.stringify(confirmedHighs)}. Fix each within the split's scope; re-run full unit + E2E before committing.`,
{ label: `C4a:r${reviewRound}`, phase: 'C4 security review', model: 'fable', effort: 'high', schema: RESULT_SCHEMA },
))
}
log(`Security review outcome: ${results.securityReview.outcome} after ${results.securityReview.rounds} round(s)`)
// ---------------------------------------------------------------------------
// HARD GATE passed — C5 and C6 (sequential commits; C6 depends only on branch order, not C5 content)
// ---------------------------------------------------------------------------
phase('C5 test moves')
ensure('C5', await agent(
`${COMMON}
STAGE: C5 — tests/<domain>/ moves (plan section "C5", scoping §2a + §2c placements).
git mv every test per the move-map; apply remaining R<100 monkeypatch retargets not already done in C4; place the 13 fork-only tests per the approved table (webhooks/, auth/, tools/, sandbox/, github/). Path-only commit — zero logic edits.
Extra gate: uv run pytest --co -q collects everything (no import errors, count matches pre-move) then full unit green.`,
{ label: 'C5', phase: 'C5 test moves', model: 'sonnet', effort: 'low', schema: RESULT_SCHEMA },
))
phase('C6 UI moves')
ensure('C6', await agent(
`${COMMON}
STAGE: C6 — ui/src/features/ moves (plan section "C6").
git mv per the move-map including ported/ -> features/agents/experiments|chat; rewrite @/components/agents and @/lib/agents imports across the 54 importer files AND the moved files themselves (grep-driven); swap the tsconfig/eslint exclude lists for the single experiments glob; retarget the AgentPromptBar shim; delete ui/pnpm-workspace.yaml. Fork-diverged files (PlanReview.tsx, WorkflowApprovalCard.tsx, AgentsSidebar.tsx, SidebarFilterMenu.tsx, AgentGitPanel.tsx, AutomationEditor.tsx, DiffView.tsx, CloudPromptBar.tsx) keep fork content — imports only.
Extra gate: cd ui && bun install && bunx tsc --noEmit && bun run build, then Playwright E2E.`,
{ label: 'C6', phase: 'C6 UI moves', model: 'sonnet', schema: RESULT_SCHEMA },
))
phase('C7 docs/ledger/memory')
ensure('C7', await agent(
`${COMMON}
STAGE: C7 — docs + ledger + memory (plan section "C7" — read its obligations verbatim; you own them).
1. Update fork CLAUDE.md / README.md / AGENTS.md path references (architecture sections naming agent/webapp.py, old test paths, ui paths).
2. Ledger: flip 8356eb34 to landed in docs/upstream-sync/triage.jsonl with branch ${BRANCH}; add re-triage notes to the 8 unblocked rows per scoping §5; make triage-render; make triage-check must pass.
3. Memory: update /Users/adammoussa/.claude/projects/-Users-adammoussa-Documents-repositories-seahaven-open-swe/memory/open-swe-upstream-triage-status.md (+ MEMORY.md hook if needed) with the new module layout summary (agent/{graphs,runtime,api,review,resources,webhooks/*_routes}, tests/<domain>/, ui/src/features/) and the reorg-landed status. Memory edits are NOT part of the git commit.
4. Confluence check: via ToolSearch load the Atlassian MCP search tools; search the IT space for any page documenting this repo's module map. If found, update it; if none, or if Confluence is unreachable, record the exact outcome string ("updated page <id>" / "no Confluence change required" / "OUTSTANDING as of <today> — Confluence inaccessible") in your notes AND in the memory file.
5. Stage docs/upstream-sync/domain-reorg/ and .claude/workflows/domain-reorg-adoption.mjs so the plan, scoping artifacts, and this workflow ship with the exercise.
Commit all repo-file changes as the C7 commit; run ruff + triage-check as gates.`,
{ label: 'C7', phase: 'C7 docs/ledger/memory', model: 'opus', schema: RESULT_SCHEMA },
))
// ---------------------------------------------------------------------------
// PR — push and open, NO merge. Merge is Adam's (after @openswe review).
// ---------------------------------------------------------------------------
phase('PR')
results.pr = await agent(
`${COMMON}
STAGE: PR (no commit). Push ${BRANCH} to origin (plain push — the pre-push hook runs security scanners; if it blocks, report, never bypass). Open a PR against dev with gh pr create --base dev. Title: "refactor: adopt upstream domain reorg (8356eb34) — fork content, upstream layout". Body MUST include: the commit-by-commit summary from this build (paste from your reading of git log), explicit review-scope callouts for C2 (reviewer-module moves) and C4 (auth-surface split), the security-review outcome (${JSON.stringify(results.securityReview?.outcome)}), the C7 Confluence-check outcome, the post-deploy verification checklist from the plan (as unchecked boxes), and "MERGE-COMMIT ONLY — do not squash/rebase". Do NOT merge, do NOT approve, do NOT comment @openswe — Adam drives the review.
Return JSON: {ok, notes} with the PR URL in notes.`,
{ label: 'open-pr', phase: 'PR', model: 'sonnet', schema: RESULT_SCHEMA },
)
return {
branch: BRANCH,
commits: results.commits,
securityReview: results.securityReview,
pr: results.pr?.notes || 'PR step failed — branch is pushed or local; open manually',
next: 'Adam: review PR (C2+C4 scope), run @openswe review, merge with a MERGE COMMIT, then run the post-deploy checklist and post results to the PR.',
}

View file

@ -45,6 +45,8 @@ jobs:
steps:
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.3.14"
- name: Install UI dependencies
working-directory: ui
run: bun install --frozen-lockfile
@ -74,6 +76,8 @@ jobs:
with:
node-version: "24"
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.3.14"
- name: Install Python deps (langgraph dev runtime)
run: uv sync --locked
- name: Install Playwright + Chromium
@ -132,6 +136,8 @@ jobs:
steps:
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.3.14"
- name: ui/package.json and ui/bun.lock agree
working-directory: ui
run: bun install --frozen-lockfile

View file

@ -14,25 +14,27 @@ Dependencies are managed with **uv**. Tests use pytest (`asyncio_mode = "auto"`)
```bash
make install # uv pip install -e .
make dev # uv run langgraph dev — serves all three graphs + the FastAPI app from langgraph.json
make dev # uv run langgraph dev — serves all six graphs + the FastAPI app from langgraph.json
make run # uvicorn agent.webapp:app --reload --port 8000 (FastAPI only, no LangGraph runtime)
make test # uv run pytest -vvv tests/
make test TEST_FILE=tests/test_open_pr_middleware.py # single test file
uv run pytest -vvv tests/test_open_pr_middleware.py::test_name # single test
make test TEST_FILE=tests/github/test_open_pull_request.py # single test file
uv run pytest -vvv tests/github/test_open_pull_request.py::test_name # single test
make lint # ruff check + ruff format --diff
make format # ruff format + ruff check --fix
```
`langgraph.json` declares three graph entrypoints and the FastAPI app, all served together by `langgraph dev`:
`langgraph.json` declares six graph entrypoints and the FastAPI app, all served together by `langgraph dev`. Every graph entrypoint targets a thin `agent.graphs.*` re-export shim that delegates to the unmoved factory module (domain reorg):
| Graph | Entrypoint | Purpose |
|---|---|---|
| `agent` | `agent.server:get_agent` | Main coding agent (Slack/Linear/Jira/Confluence/GitHub-triggered). |
| `reviewer` | `agent.reviewer:get_reviewer_agent` | Read-only PR reviewer. Findings model + `publish_review`. |
| `analyzer` | `agent.analyzer:get_analyzer` | Learns per-repo reviewer style from historical PRs and this reviewer's own finding outcomes. |
| `ci_monitor` | `agent.ci_monitor:get_ci_monitor` | Polling fallback for CI auto-fix: each tick sweeps open agent-authored PRs for failing checks / merge conflicts via `agent.ci_autofix.sweep_open_prs`. |
| `agent` | `agent.graphs.agent:traced_agent` (shim → `agent.server:get_agent`) | Main coding agent (Slack/Linear/Jira/Confluence/GitHub-triggered). |
| `reviewer` | `agent.graphs.reviewer:traced_reviewer_agent` (shim → `agent.reviewer:get_reviewer_agent`) | Read-only PR reviewer. Findings model + `publish_review`. |
| `analyzer` | `agent.graphs.analyzer:traced_analyzer` (shim → `agent.analyzer:get_analyzer`) | Learns per-repo reviewer style from historical PRs and this reviewer's own finding outcomes. |
| `chat` | `agent.graphs.chat:traced_chat_agent` | Dashboard Agents chat graph. |
| `scheduler` | `agent.graphs.scheduler:get_scheduler` | Reconcile sweep for stragglers. |
| `ci_monitor` | `agent.graphs.ci_monitor:get_ci_monitor` (shim → `agent.ci_monitor:get_ci_monitor`) | Polling fallback for CI auto-fix: each tick sweeps open agent-authored PRs for failing checks / merge conflicts via `agent.ci_autofix.sweep_open_prs`. |
The FastAPI app is `agent.webapp:app`.
The FastAPI app is `agent.webapp:app` — now a compatibility shim re-exporting `agent.api.app:app`.
CI auto-fix ("PR babysitting") lives in `agent/ci_autofix.py`: when a CI check fails (webhook `check_run` / `check_suite` / `workflow_run` / `status`) or a reviewer leaves actionable feedback on a PR Open SWE opened, it locates the originating agent thread (by `pr_url` metadata) and dispatches a confidence-gated fix run on the `agent` graph. Gated by the per-user `auto_fix_ci` profile flag, the enabled-repos opt-in, and a per-PR `@open-swe autofix on|off` toggle (`agent/dashboard/autofix_state.py`). Skip-rules (base-branch failures, human commits, same-head dedupe, batching while runs are active, loop cap) all live in `ci_autofix.py`.
@ -41,9 +43,9 @@ CI auto-fix ("PR babysitting") lives in `agent/ci_autofix.py`: when a CI check f
### Entrypoints
- **`agent/server.py` → `get_agent(config)`** — main graph factory. Called per-thread. Resolves the GitHub token, gets-or-creates the sandbox for the thread, resolves the team/profile/per-thread model + effort, then constructs a fresh `create_deep_agent(...)` with the curated tool list and middleware stack. The agent itself is stateless — all per-thread state lives in the sandbox + thread metadata.
- **`agent/reviewer.py` → `get_reviewer_agent(config)`** — reviewer graph factory. Shares `ensure_sandbox_for_thread` with the main agent but wires a reviewer-only toolset (`add_finding`, `update_finding`, `list_findings`, `publish_review`, `web_search`, `fetch_url`, `http_request`) and a different system prompt that pins the single-evolving-findings model and the diff-anchored bar for filing a finding. Read-only: no commit/push/PR-opening tools.
- **`agent/reviewer.py` → `get_reviewer_agent(config)`** — reviewer graph factory. Shares `ensure_sandbox_for_thread` with the main agent but wires a reviewer-only toolset (`add_finding`, `update_finding`, `list_findings`, `publish_review`, `web_search`, `fetch_url`, `http_request`) and a different system prompt that pins the single-evolving-findings model and the diff-anchored bar for filing a finding. Read-only: no commit/push/PR-opening tools. Its supporting modules live in the `agent/review/` package (domain reorg): `findings.py`, `publish.py`, `reconcile.py`, `trace_context.py`, `diff.py`, `groups.py`, `eval_store.py`, `style_collector.py`, `style_guidance.py`.
- **`agent/analyzer.py` → `get_analyzer(config)`** — small graph that emits a per-repo style prompt via the `save_review_style_prompt` tool, consumed by the reviewer as a "repository-specific review style" appendix. It runs in one of two modes (`analyzer_mode` in `configurable`): **bootstrap** (cold-start: crawl historical PR reviews) and **continual** (nightly: refine using this reviewer's own finding outcomes via `read_finding_outcomes`). Each mode's procedure lives in a deepagents **skill** (`agent/skills/bootstrap-repo-analysis/`, `agent/skills/continual-learning/`) served as virtual files via a `CompositeBackend` `/skills/` route + `StateBackend` (seeded into the run's `files` channel by the launcher — never written to the sandbox). Launchers and the per-repo nightly cron live in `agent/dashboard/review_style_jobs.py` and `agent/dashboard/analyzer_cron.py`; the cron is registered when bootstrap completes.
- **`agent/webapp.py`** — custom FastAPI routes mounted alongside the LangGraph server. Webhooks land here (GitHub, Linear, Slack, Jira, and the Confluence Atlassian Connect `/connect/*` routes). Each webhook resolves a deterministic `thread_id` (so follow-up messages route to the same agent run) and triggers/streams a run via the `langgraph_sdk` client. Also auto-reviews PRs on `opened` / `ready_for_review` events when the repo+author opt in. The Atlassian triggers (`agent/webhooks/{jira,confluence}.py`, `agent/utils/atlassian_connect.py`) verify webhook trust — a Jira Automation shared secret / optional HMAC, and a Confluence Connect HS256-JWT + `qsh` with RS256 signed-install — then re-fetch the triggering comment server-side before deriving identity.
- **FastAPI layer (`agent/api/` + `agent/webhooks/`)** — custom FastAPI routes mounted alongside the LangGraph server. The domain reorg split the fork's former `agent/webapp.py` monolith into `agent/api/app.py` (app composition + router mounts), `agent/api/health.py` (`/health`, `/webhooks/run-complete`), shared helpers in `agent/webhooks/common.py`, and per-source route modules `agent/webhooks/{github,linear,slack,jira,confluence}_routes.py`; `agent/webapp.py` remains a compatibility shim re-exporting `app`. Webhooks land in those route modules (GitHub, Linear, Slack, Jira, and the Confluence Atlassian Connect `/connect/*` routes, which fold into `confluence_routes.py`). Each webhook resolves a deterministic `thread_id` (so follow-up messages route to the same agent run) and triggers/streams a run via the `langgraph_sdk` client. Also auto-reviews PRs on `opened` / `ready_for_review` events when the repo+author opt in. The Atlassian triggers (`agent/webhooks/{jira,confluence}.py`, `agent/utils/atlassian_connect.py`) verify webhook trust — a Jira Automation shared secret / optional HMAC, and a Confluence Connect HS256-JWT + `qsh` with RS256 signed-install — then re-fetch the triggering comment server-side before deriving identity.
- **`agent/dashboard/`** — `router` mounted under the FastAPI app at startup (`app.include_router(dashboard_router)`). Owns GitHub OAuth, per-user profiles, admin endpoints, team defaults, enabled-repo lists, review-style management, and the Agents chat thread API used by the UI in `ui/`.
### Sandbox lifecycle (the tricky part)
@ -114,10 +116,10 @@ Webhooks compute deterministic thread ids so the same Linear issue / Slack threa
## Conventions
- Tests are unit-only by default (`tests/`). Integration tests would go under `tests/integration_tests/` (currently empty — `make integration_tests` no-ops if missing).
- Tests are unit-only by default and organized by domain under `tests/<domain>/` (`tests/agent/`, `tests/auth/`, `tests/github/`, `tests/webhooks/`, `tests/reviewer/`, `tests/sandbox/`, `tests/middleware/`, `tests/models/`, `tests/slack/`, `tests/tools/`, `tests/dashboard/`, `tests/analyzer/`); `tests/conftest.py` and the `tests/e2e/` harness stay at the top level. Integration tests would go under `tests/integration_tests/` (currently empty — `make integration_tests` no-ops if missing).
- New sandbox providers: add a module under `agent/integrations/` and wire it into `SANDBOX_FACTORIES` in `agent/utils/sandbox.py`. See `CUSTOMIZATION.md`.
- New tools: add to `agent/tools/`, export from `agent/tools/__init__.py`, add to the `tools=[...]` list in `server.py:get_agent` (or `reviewer.py` for reviewer-only tools).
- New middleware: add to `agent/middleware/`, export from `agent/middleware/__init__.py`, add to the `middleware=[...]` list in `server.py:get_agent` — order is significant (see the stack above).
- New dashboard endpoints: add to `agent/dashboard/routes.py`. The router is auto-mounted on the FastAPI app.
- New graphs: register the entrypoint in `langgraph.json` under `graphs`.
- New graphs: add an `agent/graphs/<name>.py` re-export shim that delegates to the factory module, then register the shim entrypoint (`agent.graphs.<name>:<symbol>`) in `langgraph.json` under `graphs`.
- Minimal-to-no code comments — only when the *why* isn't obvious from the code.

View file

@ -14,33 +14,40 @@ Dependencies are managed with **uv**. Tests use pytest (`asyncio_mode = "auto"`)
```bash
make install # uv pip install -e .
make dev # uv run langgraph dev — serves all three graphs + the FastAPI app from langgraph.json
make dev # uv run langgraph dev — serves all six graphs + the FastAPI app from langgraph.json
make run # uvicorn agent.webapp:app --reload --port 8000 (FastAPI only, no LangGraph runtime)
make test # uv run pytest -vvv tests/
make test TEST_FILE=tests/test_open_pr_middleware.py # single test file
uv run pytest -vvv tests/test_open_pr_middleware.py::test_name # single test
make test TEST_FILE=tests/github/test_open_pull_request.py # single test file
uv run pytest -vvv tests/github/test_open_pull_request.py::test_name # single test
make lint # ruff check + ruff format --diff
make format # ruff format + ruff check --fix
```
`langgraph.json` declares three graph entrypoints and the FastAPI app, all served together by `langgraph dev`:
`langgraph.json` declares six graph entrypoints and the FastAPI app, all served together by `langgraph dev`. Since the domain reorg, every graph entrypoint targets a thin `agent.graphs.*` re-export shim (`agent/graphs/<name>.py`) rather than the factory module directly; the shims delegate to the unmoved factories (`agent/server.py`, `agent/reviewer.py`, `agent/analyzer.py`, `agent/chat.py`, `agent/scheduler.py`, `agent/ci_monitor.py`):
| Graph | Entrypoint | Purpose |
|---|---|---|
| `agent` | `agent.server:traced_agent` (wraps `get_agent`) | Main coding agent (Slack/Linear/Jira/Confluence/GitHub-triggered). |
| `reviewer` | `agent.reviewer:traced_reviewer_agent` (wraps `get_reviewer_agent`) | Read-only PR reviewer. Findings model + `publish_review`. |
| `analyzer` | `agent.analyzer:traced_analyzer` (wraps `get_analyzer`) | Learns per-repo reviewer style from historical PRs and this reviewer's own finding outcomes. |
| `agent` | `agent.graphs.agent:traced_agent` (shim → `agent.server:get_agent`) | Main coding agent (Slack/Linear/Jira/Confluence/GitHub-triggered). |
| `reviewer` | `agent.graphs.reviewer:traced_reviewer_agent` (shim → `agent.reviewer:get_reviewer_agent`) | Read-only PR reviewer. Findings model + `publish_review`. |
| `analyzer` | `agent.graphs.analyzer:traced_analyzer` (shim → `agent.analyzer:get_analyzer`) | Learns per-repo reviewer style from historical PRs and this reviewer's own finding outcomes. |
| `chat` | `agent.graphs.chat:traced_chat_agent` | Dashboard Agents chat graph. |
| `scheduler` | `agent.graphs.scheduler:get_scheduler` | Reconcile sweep for stragglers. |
| `ci_monitor` | `agent.graphs.ci_monitor:get_ci_monitor` | Fork-only polling fallback for the CI auto-fix flow. |
The FastAPI app is `agent.webapp:app`.
The FastAPI app is `agent.webapp:app` — now a compatibility shim that re-exports `agent.api.app:app` (see the FastAPI split under "Entrypoints").
## Architecture
### Entrypoints
- **`agent/server.py` → `get_agent(config)`** — main graph factory. Called per-thread. Resolves the GitHub token, gets-or-creates the sandbox for the thread, resolves the team/profile/per-thread model + effort, then constructs a fresh `create_deep_agent(...)` with the curated tool list and middleware stack. The agent itself is stateless — all per-thread state lives in the sandbox + thread metadata.
- **`agent/reviewer.py` → `get_reviewer_agent(config)`** — reviewer graph factory. Shares `ensure_sandbox_for_thread` with the main agent but wires a reviewer-only toolset (`add_finding`, `update_finding`, `list_findings`, `publish_review`, `web_search`, `fetch_url`, `http_request`) and a different system prompt that pins the single-evolving-findings model and the diff-anchored bar for filing a finding. Read-only: no commit/push/PR-opening tools.
- **`agent/reviewer.py` → `get_reviewer_agent(config)`** — reviewer graph factory. Shares `ensure_sandbox_for_thread` with the main agent but wires a reviewer-only toolset (`add_finding`, `update_finding`, `list_findings`, `publish_review`, `web_search`, `fetch_url`, `http_request`) and a different system prompt that pins the single-evolving-findings model and the diff-anchored bar for filing a finding. Read-only: no commit/push/PR-opening tools. Its supporting modules live in the **`agent/review/`** package (domain reorg): `findings.py`, `publish.py`, `reconcile.py`, `trace_context.py`, `diff.py`, `groups.py`, `eval_store.py`, `style_collector.py`, `style_guidance.py`.
- **`agent/analyzer.py` → `get_analyzer(config)`** — small graph that emits a per-repo style prompt via the `save_review_style_prompt` tool, consumed by the reviewer as a "repository-specific review style" appendix. It runs in one of two modes (`analyzer_mode` in `configurable`): **bootstrap** (cold-start: crawl historical PR reviews) and **continual** (nightly: refine using this reviewer's own finding outcomes via `read_finding_outcomes`). Each mode's procedure lives in a deepagents **skill** (`agent/skills/bootstrap-repo-analysis/`, `agent/skills/continual-learning/`) served as virtual files via a `CompositeBackend` `/skills/` route + `StateBackend` (seeded into the run's `files` channel by the launcher — never written to the sandbox). Launchers and the per-repo nightly cron live in `agent/dashboard/review_style_jobs.py` and `agent/dashboard/analyzer_cron.py`; the cron is registered when bootstrap completes.
- **`agent/webapp.py`** — thin FastAPI routing layer mounted alongside the LangGraph server. Defines the webhook routes (GitHub, Linear, Slack, Jira, Confluence Connect `/connect/*`) plus `/webhooks/run-complete`, and keeps the shared helpers/constants; the per-source handlers live in **`agent/webhooks/{github,slack,linear,jira,confluence}.py`** (re-exported from `webapp` so existing call sites and tests keep working). Each webhook resolves a deterministic `thread_id` (so follow-up messages route to the same agent run) and triggers a run through the single durable dispatch contract in **`agent/dispatch.py`** (`dispatch_agent_run`: `multitask_strategy="interrupt"` + `durability="sync"` + completion webhook); `agent/completion.py` posts a failure reply if a run dies, and `agent/reconcile.py` (a `scheduler`-graph sweep) catches stragglers. The GitHub handler also auto-reviews PRs on `opened` / `ready_for_review` and drives the CI auto-fix flow (`agent/ci_autofix.py`).
- **FastAPI layer (`agent/api/` + `agent/webhooks/`)** — the domain reorg split the fork's former 2,590-line `agent/webapp.py` monolith into a per-source layout; `agent/webapp.py` is now a 4-line compatibility shim (`from .api.app import app`). The pieces:
- **`agent/api/app.py`** composes the FastAPI app and mounts every router; **`agent/api/health.py`** owns `/health` and `/webhooks/run-complete`.
- **`agent/webhooks/common.py`** holds the shared verify/dispatch helpers and constants (signature verification, thread-id derivation, the dispatch entry). Route modules and handlers reach these via **module-attribute access** (`common.X`) so the ~240 `monkeypatch.setattr` test sites retarget cleanly.
- Per-source **route modules** `agent/webhooks/{github,linear,slack,jira,confluence}_routes.py` define the HTTP routes (GitHub, Linear, Slack, Jira, and the Confluence Atlassian Connect `/connect/*` + `/webhooks/jira` routes; the fork-only `jira_routes.py`/`confluence_routes.py` mirror upstream's `github_routes.py` pattern, and the Connect lifecycle/descriptor routes fold into `confluence_routes.py`). The **handler modules** `agent/webhooks/{github,slack,linear,jira,confluence}.py` carry the per-source logic.
- Each webhook resolves a deterministic `thread_id` (so follow-up messages route to the same agent run) and triggers a run through the single durable dispatch contract in **`agent/dispatch.py`** (`dispatch_agent_run`: `multitask_strategy="interrupt"` + `durability="sync"` + completion webhook); `agent/completion.py` posts a failure reply if a run dies, and `agent/reconcile.py` (a `scheduler`-graph sweep) catches stragglers. The GitHub handler also auto-reviews PRs on `opened` / `ready_for_review` and drives the CI auto-fix flow (`agent/ci_autofix.py`).
- **Atlassian triggers.** The Jira trigger is a Jira **Automation** rule POSTing to `/webhooks/jira` with a shared-secret header (`verify_jira_secret`; optional HMAC-body+timestamp via `JIRA_WEBHOOK_REQUIRE_SIGNATURE`), since Jira Cloud has no native webhook signing. The Confluence trigger is a private **Atlassian Connect app** (`agent/utils/atlassian_connect.py`): the `comment_created` webhook is HS256-JWT-verified against the per-tenant stored `sharedSecret` with a hand-rolled `qsh` (query-string-hash) check; `signed-install` is on, so install/uninstall lifecycle callbacks are RS256-verified against Atlassian's published keys (no trust-on-first-use). Install secrets are stored **encrypted** in the LangGraph store, keyed by `clientKey`. Both Atlassian webhook bodies are treated as pointers only — the triggering comment's real author/text is re-fetched server-side via the Basic-auth service account before anything security-relevant is derived, and attribution is gated on an active user mapping (mirrors the GitHub-login / token-attribution flow). Descriptor served at `GET /connect/atlassian-connect.json`.
- **`agent/dashboard/`** — `router` mounted under the FastAPI app at startup (`app.include_router(dashboard_router)`). Owns GitHub OAuth, per-user profiles, admin endpoints, team defaults, enabled-repo lists, review-style management, and the Agents chat thread API used by the UI in `ui/`.
@ -112,17 +119,17 @@ Webhooks compute deterministic thread ids so the same Linear issue / Slack threa
## Conventions
- Tests are unit-only by default (`tests/`). Integration tests would go under `tests/integration_tests/` (currently empty — `make integration_tests` no-ops if missing).
- Tests are unit-only by default and organized by domain under `tests/<domain>/` (`tests/agent/`, `tests/auth/`, `tests/github/`, `tests/webhooks/`, `tests/reviewer/`, `tests/sandbox/`, `tests/middleware/`, `tests/models/`, `tests/slack/`, `tests/tools/`, `tests/dashboard/`, `tests/analyzer/`); `tests/conftest.py` and the `tests/e2e/` harness stay at the top level. Integration tests would go under `tests/integration_tests/` (currently empty — `make integration_tests` no-ops if missing).
- New sandbox providers: add a module under `agent/integrations/` and wire it into `SANDBOX_FACTORIES` in `agent/utils/sandbox.py`. See `CUSTOMIZATION.md`.
- New tools: add to `agent/tools/`, export from `agent/tools/__init__.py`, add to the `tools=[...]` list in `server.py:get_agent` (or `reviewer.py` for reviewer-only tools).
- New middleware: add to `agent/middleware/`, export from `agent/middleware/__init__.py`, add to the `middleware=[...]` list in `server.py:get_agent` — order is significant (see the stack above).
- New dashboard endpoints: add to `agent/dashboard/routes.py`. The router is auto-mounted on the FastAPI app.
- New graphs: register the entrypoint in `langgraph.json` under `graphs`.
- New graphs: add an `agent/graphs/<name>.py` re-export shim that delegates to the factory module, then register the shim entrypoint (`agent.graphs.<name>:<symbol>`) in `langgraph.json` under `graphs`.
- Minimal-to-no code comments — only when the *why* isn't obvious from the code.
## Fork maintenance — syncing `upstream/main`
This is a long-lived fork of `langchain-ai/open-swe` with Sea Haven customizations woven into upstream-owned files (notably `agent/prompt.py` prompt constants, `agent/webapp.py`, and the tool/middleware wiring). Merging upstream is a triage exercise, not a fast-forward. When you want upstream's clean changes but must **defer a large structural refactor** (and its entangled features), work in this order:
This is a long-lived fork of `langchain-ai/open-swe` with Sea Haven customizations woven into upstream-owned files (notably `agent/prompt.py` prompt constants, the `agent/api/` + `agent/webhooks/` FastAPI layer — the former `agent/webapp.py` monolith, now split and left as a shim — and the tool/middleware wiring). Merging upstream is a triage exercise, not a fast-forward. When you want upstream's clean changes but must **defer a large structural refactor** (and its entangled features), work in this order:
1. **Triage before resolving.** Merge-base is `git merge-base HEAD upstream/main`. The truthful conflict set is the combined merge, `git merge-tree --write-tree --name-only HEAD upstream/main` — a per-commit probe against each commit's parent *overstates* conflicts (a file a refactor merely added shows up as a phantom `modify/delete`). Decide keep-baseline vs adopt-refactor **before** resolving, and surface the choice to a human for any auth/webhook/IAM surface.
2. **Chase the cascade, not just the textual conflicts.** The hard part is the non-conflicting files the refactor also touched. Get the refactor's file set (`git diff-tree --no-commit-id --name-status -r <refactor-sha>`) and cross-reference the files this fork modified (`git diff --name-only <fork-base> HEAD`). Files in both = hand-resolve; files only the refactor touched = mechanical.

View file

@ -1,9 +1,9 @@
<div align="center">
<a href="https://github.com/langchain-ai/open-swe">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="static/dark.svg">
<source media="(prefers-color-scheme: light)" srcset="static/light.svg">
<img alt="Open SWE Logo" src="static/dark.svg" width="35%">
<source media="(prefers-color-scheme: dark)" srcset="assets/dark.svg">
<source media="(prefers-color-scheme: light)" srcset="assets/light.svg">
<img alt="Open SWE Logo" src="assets/dark.svg" width="35%">
</picture>
</a>
</div>
@ -54,7 +54,7 @@ create_deep_agent(
Every task runs in its own **isolated cloud sandbox** — a remote Linux environment with full shell access. The repo is cloned in, the agent gets full permissions, and the blast radius of any mistake is fully contained. No production access, no confirmation prompts.
Open SWE supports multiple sandbox providers out of the box — [Modal](https://modal.com/), [Daytona](https://www.daytona.io/), [Runloop](https://www.runloop.ai/), [E2B](https://e2b.dev/), and [LangSmith](https://smith.langchain.com/) — and you can plug in your own. See the [Customization Guide](CUSTOMIZATION.md#1-sandbox) for details.
Open SWE supports multiple sandbox providers out of the box — [Modal](https://modal.com/), [Daytona](https://www.daytona.io/), [Runloop](https://www.runloop.ai/), [E2B](https://e2b.dev/), and [LangSmith](https://smith.langchain.com/) — and you can plug in your own. See the [Customization Guide](docs/CUSTOMIZATION.md#1-sandbox) for details.
This follows the principle all three companies converge on: **isolate first, then give full permissions inside the boundary.**
@ -112,7 +112,7 @@ All three companies in the article converge on **Slack as the primary invocation
- **Confluence** — Comment `@openswe` on a page. A private Atlassian Connect app delivers the `comment_created` event; the agent acts and replies on the page.
- **GitHub** — Tag `@openswe` in PR comments on agent-created PRs to have it address review feedback and push fixes to the same branch.
See **[INSTALLATION.md](./INSTALLATION.md) §5** for per-surface trigger setup.
See **[INSTALLATION.md](./docs/INSTALLATION.md) §5** for per-surface trigger setup.
Each invocation creates a deterministic thread ID, so follow-up messages on the same issue or thread route to the same running agent.
@ -123,7 +123,7 @@ Each invocation creates a deterministic thread ID, so follow-up messages on the
### 7. Validation — Prompt-Driven
The agent is instructed to run linters, formatters, and tests before committing, and is responsible end-to-end for committing, pushing, opening/updating the draft PR, and replying in the source channel.
This is an area where you can extend Open SWE for your org: add deterministic CI checks, visual verification, or review gates as additional middleware. See the [Customization Guide](CUSTOMIZATION.md#6-middleware) for how.
This is an area where you can extend Open SWE for your org: add deterministic CI checks, visual verification, or review gates as additional middleware. See the [Customization Guide](docs/CUSTOMIZATION.md#6-middleware) for how.
---
@ -156,8 +156,8 @@ This is an area where you can extend Open SWE for your org: add deterministic CI
## Getting Started
- **[Installation Guide](INSTALLATION.md)** — local dev (backend + dashboard), GitHub App creation, LangSmith, Linear/Slack/GitHub triggers, and production deployment
- **[Customization Guide](CUSTOMIZATION.md)** — swap the sandbox, model, tools, triggers, system prompt, and middleware for your org
- **[Installation Guide](docs/INSTALLATION.md)** — local dev (backend + dashboard), GitHub App creation, LangSmith, Linear/Slack/GitHub triggers, and production deployment
- **[Customization Guide](docs/CUSTOMIZATION.md)** — swap the sandbox, model, tools, triggers, system prompt, and middleware for your org
## Deployment (Sea Haven fork)
@ -169,7 +169,7 @@ and secrets live in the LangGraph deployment config and Vercel environment
variables. Promotion from `dev` to `prod` (`main`) is handled by
[`.github/workflows/promote-to-main.yml`](.github/workflows/promote-to-main.yml).
See **[INSTALLATION.md § 10 "Production deployment"](INSTALLATION.md#10-production-deployment)**
See **[INSTALLATION.md § 10 "Production deployment"](docs/INSTALLATION.md#10-production-deployment)**
for the full backend + dashboard setup.
> The earlier self-hosted AWS stack (CDK under `infra/`, an ARM64 EC2 box + nginx

View file

@ -36,7 +36,7 @@ from .middleware import (
TimeoutWrapupMiddleware,
ToolErrorMiddleware,
)
from .review_style_guidance import REVIEWER_STYLE_THEMES
from .review.style_guidance import REVIEWER_STYLE_THEMES
from .server import (
DEFAULT_LLM_MAX_TOKENS,
DEFAULT_LLM_MODEL_ID,

1
agent/api/__init__.py Normal file
View file

@ -0,0 +1 @@
"""FastAPI application composition."""

57
agent/api/app.py Normal file
View file

@ -0,0 +1,57 @@
"""FastAPI application composition."""
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from ..dashboard import router as dashboard_router
from ..dashboard.plan_api import plan_router
from ..dashboard.workflow_approval_api import workflow_approval_router
from ..webhooks.confluence_routes import router as confluence_webhook_router
from ..webhooks.github_routes import router as github_webhook_router
from ..webhooks.jira_routes import router as jira_webhook_router
from ..webhooks.linear_routes import router as linear_webhook_router
from ..webhooks.slack_routes import router as slack_webhook_router
from .health import router as health_router
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
from ..utils.model import validate_local_dev_llm_config
from ..utils.sandbox import validate_sandbox_startup_config
validate_sandbox_startup_config()
validate_local_dev_llm_config()
yield
app = FastAPI(lifespan=lifespan)
DASHBOARD_ALLOWED_ORIGINS: list[str] = [
o.strip() for o in os.environ.get("DASHBOARD_ALLOWED_ORIGINS", "").split(",") if o.strip()
]
if DASHBOARD_ALLOWED_ORIGINS:
if "*" in DASHBOARD_ALLOWED_ORIGINS:
raise RuntimeError(
"DASHBOARD_ALLOWED_ORIGINS must not include '*' when allow_credentials=True"
)
app.add_middleware(
CORSMiddleware,
allow_origins=DASHBOARD_ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["*"],
)
app.include_router(dashboard_router)
app.include_router(plan_router)
app.include_router(workflow_approval_router)
app.include_router(linear_webhook_router)
app.include_router(jira_webhook_router)
app.include_router(confluence_webhook_router)
app.include_router(slack_webhook_router)
app.include_router(health_router)
app.include_router(github_webhook_router)

27
agent/api/health.py Normal file
View file

@ -0,0 +1,27 @@
"""Health and run-completion routes."""
from fastapi import APIRouter, HTTPException, Request
from ..completion import handle_run_completion, verify_run_complete_token
router = APIRouter()
@router.get("/health")
async def health_check() -> dict[str, str]:
"""Health check endpoint."""
return {"status": "healthy"}
@router.post("/webhooks/run-complete")
async def run_complete_webhook(request: Request) -> dict[str, str]:
"""Platform run-completion webhook: post a failure reply for runs that died."""
if not verify_run_complete_token(request.query_params.get("token")):
raise HTTPException(status_code=401, detail="Invalid run-complete token")
try:
payload = await request.json()
except Exception: # noqa: BLE001
return {"status": "error", "message": "Invalid JSON"}
if not isinstance(payload, dict):
return {"status": "ignored", "reason": "payload not an object"}
return await handle_run_completion(payload)

View file

@ -4,7 +4,7 @@ This is the shared core for "PR babysitting": when a CI check fails (or a
reviewer leaves actionable feedback) on a PR that Open SWE opened, locate the
originating agent thread and dispatch a confidence-gated fix run on it.
Both the GitHub webhook path (:mod:`agent.webapp`) and the polling fallback
Both the GitHub webhook path (:mod:`agent.webhooks.github_routes`) and the polling fallback
(:mod:`agent.ci_monitor`) call into here, so all the skip-rules, dedupe, and
loop-capping live in one place. Skip-rules mirror Cursor/Claude Code:
@ -26,7 +26,7 @@ from .dashboard.agent_overrides import load_profile, resolve_login_from_email_as
from .dashboard.autofix_state import is_pr_autofix_disabled
from .dashboard.enabled_repos import is_review_repo_enabled
from .dispatch import dispatch_agent_run
from .reviewer_findings import REVIEWER_THREAD_KIND
from .review.findings import REVIEWER_THREAD_KIND
from .utils.dashboard_links import dashboard_thread_url
from .utils.github_app import get_github_app_installation_token
from .utils.github_checks import post_autofix_status_check

View file

@ -3,7 +3,7 @@
``router`` is loaded lazily (PEP 562): importing any dashboard submodule
(e.g. ``agent.dashboard.options`` from middleware) executes this __init__,
and it must NOT drag in routes.py + FastAPI + every API/job module. Only the
webapp, which actually mounts the router, pays that cost.
API app (``agent.api.app``), which actually mounts the router, pays that cost.
"""
from typing import Any

View file

@ -12,7 +12,7 @@ from typing import Any, Literal
import httpx
from langgraph_sdk import get_client
from ..reviewer_findings import REVIEWER_THREAD_KIND
from ..review.findings import REVIEWER_THREAD_KIND
from ..utils.github_app import get_github_app_installation_token
USAGE_THREAD_NAMESPACE: list[str] = ["agent_usage", "threads"]

View file

@ -17,13 +17,13 @@ from typing import Any, Literal, TypedDict
from langgraph_sdk import get_client
from agent.reviewer_eval_store import (
from agent.review.eval_store import (
_HEARTBEAT_STALE_SECONDS,
DEFAULT_EVAL_PROJECT,
EVALS_NAMESPACE,
REVIEWER_EVAL_KEY,
)
from agent.reviewer_findings import REVIEW_FINDING_CAP
from agent.review.findings import REVIEW_FINDING_CAP
logger = logging.getLogger(__name__)

View file

@ -19,7 +19,7 @@ from urllib.parse import urljoin, urlparse
import httpx
from fastapi import HTTPException, Response
from ..reviewer_findings import REVIEWER_THREAD_KIND
from ..review.findings import REVIEWER_THREAD_KIND
from ..utils.github_app import get_github_app_installation_token
from ..utils.github_checks import github_headers
from ..utils.thread_ops import langgraph_client
@ -443,7 +443,7 @@ async def create_review_comment(
_HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
# Inline comments the reviewer posts carry this hidden marker (see reviewer_publish).
# Inline comments the reviewer posts carry this hidden marker (see review.publish).
_OPEN_SWE_COMMENT_RE = re.compile(r"<!--\s*open-swe-review-comment\b")
_REVIEW_COMMENTS_PER_PAGE = 100
# Bound the fetch so a pathological PR can't trigger unbounded paging (~2000 comments).
@ -697,7 +697,7 @@ async def proxy_pr_image(owner: str, repo: str, pr_number: int, url: str) -> Res
async def trigger_re_review(owner: str, repo: str, pr_number: int, login: str) -> dict[str, Any]:
from ..utils.slack import GitHubPrRef
from ..webapp import trigger_pr_review_from_ref
from ..webhooks.github import trigger_pr_review_from_ref
pr_ref = GitHubPrRef(
owner=owner,
@ -715,10 +715,10 @@ async def dry_run_trace_resolution(owner: str, repo: str, pr_number: int) -> dic
"""Resolve a PR to its author coding-agent thread without running a review."""
from dataclasses import asdict
from ..reviewer_trace_context import resolve_pr_trace
from ..review.trace_context import resolve_pr_trace
from ..utils.github_app import get_github_app_installation_token_with_expiry
from ..utils.slack import GitHubPrRef
from ..webapp import fetch_github_pr_metadata
from ..webhooks.common import fetch_github_pr_metadata
pr_ref = GitHubPrRef(
owner=owner,

View file

@ -19,8 +19,8 @@ from typing import Any
import httpx
from fastapi import HTTPException
from ..reviewer_diff import fetch_pr_diff
from ..reviewer_findings import REVIEWER_THREAD_KIND
from ..review.diff import fetch_pr_diff
from ..review.findings import REVIEWER_THREAD_KIND
from ..utils.github_app import get_github_app_installation_token
from ..utils.thread_ops import langgraph_client, langgraph_url
from .options import SUPPORTED_MODEL_IDS, model_supports_effort

View file

@ -8,7 +8,7 @@ from typing import Any
from langgraph_sdk import get_client
from ..review_style_collector import (
from ..review.style_collector import (
collect_review_samples,
format_samples_for_analyzer,
generate_review_style_thread_id,
@ -29,7 +29,7 @@ _ASSISTANT_ID = "analyzer"
def _client():
"""LangGraph SDK client for the current deployment (same resolution as webapp)."""
"""LangGraph SDK client for the current deployment (same resolution as webhook common)."""
url = os.environ.get("LANGGRAPH_URL") or os.environ.get("LANGGRAPH_URL_PROD")
if url:
return get_client(url=url)

1
agent/graphs/__init__.py Normal file
View file

@ -0,0 +1 @@
__all__: list[str] = []

3
agent/graphs/agent.py Normal file
View file

@ -0,0 +1,3 @@
from agent.server import get_agent, traced_agent
__all__ = ["get_agent", "traced_agent"]

3
agent/graphs/analyzer.py Normal file
View file

@ -0,0 +1,3 @@
from agent.analyzer import get_analyzer, traced_analyzer
__all__ = ["get_analyzer", "traced_analyzer"]

3
agent/graphs/chat.py Normal file
View file

@ -0,0 +1,3 @@
from agent.chat import get_chat_agent, traced_chat_agent
__all__ = ["get_chat_agent", "traced_chat_agent"]

View file

@ -0,0 +1,3 @@
from agent.ci_monitor import get_ci_monitor
__all__ = ["get_ci_monitor"]

3
agent/graphs/reviewer.py Normal file
View file

@ -0,0 +1,3 @@
from agent.reviewer import get_reviewer_agent, traced_reviewer_agent
__all__ = ["get_reviewer_agent", "traced_reviewer_agent"]

View file

@ -0,0 +1,3 @@
from agent.scheduler import get_scheduler
__all__ = ["get_scheduler"]

View file

@ -16,8 +16,8 @@ from langchain.agents.middleware import AgentState, after_agent
from langgraph.config import get_config
from langgraph.runtime import Runtime
from ..reviewer_findings import get_thread_metadata
from ..reviewer_publish import settle_review_check_run
from ..review.findings import get_thread_metadata
from ..review.publish import settle_review_check_run
from ..utils.github_token import get_github_token
logger = logging.getLogger(__name__)

View file

@ -1,6 +1,7 @@
import logging
import os
import shlex
from importlib import resources
from pathlib import Path
from deepagents import HarnessProfile, register_harness_profile
@ -14,10 +15,7 @@ from .utils.github_comments import UNTRUSTED_GITHUB_COMMENT_OPEN_TAG
logger = logging.getLogger(__name__)
DEFAULT_PROMPT_PATH = os.environ.get(
"DEFAULT_PROMPT_PATH",
str(Path(__file__).resolve().parent.parent / "default_prompt.md"),
)
DEFAULT_PROMPT_PATH = os.environ.get("DEFAULT_PROMPT_PATH")
# Tools stripped from the agent regardless of run state (none today: plan-mode
# tool stripping is dynamic and handled by PlanModeMiddleware, not the profile).
@ -37,19 +35,28 @@ def _load_default_prompt() -> str:
Returns empty string if the file doesn't exist or can't be read.
"""
try:
path = Path(DEFAULT_PROMPT_PATH)
if path.is_file():
content = path.read_text().strip()
if content:
# Escape curly braces so .format() doesn't choke on them
escaped = content.replace("{", "{{").replace("}", "}}")
return f"""---
if DEFAULT_PROMPT_PATH:
content = Path(DEFAULT_PROMPT_PATH).read_text().strip()
else:
content = (
resources.files("agent.resources")
.joinpath("default_prompt.md")
.read_text(encoding="utf-8")
.strip()
)
if content:
# Escape curly braces so .format() doesn't choke on them
escaped = content.replace("{", "{{").replace("}", "}}")
return f"""---
### Custom Instructions
{escaped}"""
except Exception:
logger.warning("Failed to read default prompt file at %s", DEFAULT_PROMPT_PATH)
logger.warning(
"Failed to read default prompt from %s",
DEFAULT_PROMPT_PATH or "agent.resources/default_prompt.md",
)
return ""

View file

@ -0,0 +1,15 @@
from agent.utils.model import (
DEFAULT_LLM_REASONING,
ModelKwargs,
fallback_model_id_for,
make_model,
provider_model_kwargs,
)
__all__ = [
"DEFAULT_LLM_REASONING",
"ModelKwargs",
"fallback_model_id_for",
"make_model",
"provider_model_kwargs",
]

View file

@ -0,0 +1 @@
__all__: list[str] = []

11
agent/review/__init__.py Normal file
View file

@ -0,0 +1,11 @@
from agent.review.findings import (
REVIEW_FINDING_CAP,
REVIEWER_THREAD_KIND,
Finding,
)
__all__ = [
"Finding",
"REVIEWER_THREAD_KIND",
"REVIEW_FINDING_CAP",
]

View file

@ -217,7 +217,7 @@ async def fetch_pr_diff(
"""
import httpx
from .utils.github_http import github_client, github_request
from ..utils.github_http import github_client, github_request
url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
try:
@ -253,7 +253,7 @@ async def fetch_pr_metadata(
"""
import httpx
from .utils.github_http import github_client, github_request
from ..utils.github_http import github_client, github_request
url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
try:

View file

@ -22,8 +22,8 @@ from typing import TypedDict
from langchain_core.language_models.chat_models import BaseChatModel
from pydantic import BaseModel, Field
from .reviewer_diff import parse_unified_diff
from .reviewer_findings import get_thread_metadata, set_reviewer_thread_metadata
from .diff import parse_unified_diff
from .findings import get_thread_metadata, set_reviewer_thread_metadata
logger = logging.getLogger(__name__)

View file

@ -25,22 +25,22 @@ from typing import Any, TypedDict
import httpx
from .reviewer_findings import (
from ..utils.dashboard_links import dashboard_thread_url
from ..utils.github_checks import CheckConclusion, complete_review_check_run
from ..utils.github_http import (
GITHUB_API_BASE,
GITHUB_GRAPHQL,
github_client,
github_request,
)
from ..utils.github_token import GitHubAuthError
from .findings import (
DiffSide,
Finding,
get_thread_metadata,
normalize_finding_title,
set_reviewer_thread_metadata,
)
from .utils.dashboard_links import dashboard_thread_url
from .utils.github_checks import CheckConclusion, complete_review_check_run
from .utils.github_http import (
GITHUB_API_BASE,
GITHUB_GRAPHQL,
github_client,
github_request,
)
from .utils.github_token import GitHubAuthError
logger = logging.getLogger(__name__)

View file

@ -2,14 +2,14 @@ from __future__ import annotations
from typing import Any
from .reviewer_findings import (
from .findings import (
Finding,
FindingInteraction,
_coerce_surface,
list_findings,
replace_findings,
)
from .reviewer_publish import parse_review_comment_marker
from .publish import parse_review_comment_marker
ReviewThread = dict[str, Any]
ReviewThreadMatch = tuple[ReviewThread, int | None]

View file

@ -14,10 +14,10 @@ from typing import Any
from deepagents.backends.protocol import SandboxBackendProtocol
from .dashboard.team_credentials import get_langsmith_credentials
from .dashboard.team_settings import get_team_review_tracing_project
from .integrations.langsmith_tools import _client
from .utils.langsmith import get_langsmith_trace_url
from ..dashboard.team_credentials import get_langsmith_credentials
from ..dashboard.team_settings import get_team_review_tracing_project
from ..integrations.langsmith_tools import _client
from ..utils.langsmith import get_langsmith_trace_url
logger = logging.getLogger(__name__)

View file

@ -55,17 +55,17 @@ from .middleware import (
refresh_github_proxy_before_model,
settle_review_check_on_exit,
)
from .reviewer_diff import compute_diff_line_set, fetch_pr_diff, fetch_pr_metadata
from .reviewer_findings import (
from .review.diff import compute_diff_line_set, fetch_pr_diff, fetch_pr_metadata
from .review.findings import (
REVIEW_FINDING_CAP,
)
from .reviewer_findings import (
from .review.findings import (
list_findings as list_findings_async,
)
from .reviewer_groups import maybe_generate_and_store_diff_groups
from .reviewer_publish import fetch_pr_review_threads
from .reviewer_reconcile import reconcile_findings_with_review_threads
from .reviewer_trace_context import (
from .review.groups import maybe_generate_and_store_diff_groups
from .review.publish import fetch_pr_review_threads
from .review.reconcile import reconcile_findings_with_review_threads
from .review.trace_context import (
PRTraceContext,
format_pr_trace_context_prompt,
prepare_pr_trace_context,

25
agent/runtime/__init__.py Normal file
View file

@ -0,0 +1,25 @@
from .constants import (
DEFAULT_LLM_MAX_TOKENS,
DEFAULT_LLM_MODEL_ID,
DEFAULT_RECURSION_LIMIT,
MODEL_CALL_RECURSION_LIMIT,
)
from .execution import graph_loaded_for_execution
from .sandbox import (
configure_git_identity,
ensure_sandbox_for_thread,
get_cached_sandbox_backend,
recreate_sandbox,
)
__all__ = [
"DEFAULT_LLM_MAX_TOKENS",
"DEFAULT_LLM_MODEL_ID",
"DEFAULT_RECURSION_LIMIT",
"MODEL_CALL_RECURSION_LIMIT",
"configure_git_identity",
"ensure_sandbox_for_thread",
"get_cached_sandbox_backend",
"graph_loaded_for_execution",
"recreate_sandbox",
]

View file

@ -0,0 +1,13 @@
from agent.server import (
DEFAULT_LLM_MAX_TOKENS,
DEFAULT_LLM_MODEL_ID,
DEFAULT_RECURSION_LIMIT,
MODEL_CALL_RECURSION_LIMIT,
)
__all__ = [
"DEFAULT_LLM_MAX_TOKENS",
"DEFAULT_LLM_MODEL_ID",
"DEFAULT_RECURSION_LIMIT",
"MODEL_CALL_RECURSION_LIMIT",
]

View file

@ -0,0 +1,3 @@
from agent.server import graph_loaded_for_execution
__all__ = ["graph_loaded_for_execution"]

53
agent/runtime/sandbox.py Normal file
View file

@ -0,0 +1,53 @@
from collections.abc import Sequence
from deepagents.backends.protocol import SandboxBackendProtocol
async def ensure_sandbox_for_thread(
thread_id: str,
*,
github_proxy_token: str | None = None,
github_proxy_repositories: Sequence[str] | None = None,
repo: dict[str, str] | None = None,
) -> SandboxBackendProtocol:
from agent.server import ensure_sandbox_for_thread as ensure
return await ensure(
thread_id,
github_proxy_token=github_proxy_token,
github_proxy_repositories=github_proxy_repositories,
repo=repo,
)
def get_cached_sandbox_backend(thread_id: str) -> SandboxBackendProtocol:
# Fork's sync 4-case `__creating__` sentinel lifecycle (see AGENTS.md) handles
# reconnection internally in `ensure_sandbox_for_thread`/`check_or_recreate_sandbox`,
# so unlike upstream's shim there is no caller-supplied `reconnect` callback here:
# fork's `_get_cached_sandbox_backend` is a plain cache lookup.
from agent.server import _get_cached_sandbox_backend as get_backend
return get_backend(thread_id)
async def configure_git_identity(sandbox_backend: SandboxBackendProtocol) -> None:
from agent.server import _configure_git_identity as configure
await configure(sandbox_backend)
async def recreate_sandbox(
thread_id: str,
*,
github_proxy_token: str | None = None,
github_proxy_repositories: Sequence[str] | None = None,
repo: dict[str, str] | None = None,
) -> SandboxBackendProtocol:
from agent.server import _recreate_sandbox as recreate
return await recreate(
thread_id,
github_proxy_token=github_proxy_token,
github_proxy_repositories=github_proxy_repositories,
repo=repo,
)

View file

@ -6,8 +6,8 @@ from typing import Any
from langgraph.config import get_config
from ..reviewer_diff import is_range_in_diff
from ..reviewer_findings import (
from ..review.diff import is_range_in_diff
from ..review.findings import (
DEFAULT_FINDING_TITLE,
MAX_SUGGESTION_LINES,
Confidence,
@ -133,7 +133,7 @@ async def add_finding(
diff_hunk: str | None = None
if isinstance(diff_text, str) and diff_text:
from ..reviewer_diff import extract_diff_hunk
from ..review.diff import extract_diff_hunk
diff_hunk = extract_diff_hunk(diff_text, file, start_line, end_line)

View file

@ -4,12 +4,12 @@ from __future__ import annotations
from typing import Any
from ..reviewer_findings import (
from ..review.findings import (
ReviewerThreadMissingError,
get_thread_id_from_runtime,
thread_missing_tool_result,
)
from ..reviewer_findings import (
from ..review.findings import (
list_findings as list_findings_async,
)

View file

@ -11,7 +11,7 @@ from typing import Any
from langgraph.config import get_config
from ..reviewer_findings import list_findings as list_findings_async
from ..review.findings import list_findings as list_findings_async
_COMPACT_FIELDS = (
"id",

View file

@ -7,8 +7,8 @@ from typing import Any
from langgraph.config import get_config
from ..dashboard.team_settings import get_team_review_trace_links_enabled
from ..reviewer_diff import compute_diff_line_set, fetch_pr_diff, is_range_in_diff
from ..reviewer_findings import (
from ..review.diff import compute_diff_line_set, fetch_pr_diff, is_range_in_diff
from ..review.findings import (
REVIEW_FINDING_CAP,
REVIEWER_EVAL_PUBLICATION_KEY,
SEVERITY_ORDER,
@ -26,10 +26,10 @@ from ..reviewer_findings import (
set_reviewer_thread_metadata,
thread_missing_tool_result,
)
from ..reviewer_findings import (
from ..review.findings import (
list_findings as list_findings_async,
)
from ..reviewer_publish import (
from ..review.publish import (
clear_review_started_comment,
fetch_pr_review_threads,
fetch_review_comments,
@ -44,7 +44,7 @@ from ..reviewer_publish import (
resolve_review_thread,
settle_review_check_run,
)
from ..reviewer_reconcile import reconcile_findings_with_review_threads
from ..review.reconcile import reconcile_findings_with_review_threads
from ..utils.dashboard_links import dashboard_review_url
from ..utils.github_checks import review_check_conclusion
from ..utils.github_token import (

View file

@ -4,7 +4,7 @@ from typing import Any
from langgraph.config import get_config
from ..reviewer_findings import (
from ..review.findings import (
FindingInteraction,
ReviewerThreadMissingError,
append_finding_interaction,
@ -13,7 +13,7 @@ from ..reviewer_findings import (
thread_missing_tool_result,
update_finding_fields,
)
from ..reviewer_publish import reply_to_review_comment
from ..review.publish import reply_to_review_comment
from ..utils.github_token import get_github_token

View file

@ -14,7 +14,7 @@ async def trigger_pr_review_from_ref(
slack_channel_id: str = "",
slack_thread_ts: str = "",
) -> dict[str, Any]:
from agent.webapp import trigger_pr_review_from_ref as _trigger_pr_review_from_ref
from agent.webhooks.github import trigger_pr_review_from_ref as _trigger_pr_review_from_ref
return await _trigger_pr_review_from_ref(
pr_ref,

View file

@ -4,7 +4,7 @@ from typing import Any
from langgraph.config import get_config
from ..reviewer_findings import (
from ..review.findings import (
Finding,
ReviewerThreadMissingError,
get_finding,
@ -13,14 +13,14 @@ from ..reviewer_findings import (
update_finding_fields,
update_finding_surface,
)
from ..reviewer_publish import (
from ..review.publish import (
fetch_pr_review_threads,
fetch_review_thread_id_for_comment,
render_resolution_comment,
reply_to_review_comment,
resolve_review_thread,
)
from ..reviewer_reconcile import reconcile_findings_with_review_threads
from ..review.reconcile import reconcile_findings_with_review_threads
from ..utils.github_token import get_github_token
from ..utils.reviewer_outcomes import emit_finding_status_outcome

View file

@ -6,7 +6,7 @@ from typing import Any
from langgraph.config import get_config
from ..reviewer_findings import (
from ..review.findings import (
DEFAULT_FINDING_TITLE,
MAX_SUGGESTION_LINES,
Finding,

View file

@ -10,7 +10,7 @@ from typing import Any
from langgraph_sdk import get_client
from langgraph_sdk.client import LangGraphClient
from ..reviewer_findings import list_findings
from ..review.findings import list_findings
from .langsmith import create_langsmith_feedback, delete_langsmith_feedback
from .reviewer_outcomes import outcome_from_score, upsert_finding_outcome

File diff suppressed because it is too large Load diff

1732
agent/webhooks/common.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -12,9 +12,10 @@ from typing import Any
from langchain_core.messages.content import create_text_block
from agent import webapp
from agent.utils import atlassian_connect as ac
from . import common
# The Connect app's own Confluence service-account accountId. When set, comments
# authored by it are ignored (self-trigger loop guard, like the Linear botActor
# / Jira comment_author_is_bot early-outs).
@ -39,7 +40,7 @@ async def process_install(request: Any, body: dict[str, Any]) -> tuple[int, str]
claims = await ac.verify_asymmetric_install_jwt(request, expected_client_key=client_key)
if claims is None:
webapp.logger.warning("Rejecting Connect install for %s: signature unverified", client_key)
common.logger.warning("Rejecting Connect install for %s: signature unverified", client_key)
return 401, "Install verification failed"
# Mandatory tenant binding: signed-install proves the caller is *an*
@ -47,7 +48,7 @@ async def process_install(request: Any, body: dict[str, Any]) -> tuple[int, str]
# (signature-verified) clientKey are accepted. To bootstrap, add the
# clientKey logged here to CONNECT_EXPECTED_CLIENT_KEYS and re-install.
if not ac.client_key_allowed(client_key):
webapp.logger.warning(
common.logger.warning(
"Rejecting Connect install: clientKey %s not in CONNECT_EXPECTED_CLIENT_KEYS",
client_key,
)
@ -56,14 +57,14 @@ async def process_install(request: Any, body: dict[str, Any]) -> tuple[int, str]
# Defense-in-depth (only enforced when configured): the callback's baseUrl
# host must be our Confluence site.
if ac.CONNECT_EXPECTED_BASE_URL_HOSTS and not ac.base_url_host_allowed(base_url):
webapp.logger.warning("Rejecting Connect install: baseUrl %s not allowed", base_url)
common.logger.warning("Rejecting Connect install: baseUrl %s not allowed", base_url)
return 403, "baseUrl host not allowed"
existing = await ac.get_installation(client_key)
await ac.put_installation(
client_key, shared_secret, base_url, product_type, first_install=existing is None
)
webapp.logger.info(
common.logger.info(
"Connect %s verified and stored for %s",
"first-install" if existing is None else "re-install",
client_key,
@ -78,13 +79,13 @@ async def process_uninstall(request: Any, body: dict[str, Any]) -> tuple[int, st
return 400, "Missing clientKey"
claims = await ac.verify_asymmetric_install_jwt(request, expected_client_key=client_key)
if claims is None:
webapp.logger.warning("Rejecting Connect uninstall for %s: unverified", client_key)
common.logger.warning("Rejecting Connect uninstall for %s: unverified", client_key)
return 401, "Uninstall verification failed"
existing = await ac.get_installation(client_key)
if existing is None:
return 204, "" # idempotent
await ac.delete_installation(client_key)
webapp.logger.info("Connect uninstall verified for %s", client_key)
common.logger.info("Connect uninstall verified for %s", client_key)
return 204, ""
@ -104,12 +105,12 @@ async def process_confluence_comment(payload: dict[str, Any], client_key: str =
"""Corroborate a comment_created event server-side and dispatch a run."""
comment_id = _extract_comment_id(payload)
if not comment_id:
webapp.logger.debug("Ignoring Confluence webhook: no comment id in payload")
common.logger.debug("Ignoring Confluence webhook: no comment id in payload")
return
server_comment = await webapp.fetch_confluence_comment(comment_id)
server_comment = await common.fetch_confluence_comment(comment_id)
if not server_comment:
webapp.logger.warning(
common.logger.warning(
"Rejecting Confluence webhook: comment %s could not be corroborated", comment_id
)
return
@ -124,43 +125,43 @@ async def process_confluence_comment(payload: dict[str, Any], client_key: str =
# Self-trigger loop guard: ignore the app's own comments (its confluence_comment
# replies can echo "@openswe" and otherwise re-trigger).
if CONFLUENCE_BOT_ACCOUNT_ID and account_id == CONFLUENCE_BOT_ACCOUNT_ID:
webapp.logger.debug("Ignoring Confluence webhook: comment authored by the bot account")
common.logger.debug("Ignoring Confluence webhook: comment authored by the bot account")
return
for prefix in webapp._GITHUB_BOT_MESSAGE_PREFIXES:
for prefix in common._GITHUB_BOT_MESSAGE_PREFIXES:
if body_text.startswith(prefix):
webapp.logger.debug("Ignoring Confluence webhook: comment is our own bot message")
common.logger.debug("Ignoring Confluence webhook: comment is our own bot message")
return
if "@openswe" not in body_text.lower():
webapp.logger.debug("Ignoring Confluence webhook: comment doesn't mention @openswe")
common.logger.debug("Ignoring Confluence webhook: comment doesn't mention @openswe")
return
actor_email = await webapp.get_confluence_user_email(account_id) if account_id else None
actor_email = await common.get_confluence_user_email(account_id) if account_id else None
repo_config = webapp.extract_repo_from_text(body_text, default_owner=webapp.DEFAULT_REPO_OWNER)
repo_config = common.extract_repo_from_text(body_text, default_owner=common.DEFAULT_REPO_OWNER)
if not repo_config:
repo_config = webapp.get_repo_config_from_confluence_mapping(space_key)
repo_config = common.get_repo_config_from_confluence_mapping(space_key)
if not repo_config:
repo_config = await webapp.get_team_default_repo()
repo_config = await common.get_team_default_repo()
if not repo_config:
webapp.logger.info("Ignoring Confluence webhook: no repo resolved for space %s", space_key)
common.logger.info("Ignoring Confluence webhook: no repo resolved for space %s", space_key)
return
if not webapp._is_repo_allowed(repo_config):
webapp.logger.warning(
if not common._is_repo_allowed(repo_config):
common.logger.warning(
"Rejecting Confluence webhook: repo '%s/%s' not in allowlist",
repo_config.get("owner"),
repo_config.get("name"),
)
return
mapped_login = await webapp.resolve_login_from_email_async(actor_email) if actor_email else None
if mapped_login and not webapp.is_login_mapped(mapped_login):
webapp.logger.info(
mapped_login = await common.resolve_login_from_email_async(actor_email) if actor_email else None
if mapped_login and not common.is_login_mapped(mapped_login):
common.logger.info(
"Confluence actor login %s is not an active mapping; running unattributed", mapped_login
)
mapped_login = None
thread_id = webapp.generate_thread_id_from_confluence_comment(client_key, comment_id)
page = await webapp.fetch_confluence_page(page_id) if page_id else None
thread_id = common.generate_thread_id_from_confluence_comment(client_key, comment_id)
page = await common.fetch_confluence_page(page_id) if page_id else None
page_title = (page or {}).get("title", "") or "Confluence page"
page_url = (page or {}).get("url", "")
@ -191,7 +192,7 @@ async def process_confluence_comment(payload: dict[str, Any], client_key: str =
if mapped_login:
configurable["github_login"] = mapped_login
await webapp.upsert_agent_thread_owner_metadata(
await common.upsert_agent_thread_owner_metadata(
thread_id,
source="confluence",
repo_config=repo_config,
@ -201,14 +202,14 @@ async def process_confluence_comment(payload: dict[str, Any], client_key: str =
source_context={"confluence": configurable["confluence"]},
)
run = await webapp.dispatch_agent_run(
run = await common.dispatch_agent_run(
thread_id,
content_blocks,
configurable,
source="confluence",
metadata=webapp._AGENT_VERSION_METADATA,
metadata=common._AGENT_VERSION_METADATA,
)
webapp.logger.info(
common.logger.info(
"LangGraph run dispatched for Confluence thread %s (run=%s)",
thread_id,
run.get("run_id") if isinstance(run, dict) else None,

View file

@ -0,0 +1,81 @@
"""Confluence webhook HTTP routes (Atlassian Connect app).
The Confluence trigger is a private Atlassian Connect app, so the descriptor
and install/uninstall lifecycle callbacks (``/connect/*``) live here alongside
the JWT-verified ``comment_created`` webhook. JWT/qsh verification machinery
stays in ``agent.utils.atlassian_connect``.
"""
from fastapi import APIRouter
from . import common
from . import confluence as service
router = APIRouter()
@router.get("/connect/atlassian-connect.json")
async def connect_descriptor() -> dict[str, common.Any]:
"""Serve the Atlassian Connect app descriptor (baseUrl from CONNECT_BASE_URL).
signed-install is true: Atlassian asymmetrically (RS256) signs the lifecycle
callbacks, so install/uninstall are cryptographically authenticated against
Atlassian's published keys (no trust-on-first-use). The comment_created
webhook stays symmetric (HS256 against the stored per-tenant sharedSecret).
"""
return {
"key": "sea-haven-open-swe-confluence",
"name": "Open SWE",
"description": "Triggers Open SWE runs from Confluence comments mentioning @openswe.",
"baseUrl": common.CONNECT_BASE_URL,
"vendor": {"name": "Sea Haven Industries", "url": "https://seahavenind.com"},
"authentication": {"type": "jwt"},
"apiMigrations": {"signed-install": True, "gdpr": True},
"lifecycle": {"installed": "/connect/installed", "uninstalled": "/connect/uninstalled"},
"scopes": ["READ"],
"modules": {
"webhooks": [{"event": "comment_created", "url": "/connect/webhook/comment-created"}]
},
}
@router.post("/connect/installed")
async def connect_installed(request: common.Request) -> common.Response:
"""Connect install lifecycle: trust-on-first-use (host-gated), verify re-install."""
try:
body = await request.json()
except Exception: # noqa: BLE001
raise common.HTTPException(status_code=400, detail="Invalid JSON") from None
code, detail = await service.process_install(request, body)
if code >= 400:
raise common.HTTPException(status_code=code, detail=detail)
return common.Response(status_code=code)
@router.post("/connect/uninstalled")
async def connect_uninstalled(request: common.Request) -> common.Response:
"""Connect uninstall lifecycle: verify against the stored secret before deleting."""
try:
body = await request.json()
except Exception: # noqa: BLE001
raise common.HTTPException(status_code=400, detail="Invalid JSON") from None
code, detail = await service.process_uninstall(request, body)
if code >= 400:
raise common.HTTPException(status_code=code, detail=detail)
return common.Response(status_code=code)
@router.post("/connect/webhook/comment-created")
async def connect_comment_created(
request: common.Request, background_tasks: common.BackgroundTasks
) -> dict[str, str]:
"""JWT-verified Confluence comment_created trigger."""
claims = await common.verify_connect_webhook(request)
if claims is None:
raise common.HTTPException(status_code=401, detail="Invalid Connect JWT")
try:
payload = await request.json()
except Exception: # noqa: BLE001
return {"status": "error", "message": "Invalid JSON"}
background_tasks.add_task(service.process_confluence_comment, payload, claims.get("iss", ""))
return {"status": "accepted"}

View file

@ -1,18 +1,16 @@
"""GitHub webhook handlers — moved out of webapp.py (behavior-identical).
"""GitHub webhook handlers — moved out of common.py (behavior-identical).
Helpers and constants stay in webapp.py; they are accessed through the module
object (``webapp.X``) so tests that monkeypatch them keep working.
Helpers and constants stay in common.py; they are accessed through the module
object (``common.X``) so tests that monkeypatch them keep working.
"""
import re
import uuid
from typing import Any
from agent import webapp
from ..ci_autofix import handle_ci_failure, handle_review_feedback
from ..dashboard.autofix_state import set_pr_autofix_disabled
from ..reviewer_findings import FindingInteraction, ReviewerPRMeta, ReviewerSlackThread
from ..review.findings import FindingInteraction, ReviewerPRMeta, ReviewerSlackThread
from ..utils.github_ci import (
branch_from_check_payload,
head_sha_from_check_payload,
@ -20,6 +18,7 @@ from ..utils.github_ci import (
)
from ..utils.github_comments import GitHubAuthError
from ..utils.slack import GitHubPrRef
from . import common
def build_github_issue_prompt(
@ -37,9 +36,9 @@ def build_github_issue_prompt(
"""Build the user prompt for a GitHub issue-triggered run."""
triggered_by_line = f"## Triggered by: {github_login}\n\n" if github_login else ""
issue_url_line = f"## Issue URL: {issue_url}\n\n" if issue_url else ""
comments_text = webapp._build_github_issue_comments_text(comments)
sanitized_title = webapp.sanitize_github_comment_body(title)
formatted_body = webapp.format_github_comment_body_for_prompt(
comments_text = common._build_github_issue_comments_text(comments)
sanitized_title = common.sanitize_github_comment_body(title)
formatted_body = common.format_github_comment_body_for_prompt(
issue_author or github_login, body
)
return (
@ -63,13 +62,13 @@ def build_github_issue_prompt(
def build_github_issue_followup_prompt(github_login: str, comment_body: str) -> str:
"""Build the prompt for a follow-up GitHub issue comment."""
return f"**{github_login}:**\n{webapp.format_github_comment_body_for_prompt(github_login, comment_body)}"
return f"**{github_login}:**\n{common.format_github_comment_body_for_prompt(github_login, comment_body)}"
def build_github_issue_update_prompt(github_login: str, title: str, body: str) -> str:
"""Build the prompt for a follow-up GitHub issue title/body update."""
sanitized_title = webapp.sanitize_github_comment_body(title)
formatted_body = webapp.format_github_comment_body_for_prompt(github_login, body)
sanitized_title = common.sanitize_github_comment_body(title)
formatted_body = common.format_github_comment_body_for_prompt(github_login, body)
return (
f"**{github_login}:** updated the GitHub issue title/body.\n\n"
f"Title: {sanitized_title}\n\n"
@ -110,24 +109,24 @@ async def trigger_pr_review_from_ref(
# Full token to read PR metadata (privacy/id aren't in the trigger ref);
# re-scoped below once we know whether the repo is public.
app_token, app_token_expires_at = await webapp.get_github_app_installation_token_with_expiry()
app_token, app_token_expires_at = await common.get_github_app_installation_token_with_expiry()
if not app_token:
webapp.logger.warning("No GitHub App token available for PR reviewer request")
common.logger.warning("No GitHub App token available for PR reviewer request")
return {"success": False, "error": "No GitHub App token available"}
pr_metadata = await webapp.fetch_github_pr_metadata(pr_ref, token=app_token)
pr_metadata = await common.fetch_github_pr_metadata(pr_ref, token=app_token)
if not pr_metadata:
return {"success": False, "error": "Could not fetch pull request metadata"}
repo_private = webapp._repo_private_from_pr_metadata(pr_metadata)
repo_id = webapp._repo_id_from_pr_metadata(pr_metadata)
app_token, app_token_expires_at = await webapp._reviewer_token_for_repo(
repo_private = common._repo_private_from_pr_metadata(pr_metadata)
repo_id = common._repo_id_from_pr_metadata(pr_metadata)
app_token, app_token_expires_at = await common._reviewer_token_for_repo(
repo_config,
repo_private=repo_private,
repo_id=repo_id,
)
if not app_token:
webapp.logger.warning("No GitHub App token available for PR reviewer request")
common.logger.warning("No GitHub App token available for PR reviewer request")
return {"success": False, "error": "No GitHub App token available"}
base_sha = pr_metadata.get("base", {}).get("sha", "")
@ -138,12 +137,12 @@ async def trigger_pr_review_from_ref(
pr_title = pr_metadata.get("title", "")
pr_url = pr_metadata.get("html_url", "") or pr_ref.url
if not base_sha or not head_sha:
webapp.logger.warning("Missing base/head SHA for Slack PR review request")
common.logger.warning("Missing base/head SHA for Slack PR review request")
return {"success": False, "error": "Pull request metadata is missing base/head SHA"}
thread_id = webapp.generate_reviewer_thread_id(pr_ref.owner, pr_ref.repo, pr_ref.number)
langgraph_client = webapp.get_client(url=webapp.LANGGRAPH_URL)
if not await webapp._ensure_thread_exists_for_metadata(thread_id, langgraph_client):
thread_id = common.generate_reviewer_thread_id(pr_ref.owner, pr_ref.repo, pr_ref.number)
langgraph_client = common.get_client(url=common.LANGGRAPH_URL)
if not await common._ensure_thread_exists_for_metadata(thread_id, langgraph_client):
return {"success": False, "error": "Could not create reviewer thread"}
pr_meta: ReviewerPRMeta = {
@ -162,10 +161,10 @@ async def trigger_pr_review_from_ref(
"channel_id": slack_channel_id,
"thread_ts": slack_thread_ts,
}
await webapp.set_reviewer_thread_metadata(
await common.set_reviewer_thread_metadata(
thread_id, pr=pr_meta, watch=True, slack_thread=slack_thread_meta, head_sha=head_sha
)
await webapp.post_review_started_comment(
await common.post_review_started_comment(
thread_id=thread_id,
owner=pr_ref.owner,
repo=pr_ref.repo,
@ -174,7 +173,7 @@ async def trigger_pr_review_from_ref(
)
prompt = build_github_pr_review_prompt(repo_config, pr_ref.number, pr_url, base_sha, head_sha)
configurable = webapp._build_reviewer_configurable(
configurable = common._build_reviewer_configurable(
source=source,
github_login=github_login,
github_user_id=github_user_id,
@ -189,19 +188,19 @@ async def trigger_pr_review_from_ref(
slack_thread_ts=slack_thread_ts,
)
webapp.logger.info(
common.logger.info(
"Dispatching reviewer run for thread %s from %s PR review request", thread_id, source
)
run = await webapp.dispatch_agent_run(
run = await common.dispatch_agent_run(
thread_id,
prompt,
configurable,
source=source,
assistant_id="reviewer",
metadata=webapp._AGENT_VERSION_METADATA,
metadata=common._AGENT_VERSION_METADATA,
client=langgraph_client,
)
await webapp._store_current_reviewer_run_id(thread_id, run)
await common._store_current_reviewer_run_id(thread_id, run)
return {"success": True, "queued": False, "thread_id": thread_id, "pr_url": pr_url}
@ -213,8 +212,8 @@ async def _dispatch_first_review_from_pr_payload(payload: dict[str, Any], *, sou
"owner": repo.get("owner", {}).get("login", ""),
"name": repo.get("name", ""),
}
repo_private = webapp._repo_private_from_payload(payload)
repo_id = webapp._repo_id_from_payload(payload)
repo_private = common._repo_private_from_payload(payload)
repo_id = common._repo_id_from_payload(payload)
pr_number = pull_request.get("number")
pr_url = pull_request.get("html_url", "") or pull_request.get("url", "")
branch_name = pull_request.get("head", {}).get("ref", "")
@ -226,10 +225,10 @@ async def _dispatch_first_review_from_pr_payload(payload: dict[str, Any], *, sou
github_user_id = payload.get("sender", {}).get("id")
if not pr_number or not pr_url or not base_sha or not head_sha:
webapp.logger.warning("Missing PR context for reviewer dispatch, skipping run")
common.logger.warning("Missing PR context for reviewer dispatch, skipping run")
return
thread_id = webapp.generate_reviewer_thread_id(
thread_id = common.generate_reviewer_thread_id(
repo_config.get("owner", ""), repo_config.get("name", ""), pr_number
)
@ -245,13 +244,13 @@ async def _dispatch_first_review_from_pr_payload(payload: dict[str, Any], *, sou
}
last_reviewed_sha = ""
if payload.get("action") == "ready_for_review":
metadata = await webapp._get_thread_metadata_safe(thread_id)
if metadata is not None and metadata.get("kind") == webapp.REVIEWER_THREAD_KIND:
metadata = await common._get_thread_metadata_safe(thread_id)
if metadata is not None and metadata.get("kind") == common.REVIEWER_THREAD_KIND:
existing_last_reviewed_sha = metadata.get("last_reviewed_sha")
if isinstance(existing_last_reviewed_sha, str) and existing_last_reviewed_sha:
if existing_last_reviewed_sha == head_sha:
await webapp.set_reviewer_thread_metadata(thread_id, pr=pr_meta, watch=True)
webapp.logger.info(
await common.set_reviewer_thread_metadata(thread_id, pr=pr_meta, watch=True)
common.logger.info(
"Skipping ready_for_review auto-review for %s/%s#%s: "
"head_sha unchanged from last_reviewed_sha",
repo_config.get("owner"),
@ -261,30 +260,30 @@ async def _dispatch_first_review_from_pr_payload(payload: dict[str, Any], *, sou
return
last_reviewed_sha = existing_last_reviewed_sha
app_token, app_token_expires_at = await webapp._reviewer_token_for_repo(
app_token, app_token_expires_at = await common._reviewer_token_for_repo(
repo_config,
repo_private=repo_private,
repo_id=repo_id,
)
if not app_token:
webapp.logger.warning("No GitHub App token available for reviewer dispatch")
common.logger.warning("No GitHub App token available for reviewer dispatch")
return
langgraph_client = webapp.get_client(url=webapp.LANGGRAPH_URL)
if not await webapp._ensure_thread_exists_for_metadata(thread_id, langgraph_client):
langgraph_client = common.get_client(url=common.LANGGRAPH_URL)
if not await common._ensure_thread_exists_for_metadata(thread_id, langgraph_client):
return
await webapp.set_reviewer_thread_metadata(thread_id, pr=pr_meta, watch=True, head_sha=head_sha)
await common.set_reviewer_thread_metadata(thread_id, pr=pr_meta, watch=True, head_sha=head_sha)
check_run_id = await webapp.create_review_check_run(
check_run_id = await common.create_review_check_run(
owner=repo_config.get("owner", ""),
repo=repo_config.get("name", ""),
head_sha=head_sha,
token=app_token,
details_url=webapp.dashboard_thread_url(thread_id),
details_url=common.dashboard_thread_url(thread_id),
)
if check_run_id is not None:
await webapp.set_reviewer_thread_metadata(
await common.set_reviewer_thread_metadata(
thread_id, extra={"review_check_run_id": check_run_id}
)
@ -297,7 +296,7 @@ async def _dispatch_first_review_from_pr_payload(payload: dict[str, Any], *, sou
)
else:
prompt = build_github_pr_review_prompt(repo_config, pr_number, pr_url, base_sha, head_sha)
configurable = webapp._build_reviewer_configurable(
configurable = common._build_reviewer_configurable(
source=source,
github_login=github_login,
github_user_id=github_user_id,
@ -312,18 +311,18 @@ async def _dispatch_first_review_from_pr_payload(payload: dict[str, Any], *, sou
last_reviewed_sha=last_reviewed_sha,
)
webapp.logger.info("Dispatching reviewer run for thread %s (source=%s)", thread_id, source)
run = await webapp.dispatch_agent_run(
common.logger.info("Dispatching reviewer run for thread %s (source=%s)", thread_id, source)
run = await common.dispatch_agent_run(
thread_id,
prompt,
configurable,
source=source,
assistant_id="reviewer",
metadata=webapp._AGENT_VERSION_METADATA,
metadata=common._AGENT_VERSION_METADATA,
client=langgraph_client,
)
await webapp._store_current_reviewer_run_id(thread_id, run)
webapp.logger.info("Reviewer run dispatched for thread %s (source=%s)", thread_id, source)
await common._store_current_reviewer_run_id(thread_id, run)
common.logger.info("Reviewer run dispatched for thread %s (source=%s)", thread_id, source)
async def process_github_pr_ready(payload: dict[str, Any]) -> None:
@ -337,8 +336,8 @@ async def process_github_pr_ready(payload: dict[str, Any]) -> None:
if is_draft:
author = pull_request.get("user") or {}
author_login = author.get("login", "") if isinstance(author, dict) else ""
if not await webapp._draft_review_enabled_for_author(author_login):
webapp.logger.info(
if not await common._draft_review_enabled_for_author(author_login):
common.logger.info(
"Skipping auto-review of draft PR by %s: review_draft_prs is disabled",
author_login or "<unknown>",
)
@ -367,13 +366,13 @@ async def process_github_pr_close(payload: dict[str, Any]) -> None:
if not pr_number or not isinstance(pr_number, int):
return
thread_id = webapp.generate_reviewer_thread_id(
thread_id = common.generate_reviewer_thread_id(
repo_config.get("owner", ""), repo_config.get("name", ""), pr_number
)
metadata = await webapp._get_thread_metadata_safe(thread_id)
if metadata is None or metadata.get("kind") != webapp.REVIEWER_THREAD_KIND:
metadata = await common._get_thread_metadata_safe(thread_id)
if metadata is None or metadata.get("kind") != common.REVIEWER_THREAD_KIND:
# No reviewer thread for this PR, nothing to do.
webapp.logger.debug(
common.logger.debug(
"PR %s/%s#%s closed/reopened: no reviewer thread, skipping watch update",
repo_config.get("owner"),
repo_config.get("name"),
@ -384,8 +383,8 @@ async def process_github_pr_close(payload: dict[str, Any]) -> None:
if action == "converted_to_draft":
author = pull_request.get("user") or {}
author_login = author.get("login", "") if isinstance(author, dict) else ""
if await webapp._draft_review_enabled_for_author(author_login):
webapp.logger.info(
if await common._draft_review_enabled_for_author(author_login):
common.logger.info(
"PR %s/%s#%s converted to draft but author %s has draft reviews enabled; keeping watch",
repo_config.get("owner"),
repo_config.get("name"),
@ -398,8 +397,8 @@ async def process_github_pr_close(payload: dict[str, Any]) -> None:
desired_watch = action == "reopened"
if metadata.get("watch") == desired_watch:
return
await webapp.set_reviewer_thread_metadata(thread_id, watch=desired_watch)
webapp.logger.info(
await common.set_reviewer_thread_metadata(thread_id, watch=desired_watch)
common.logger.info(
"Set watch=%s on reviewer thread %s after PR %s", desired_watch, thread_id, action
)
@ -409,10 +408,10 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
ref = payload.get("ref", "")
after_sha = payload.get("after", "")
if not ref.startswith("refs/heads/"):
webapp.logger.debug("Push ignored: ref %s is not a branch", ref)
common.logger.debug("Push ignored: ref %s is not a branch", ref)
return
if not isinstance(after_sha, str) or not after_sha or set(after_sha) == {"0"}:
webapp.logger.debug("Push to %s ignored: branch deletion or missing SHA", ref)
common.logger.debug("Push to %s ignored: branch deletion or missing SHA", ref)
return
head_ref = ref[len("refs/heads/") :]
@ -421,15 +420,15 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
"owner": repo.get("owner", {}).get("login", "") or repo.get("owner", {}).get("name", ""),
"name": repo.get("name", ""),
}
repo_private = webapp._repo_private_from_payload(payload)
repo_id = webapp._repo_id_from_payload(payload)
repo_private = common._repo_private_from_payload(payload)
repo_id = common._repo_id_from_payload(payload)
if not repo_config["owner"] or not repo_config["name"]:
webapp.logger.warning(
common.logger.warning(
"Push to %s ignored: repository owner/name missing from payload", head_ref
)
return
if not await webapp._is_repo_auto_review_enabled(repo_config):
webapp.logger.info(
if not await common._is_repo_auto_review_enabled(repo_config):
common.logger.info(
"Push to %s/%s head=%s ignored: automatic review disabled",
repo_config["owner"],
repo_config["name"],
@ -437,18 +436,18 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
)
return
app_token, app_token_expires_at = await webapp._reviewer_token_for_repo(
app_token, app_token_expires_at = await common._reviewer_token_for_repo(
repo_config,
repo_private=repo_private,
repo_id=repo_id,
)
if not app_token:
webapp.logger.warning("No GitHub App token for push re-review on %s", head_ref)
common.logger.warning("No GitHub App token for push re-review on %s", head_ref)
return
pr = await webapp._fetch_open_pr_for_branch(repo_config, head_ref, token=app_token)
pr = await common._fetch_open_pr_for_branch(repo_config, head_ref, token=app_token)
if not pr:
webapp.logger.debug(
common.logger.debug(
"No open PR found for push to %s/%s head=%s",
repo_config["owner"],
repo_config["name"],
@ -460,16 +459,16 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
# If the repo turns out public, re-scope the token so reviewer.py doesn't
# proxy a full-installation token for a public PR.
if repo_private is None:
repo_private = webapp._repo_private_from_pr_metadata(pr)
repo_id = repo_id or webapp._repo_id_from_pr_metadata(pr)
repo_private = common._repo_private_from_pr_metadata(pr)
repo_id = repo_id or common._repo_id_from_pr_metadata(pr)
if repo_private is False:
app_token, app_token_expires_at = await webapp._reviewer_token_for_repo(
app_token, app_token_expires_at = await common._reviewer_token_for_repo(
repo_config,
repo_private=repo_private,
repo_id=repo_id,
)
if not app_token:
webapp.logger.warning("No GitHub App token for push re-review on %s", head_ref)
common.logger.warning("No GitHub App token for push re-review on %s", head_ref)
return
pr_number = pr.get("number")
pr_url = pr.get("html_url") or pr.get("url") or ""
@ -478,7 +477,7 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
head_sha = pr.get("head", {}).get("sha", after_sha)
pr_title = pr.get("title", "")
if not isinstance(pr_number, int) or not base_sha or not head_sha:
webapp.logger.warning(
common.logger.warning(
"Push to %s/%s head=%s ignored: PR metadata missing number/base/head SHA",
repo_config["owner"],
repo_config["name"],
@ -486,12 +485,12 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
)
return
thread_id = webapp.generate_reviewer_thread_id(
thread_id = common.generate_reviewer_thread_id(
repo_config["owner"], repo_config["name"], pr_number
)
metadata = await webapp._get_thread_metadata_safe(thread_id)
if metadata is None or metadata.get("kind") != webapp.REVIEWER_THREAD_KIND:
webapp.logger.info(
metadata = await common._get_thread_metadata_safe(thread_id)
if metadata is None or metadata.get("kind") != common.REVIEWER_THREAD_KIND:
common.logger.info(
"Push to %s/%s#%s ignored: no reviewer thread for this PR. "
"Trigger a first review (Slack `@open-swe review <url>` or request "
"open-swe[bot] as a GitHub reviewer) to start watching.",
@ -501,21 +500,21 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
)
return
if not metadata.get("watch"):
webapp.logger.info(
common.logger.info(
"Push to %s ignored: reviewer thread %s is not watching", head_ref, thread_id
)
return
last_reviewed_sha = metadata.get("last_reviewed_sha")
if isinstance(last_reviewed_sha, str) and last_reviewed_sha == head_sha:
webapp.logger.info(
common.logger.info(
"Push to %s ignored: head_sha unchanged from last_reviewed_sha", head_ref
)
return
if (
isinstance(last_reviewed_sha, str)
and last_reviewed_sha
and await webapp._is_pr_diff_unchanged_since_last_review(
and await common._is_pr_diff_unchanged_since_last_review(
repo_config,
base_ref=base_ref,
last_reviewed_sha=last_reviewed_sha,
@ -523,19 +522,19 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
token=app_token,
)
):
await webapp.set_reviewer_thread_metadata(thread_id, last_reviewed_sha=head_sha)
await common.set_reviewer_thread_metadata(thread_id, last_reviewed_sha=head_sha)
# The old head's check disappears once the head moves (GitHub only
# shows checks on the current head), so even though no re-review runs,
# surface a settled check on the new head.
unchanged_check_id = await webapp.create_review_check_run(
unchanged_check_id = await common.create_review_check_run(
owner=repo_config["owner"],
repo=repo_config["name"],
head_sha=head_sha,
token=app_token,
details_url=webapp.dashboard_thread_url(thread_id),
details_url=common.dashboard_thread_url(thread_id),
)
if unchanged_check_id is not None:
await webapp.complete_review_check_run(
await common.complete_review_check_run(
owner=repo_config["owner"],
repo=repo_config["name"],
check_run_id=unchanged_check_id,
@ -547,26 +546,26 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
f"commit {last_reviewed_sha}."
),
)
webapp.logger.info(
common.logger.info(
"Push to %s ignored: PR diff unchanged since last reviewed SHA %s",
head_ref,
last_reviewed_sha,
)
return
langgraph_client = webapp.get_client(url=webapp.LANGGRAPH_URL)
if not await webapp._ensure_thread_exists_for_metadata(thread_id, langgraph_client):
langgraph_client = common.get_client(url=common.LANGGRAPH_URL)
if not await common._ensure_thread_exists_for_metadata(thread_id, langgraph_client):
return
try:
threads = await webapp.fetch_pr_review_threads(
threads = await common.fetch_pr_review_threads(
owner=repo_config["owner"],
repo=repo_config["name"],
pr_number=pr_number,
token=app_token,
)
await webapp.reconcile_findings_with_review_threads(thread_id, threads)
await common.reconcile_findings_with_review_threads(thread_id, threads)
except Exception:
webapp.logger.warning(
common.logger.warning(
"Could not sync review threads before push re-review for %s", thread_id
)
@ -580,21 +579,21 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
"base_ref": base_ref,
"author": (pr.get("user") or {}).get("login", ""),
}
await webapp.set_reviewer_thread_metadata(thread_id, pr=pr_meta, watch=True, head_sha=head_sha)
await common.set_reviewer_thread_metadata(thread_id, pr=pr_meta, watch=True, head_sha=head_sha)
# GitHub only shows check runs on a PR's current head commit, so the check
# created on the previous head disappears after a follow-up push. Create a
# fresh in-progress check on the new head SHA so the review stays visible;
# publish (or the after-agent hook) settles this id.
check_run_id = await webapp.create_review_check_run(
check_run_id = await common.create_review_check_run(
owner=repo_config["owner"],
repo=repo_config["name"],
head_sha=head_sha,
token=app_token,
details_url=webapp.dashboard_thread_url(thread_id),
details_url=common.dashboard_thread_url(thread_id),
)
if check_run_id is not None:
await webapp.set_reviewer_thread_metadata(
await common.set_reviewer_thread_metadata(
thread_id, extra={"review_check_run_id": check_run_id}
)
@ -603,7 +602,7 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
f"{head_sha}. Reconcile existing findings against the new diff, add any "
f"net-new findings, and call `publish_review` once you're done."
)
configurable = webapp._build_reviewer_configurable(
configurable = common._build_reviewer_configurable(
source="github_push",
github_login=payload.get("sender", {}).get("login", "") or "",
github_user_id=payload.get("sender", {}).get("id"),
@ -618,17 +617,17 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
last_reviewed_sha=last_reviewed_sha if isinstance(last_reviewed_sha, str) else "",
)
webapp.logger.info("Dispatching push re-review run for thread %s", thread_id)
run = await webapp.dispatch_agent_run(
common.logger.info("Dispatching push re-review run for thread %s", thread_id)
run = await common.dispatch_agent_run(
thread_id,
re_review_prompt,
configurable,
source="github_push",
assistant_id="reviewer",
metadata=webapp._AGENT_VERSION_METADATA,
metadata=common._AGENT_VERSION_METADATA,
client=langgraph_client,
)
await webapp._store_current_reviewer_run_id(thread_id, run)
await common._store_current_reviewer_run_id(thread_id, run)
async def process_github_pr_comment(payload: dict[str, Any], event_type: str) -> None:
@ -650,20 +649,20 @@ async def process_github_pr_comment(payload: dict[str, Any], event_type: str) ->
pr_url,
comment_id,
node_id,
) = await webapp.extract_pr_context(payload, event_type)
) = await common.extract_pr_context(payload, event_type)
github_user_id = payload.get("sender", {}).get("id")
webapp.logger.info(
common.logger.info(
"Processing GitHub PR comment: event=%s, pr=%s, branch=%s",
event_type,
pr_number,
branch_name,
)
thread_id = webapp.get_thread_id_from_branch(branch_name) if branch_name else None
thread_id = common.get_thread_id_from_branch(branch_name) if branch_name else None
if not thread_id:
if not pr_number:
webapp.logger.warning(
common.logger.warning(
"Could not determine thread_id for branch '%s' (no pr_number), skipping",
branch_name,
)
@ -672,21 +671,21 @@ async def process_github_pr_comment(payload: dict[str, Any], event_type: str) ->
name = repo_config.get("name", "")
stable_key = f"{owner}/{name}/pr/{pr_number}"
thread_id = str(uuid.uuid5(uuid.NAMESPACE_URL, stable_key))
webapp.logger.info(
common.logger.info(
"Generated thread_id %s for non-open-swe branch '%s'", thread_id, branch_name
)
langgraph_client = webapp.get_client(url=webapp.LANGGRAPH_URL)
langgraph_client = common.get_client(url=common.LANGGRAPH_URL)
try:
await langgraph_client.threads.update(thread_id, metadata={"branch_name": branch_name})
except Exception as exc: # noqa: BLE001
if webapp._is_not_found_error(exc):
if common._is_not_found_error(exc):
await langgraph_client.threads.create(
thread_id=thread_id,
if_exists="do_nothing",
metadata={"branch_name": branch_name},
)
else:
webapp.logger.warning(
common.logger.warning(
"Failed to persist branch_name metadata for thread %s", thread_id
)
@ -696,28 +695,28 @@ async def process_github_pr_comment(payload: dict[str, Any], event_type: str) ->
# otherwise visible), which would drop a legitimately-mapped user. Mirrors
# the Slack mention path (process_slack_mention).
try:
await webapp.refresh_user_mapping_cache()
await common.refresh_user_mapping_cache()
except Exception: # noqa: BLE001
webapp.logger.debug(
common.logger.debug(
"Could not refresh user mapping cache for GitHub PR comment", exc_info=True
)
email = await webapp.email_for_login(github_login) or ""
email = await common.email_for_login(github_login) or ""
if email:
github_token = await webapp._get_or_resolve_thread_github_token(
github_token = await common._get_or_resolve_thread_github_token(
thread_id, email, repo=repo_config
)
else:
webapp.logger.warning("No email mapping for GitHub user '%s', skipping", github_login)
common.logger.warning("No email mapping for GitHub user '%s', skipping", github_login)
return
if not github_token:
webapp.logger.warning("No GitHub token for thread %s, skipping", thread_id)
common.logger.warning("No GitHub token for thread %s, skipping", thread_id)
return
if comment_id:
try:
await webapp.react_to_github_comment(
await common.react_to_github_comment(
repo_config,
comment_id,
event_type=event_type,
@ -726,13 +725,13 @@ async def process_github_pr_comment(payload: dict[str, Any], event_type: str) ->
node_id=node_id,
)
except GitHubAuthError:
github_token = await webapp._refresh_thread_github_token_after_401(
github_token = await common._refresh_thread_github_token_after_401(
thread_id, email, repo=repo_config
)
if not github_token:
webapp.logger.warning("Re-auth failed for thread %s after 401; skipping", thread_id)
common.logger.warning("Re-auth failed for thread %s after 401; skipping", thread_id)
return
await webapp.react_to_github_comment(
await common.react_to_github_comment(
repo_config,
comment_id,
event_type=event_type,
@ -742,29 +741,29 @@ async def process_github_pr_comment(payload: dict[str, Any], event_type: str) ->
)
if not pr_number:
webapp.logger.warning("No PR number found in payload, skipping")
common.logger.warning("No PR number found in payload, skipping")
return
try:
comments = await webapp.fetch_pr_comments_since_last_tag(
comments = await common.fetch_pr_comments_since_last_tag(
repo_config, pr_number, token=github_token
)
except GitHubAuthError:
github_token = await webapp._refresh_thread_github_token_after_401(
github_token = await common._refresh_thread_github_token_after_401(
thread_id, email, repo=repo_config
)
if not github_token:
webapp.logger.warning("Re-auth failed for thread %s after 401; skipping", thread_id)
common.logger.warning("Re-auth failed for thread %s after 401; skipping", thread_id)
return
comments = await webapp.fetch_pr_comments_since_last_tag(
comments = await common.fetch_pr_comments_since_last_tag(
repo_config, pr_number, token=github_token
)
if not comments:
webapp.logger.info("No comments found since last @open-swe tag for PR %s", pr_number)
common.logger.info("No comments found since last @open-swe tag for PR %s", pr_number)
return
prompt = webapp.build_pr_prompt(comments, pr_url, repo_config=repo_config)
await webapp._trigger_or_queue_run(
prompt = common.build_pr_prompt(comments, pr_url, repo_config=repo_config)
await common._trigger_or_queue_run(
thread_id,
prompt,
github_login=github_login,
@ -776,13 +775,13 @@ async def process_github_pr_comment(payload: dict[str, Any], event_type: str) ->
async def process_github_review_finding_reply(payload: dict[str, Any]) -> None:
"""Route replies to Open SWE review comments back to the reviewer graph."""
parent_comment_id = webapp._review_comment_reply_parent_id(payload)
parent_comment_id = common._review_comment_reply_parent_id(payload)
if parent_comment_id is None:
return
sender = payload.get("sender", {})
sender_login = sender.get("login") if isinstance(sender, dict) else None
if sender_login in webapp.INTERNAL_BOT_LOGINS:
if sender_login in common.INTERNAL_BOT_LOGINS:
return
repo = payload.get("repository", {})
@ -791,20 +790,20 @@ async def process_github_review_finding_reply(payload: dict[str, Any]) -> None:
"owner": repo.get("owner", {}).get("login", ""),
"name": repo.get("name", ""),
}
repo_private = webapp._repo_private_from_payload(payload)
repo_id = webapp._repo_id_from_payload(payload)
repo_private = common._repo_private_from_payload(payload)
repo_id = common._repo_id_from_payload(payload)
pr_number = pull_request.get("number")
if not isinstance(pr_number, int):
return
thread_id = webapp.generate_reviewer_thread_id(
thread_id = common.generate_reviewer_thread_id(
repo_config.get("owner", ""), repo_config.get("name", ""), pr_number
)
metadata = await webapp._get_thread_metadata_safe(thread_id)
if metadata is None or metadata.get("kind") != webapp.REVIEWER_THREAD_KIND:
metadata = await common._get_thread_metadata_safe(thread_id)
if metadata is None or metadata.get("kind") != common.REVIEWER_THREAD_KIND:
return
app_token, app_token_expires_at = await webapp._reviewer_token_for_repo(
app_token, app_token_expires_at = await common._reviewer_token_for_repo(
repo_config,
repo_private=repo_private,
repo_id=repo_id,
@ -812,16 +811,16 @@ async def process_github_review_finding_reply(payload: dict[str, Any]) -> None:
if not app_token:
return
threads = await webapp.fetch_pr_review_threads(
threads = await common.fetch_pr_review_threads(
owner=repo_config["owner"],
repo=repo_config["name"],
pr_number=pr_number,
token=app_token,
)
await webapp.reconcile_findings_with_review_threads(thread_id, threads)
findings = await webapp.list_reviewer_findings(thread_id)
await common.reconcile_findings_with_review_threads(thread_id, threads)
findings = await common.list_reviewer_findings(thread_id)
finding = next(
(item for item in findings if parent_comment_id in webapp._finding_comment_ids(item)), None
(item for item in findings if parent_comment_id in common._finding_comment_ids(item)), None
)
if finding is None:
return
@ -846,13 +845,13 @@ async def process_github_review_finding_reply(payload: dict[str, Any]) -> None:
else "",
"needs_reassessment": True,
}
await webapp.append_finding_interaction(thread_id, finding_id, interaction)
await common.append_finding_interaction(thread_id, finding_id, interaction)
base_sha = pull_request.get("base", {}).get("sha", "")
head_sha = pull_request.get("head", {}).get("sha", "")
pr_url = pull_request.get("html_url", "") or pull_request.get("url", "")
branch_name = pull_request.get("head", {}).get("ref", "")
configurable = webapp._build_reviewer_configurable(
configurable = common._build_reviewer_configurable(
source="github_review_comment",
github_login=reply_author,
github_user_id=sender.get("id") if isinstance(sender, dict) else None,
@ -873,23 +872,23 @@ async def process_github_review_finding_reply(payload: dict[str, Any]) -> None:
"finding_reply_body": reply_body,
}
)
finding_reply_prompt = webapp._build_queued_finding_reply_prompt(
finding_reply_prompt = common._build_queued_finding_reply_prompt(
finding_id=finding_id,
reply_author=reply_author,
reply_body=reply_body,
pr_number=pr_number,
)
langgraph_client = webapp.get_client(url=webapp.LANGGRAPH_URL)
run = await webapp.dispatch_agent_run(
langgraph_client = common.get_client(url=common.LANGGRAPH_URL)
run = await common.dispatch_agent_run(
thread_id,
finding_reply_prompt,
configurable,
source="github_review_reply",
assistant_id="reviewer",
metadata=webapp._AGENT_VERSION_METADATA,
metadata=common._AGENT_VERSION_METADATA,
client=langgraph_client,
)
await webapp._store_current_reviewer_run_id(thread_id, run)
await common._store_current_reviewer_run_id(thread_id, run)
async def process_github_issue(payload: dict[str, Any], event_type: str) -> None:
@ -910,7 +909,7 @@ async def process_github_issue(payload: dict[str, Any], event_type: str) -> None
description = issue.get("body") or "No description"
issue_author = issue.get("user", {}).get("login", "")
webapp.logger.info(
common.logger.info(
"Processing GitHub issue: event=%s, issue=%s, repo=%s/%s",
event_type,
issue_number,
@ -919,66 +918,66 @@ async def process_github_issue(payload: dict[str, Any], event_type: str) -> None
)
if not issue_id or not issue_number:
webapp.logger.warning("Missing GitHub issue id/number, skipping")
common.logger.warning("Missing GitHub issue id/number, skipping")
return
# Refresh the per-process user-mapping cache from the Store before
# resolving the author's email (multi-replica staleness; mirrors the Slack
# mention path in process_slack_mention).
try:
await webapp.refresh_user_mapping_cache()
await common.refresh_user_mapping_cache()
except Exception: # noqa: BLE001
webapp.logger.debug("Could not refresh user mapping cache for GitHub issue", exc_info=True)
common.logger.debug("Could not refresh user mapping cache for GitHub issue", exc_info=True)
email = await webapp.email_for_login(github_login) or ""
email = await common.email_for_login(github_login) or ""
if not email:
webapp.logger.warning("No email mapping for GitHub user '%s', skipping", github_login)
common.logger.warning("No email mapping for GitHub user '%s', skipping", github_login)
return
thread_id = webapp.generate_thread_id_from_github_issue(issue_id)
existing_thread = await webapp._thread_exists(thread_id)
github_token = await webapp._get_or_resolve_thread_github_token(
thread_id = common.generate_thread_id_from_github_issue(issue_id)
existing_thread = await common._thread_exists(thread_id)
github_token = await common._get_or_resolve_thread_github_token(
thread_id, email, repo=repo_config
)
app_token = await webapp.get_github_app_installation_token()
app_token = await common.get_github_app_installation_token()
reaction_token = github_token or app_token
comment = payload.get("comment", {})
comment_id = comment.get("id")
if event_type == "issue_comment" and comment_id:
if not reaction_token:
webapp.logger.warning(
common.logger.warning(
"No GitHub token available to react to issue comment %s", comment_id
)
else:
try:
reacted = await webapp.react_to_github_comment(
reacted = await common.react_to_github_comment(
repo_config,
comment_id,
event_type="issue_comment",
token=reaction_token,
)
except GitHubAuthError:
github_token = await webapp._refresh_thread_github_token_after_401(
github_token = await common._refresh_thread_github_token_after_401(
thread_id, email, repo=repo_config
)
reaction_token = github_token or app_token
reacted = False
if reaction_token:
try:
reacted = await webapp.react_to_github_comment(
reacted = await common.react_to_github_comment(
repo_config,
comment_id,
event_type="issue_comment",
token=reaction_token,
)
except GitHubAuthError:
webapp.logger.warning(
common.logger.warning(
"Re-auth still produced 401 reacting to issue comment %s",
comment_id,
)
reacted = False
if not reacted:
webapp.logger.warning("Failed to react to GitHub issue comment %s", comment_id)
common.logger.warning("Failed to react to GitHub issue comment %s", comment_id)
if existing_thread:
if event_type == "issue_comment":
@ -990,14 +989,14 @@ async def process_github_issue(payload: dict[str, Any], event_type: str) -> None
prompt = build_github_issue_update_prompt(github_login, title, description)
else:
try:
comments = await webapp.fetch_issue_comments(
comments = await common.fetch_issue_comments(
repo_config, issue_number, token=github_token or app_token
)
except GitHubAuthError:
github_token = await webapp._refresh_thread_github_token_after_401(
github_token = await common._refresh_thread_github_token_after_401(
thread_id, email, repo=repo_config
)
comments = await webapp.fetch_issue_comments(
comments = await common.fetch_issue_comments(
repo_config, issue_number, token=github_token or app_token
)
if comment_id and not any(item.get("comment_id") == comment_id for item in comments):
@ -1035,7 +1034,7 @@ async def process_github_issue(payload: dict[str, Any], event_type: str) -> None
},
}
await webapp.upsert_agent_thread_owner_metadata(
await common.upsert_agent_thread_owner_metadata(
thread_id,
source="github",
repo_config=repo_config,
@ -1044,17 +1043,17 @@ async def process_github_issue(payload: dict[str, Any], event_type: str) -> None
source_context={"github_issue": configurable["github_issue"]},
)
webapp.logger.info("Dispatching LangGraph run for thread %s from GitHub issue", thread_id)
langgraph_client = webapp.get_client(url=webapp.LANGGRAPH_URL)
await webapp.dispatch_agent_run(
common.logger.info("Dispatching LangGraph run for thread %s from GitHub issue", thread_id)
langgraph_client = common.get_client(url=common.LANGGRAPH_URL)
await common.dispatch_agent_run(
thread_id,
prompt,
configurable,
source="github_issue",
metadata=webapp._AGENT_VERSION_METADATA,
metadata=common._AGENT_VERSION_METADATA,
client=langgraph_client,
)
webapp.logger.info("LangGraph run dispatched for thread %s from GitHub issue", thread_id)
common.logger.info("LangGraph run dispatched for thread %s from GitHub issue", thread_id)
# ---- CI auto-fix handlers (re-wired onto the modular dispatch model) ----
@ -1081,7 +1080,7 @@ async def process_github_ci_event(payload: dict[str, Any], event_type: str) -> N
head_sha=head_sha,
source="github_ci",
)
webapp.logger.info(
common.logger.info(
"CI auto-fix for %s/%s@%s (%s): %s",
repo_config["owner"],
repo_config["name"],
@ -1100,7 +1099,7 @@ def _parse_autofix_command(comment_body: str) -> bool | None:
Returns ``None`` when the comment isn't an auto-fix command. Requires an
Open SWE mention so a passing reference to "autofix off" doesn't toggle it.
"""
if not any(tag in comment_body.lower() for tag in webapp.OPEN_SWE_TAGS):
if not any(tag in comment_body.lower() for tag in common.OPEN_SWE_TAGS):
return None
match = _AUTOFIX_COMMAND_RE.search(comment_body)
if not match:
@ -1135,7 +1134,7 @@ async def process_github_autofix_command(
if ref is None:
return
await set_pr_autofix_disabled(ref["owner"], ref["name"], ref["number"], disabled)
webapp.logger.info(
common.logger.info(
"Auto-fix %s for %s/%s#%s via comment",
"disabled" if disabled else "enabled",
ref["owner"],
@ -1146,11 +1145,11 @@ async def process_github_autofix_command(
comment_id = comment.get("id")
if not isinstance(comment_id, int):
return
token = await webapp.get_github_app_installation_token()
token = await common.get_github_app_installation_token()
if not token:
return
try:
await webapp.react_to_github_comment(
await common.react_to_github_comment(
{"owner": ref["owner"], "name": ref["name"]},
comment_id,
event_type=event_type,
@ -1159,7 +1158,7 @@ async def process_github_autofix_command(
node_id=comment.get("node_id"),
)
except Exception: # noqa: BLE001
webapp.logger.debug("Failed to react to auto-fix command comment", exc_info=True)
common.logger.debug("Failed to react to auto-fix command comment", exc_info=True)
# GitHub author_association values that imply at least repo-member trust. Used
@ -1192,7 +1191,7 @@ def _is_actionable_review_payload(payload: dict[str, Any], event_type: str) -> b
if not isinstance(node, dict):
return False
reviewer = (node.get("user") or {}).get("login", "")
if reviewer in webapp.INTERNAL_BOT_LOGINS:
if reviewer in common.INTERNAL_BOT_LOGINS:
return False
if node.get("author_association") not in _TRUSTED_REVIEW_ASSOCIATIONS:
return False
@ -1208,7 +1207,7 @@ async def process_github_autofix_review(payload: dict[str, Any], event_type: str
comment = payload.get("comment") or payload.get("review", {})
reviewer = (comment.get("user") or {}).get("login", "") if isinstance(comment, dict) else ""
body = (comment.get("body") or "") if isinstance(comment, dict) else ""
if not body.strip() or reviewer in webapp.INTERNAL_BOT_LOGINS:
if not body.strip() or reviewer in common.INTERNAL_BOT_LOGINS:
return
result = await handle_review_feedback(
repo_config={"owner": ref["owner"], "name": ref["name"]},
@ -1218,7 +1217,7 @@ async def process_github_autofix_review(payload: dict[str, Any], event_type: str
body=body,
source="github_review",
)
webapp.logger.info(
common.logger.info(
"Auto-fix review feedback for %s/%s#%s: %s",
ref["owner"],
ref["name"],

View file

@ -0,0 +1,198 @@
"""Github webhook HTTP routes."""
from fastapi import APIRouter
from . import common
from . import github as service
router = APIRouter()
@router.post("/webhooks/github")
async def github_webhook(
request: common.Request, background_tasks: common.BackgroundTasks
) -> dict[str, str]:
"""Handle GitHub webhooks for issue and PR events that tag @open-swe."""
body = await request.body()
signature = request.headers.get("X-Hub-Signature-256", "")
if not common.verify_github_signature(body, signature, secret=common.GITHUB_WEBHOOK_SECRET):
common.logger.warning("Invalid GitHub webhook signature")
raise common.HTTPException(status_code=401, detail="Invalid signature")
event_type = request.headers.get("X-GitHub-Event", "")
if event_type not in common._SUPPORTED_GH_EVENTS:
common.logger.info("Ignoring unsupported GitHub event type: %s", event_type)
return {"status": "ignored", "reason": f"Unsupported event type: {event_type}"}
try:
payload = common.json.loads(body)
except common.json.JSONDecodeError:
common.logger.exception("Failed to parse GitHub webhook JSON")
return {"status": "error", "message": "Invalid JSON"}
webhook_repo = payload.get("repository", {})
webhook_repo_config = {
"owner": webhook_repo.get("owner", {}).get("login", ""),
"name": webhook_repo.get("name", ""),
}
issue = payload.get("issue", {})
is_pull_request_comment = bool(event_type == "issue_comment" and issue.get("pull_request"))
is_issue_comment = bool(event_type == "issue_comment" and not issue.get("pull_request"))
is_issue_event = event_type == "issues"
is_pull_request_event = event_type == "pull_request"
if is_pull_request_event:
action = payload.get("action", "")
if action not in common._SUPPORTED_GH_PULL_REQUEST_ACTIONS:
common.logger.info("Ignoring unsupported GitHub pull_request action: %s", action)
return {
"status": "ignored",
"reason": f"Unsupported GitHub pull_request action: {action}",
}
if action in common._GH_PR_AGENT_STATE_ACTIONS:
background_tasks.add_task(common.update_agent_thread_pr_state, payload)
if action in common._GH_PR_WATCH_TOGGLE_ACTIONS:
common.logger.info(
"Accepted GitHub PR %s webhook, scheduling reviewer watch update", action
)
background_tasks.add_task(service.process_github_pr_close, payload)
return {"status": "accepted", "message": f"Processing PR {action} for reviewer watch"}
if action in common._GH_PR_FIRST_REVIEW_ACTIONS:
if not await common._is_repo_auto_review_enabled(webhook_repo_config):
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
gate_rejection = await common._enforce_public_repo_org_gate(payload, "pull_request")
if gate_rejection is not None:
return gate_rejection
common.logger.info("Accepted GitHub PR %s webhook, scheduling auto-review task", action)
background_tasks.add_task(service.process_github_pr_ready, payload)
return {"status": "accepted", "message": f"Processing PR {action} for auto-review"}
common.logger.info("Ignoring unsupported GitHub pull_request action: %s", action)
return {
"status": "ignored",
"reason": f"Unsupported GitHub pull_request action: {action}",
}
if event_type == "push":
if not await common._is_repo_auto_review_enabled(webhook_repo_config):
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
common.logger.info("Accepted GitHub push webhook, scheduling reviewer watch evaluation")
background_tasks.add_task(service.process_github_push_event, payload)
return {"status": "accepted", "message": "Processing GitHub push for reviewer watch"}
if event_type in common._GH_CI_EVENTS:
if not common.is_failing_ci_payload(payload, event_type):
return {"status": "ignored", "reason": "CI event is not a completed failure"}
if not await common._is_repo_auto_review_enabled(webhook_repo_config):
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
common.logger.info(
"Accepted GitHub %s webhook, scheduling CI auto-fix evaluation", event_type
)
background_tasks.add_task(service.process_github_ci_event, payload, event_type)
return {"status": "accepted", "message": f"Processing GitHub {event_type} for auto-fix"}
if not common._is_repo_allowed(webhook_repo_config):
common.logger.debug(
"Rejecting GitHub webhook: repo '%s/%s' not in allowlist",
webhook_repo_config.get("owner"),
webhook_repo_config.get("name"),
)
return {"status": "ignored", "reason": "Repository not in allowlist"}
if is_issue_event:
action = payload.get("action", "")
if action not in common._SUPPORTED_GH_ISSUE_ACTIONS:
common.logger.info("Ignoring unsupported GitHub issue action: %s", action)
return {"status": "ignored", "reason": f"Unsupported GitHub issue action: {action}"}
if action == "edited":
changes = payload.get("changes", {})
if not any(field in changes for field in ("body", "title")):
common.logger.info("Ignoring GitHub issue edit without title/body changes")
return {"status": "ignored", "reason": "Issue edit did not change title or body"}
issue_text = f"{issue.get('title', '')}\n\n{issue.get('body', '')}".lower()
if not any(tag in issue_text for tag in common.OPEN_SWE_TAGS):
common.logger.info("Ignoring issue that does not mention @openswe or @open-swe")
return {"status": "ignored", "reason": "Issue does not mention @openswe or @open-swe"}
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
if gate_rejection is not None:
return gate_rejection
common.logger.info("Accepted GitHub issue webhook, scheduling background task")
background_tasks.add_task(service.process_github_issue, payload, event_type)
return {"status": "accepted", "message": "Processing GitHub issue event"}
action = payload.get("action", "")
supported_comment_actions = common._SUPPORTED_GH_COMMENT_ACTIONS.get(event_type)
if supported_comment_actions is None:
common.logger.info("Ignoring unsupported GitHub payload shape for event=%s", event_type)
return {"status": "ignored", "reason": f"Unsupported payload for event type: {event_type}"}
if action and action not in supported_comment_actions:
common.logger.debug("Ignoring unsupported GitHub %s action: %s", event_type, action)
return {"status": "ignored", "reason": f"Unsupported GitHub {event_type} action: {action}"}
comment = payload.get("comment") or payload.get("review", {})
comment_body = (comment.get("body") or "") if comment else ""
is_pr_related_comment = is_pull_request_comment or event_type in {
"pull_request_review_comment",
"pull_request_review",
}
autofix_command = service._parse_autofix_command(comment_body)
if autofix_command is not None and is_pr_related_comment:
if not await common._is_repo_auto_review_enabled(webhook_repo_config):
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
if gate_rejection is not None:
return gate_rejection
background_tasks.add_task(
service.process_github_autofix_command, payload, event_type, disabled=autofix_command
)
return {"status": "accepted", "message": "Processing auto-fix toggle"}
if (
event_type == "pull_request_review_comment"
and common._review_comment_reply_parent_id(payload) is not None
):
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
if gate_rejection is not None:
return gate_rejection
background_tasks.add_task(service.process_github_review_finding_reply, payload)
return {"status": "accepted", "message": "Processing review finding reply"}
if not any(tag in comment_body.lower() for tag in common.OPEN_SWE_TAGS):
if service._is_actionable_review_payload(
payload, event_type
) and await common._is_repo_auto_review_enabled(webhook_repo_config):
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
if gate_rejection is not None:
return gate_rejection
background_tasks.add_task(service.process_github_autofix_review, payload, event_type)
return {"status": "accepted", "message": "Processing auto-fix review feedback"}
common.logger.debug(
"Ignoring GitHub %s%s that does not mention @openswe or @open-swe",
event_type,
f" action={action}" if action else "",
)
return {"status": "ignored", "reason": "Comment does not mention @openswe or @open-swe"}
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
if gate_rejection is not None:
return gate_rejection
common.logger.info("Accepted GitHub webhook: event=%s, scheduling background task", event_type)
if is_pull_request_comment or event_type in {
"pull_request_review_comment",
"pull_request_review",
}:
background_tasks.add_task(service.process_github_pr_comment, payload, event_type)
return {"status": "accepted", "message": f"Processing {event_type} event"}
if is_issue_comment:
background_tasks.add_task(service.process_github_issue, payload, event_type)
return {"status": "accepted", "message": "Processing GitHub issue comment event"}
common.logger.info("Ignoring unsupported GitHub payload shape for event=%s", event_type)
return {"status": "ignored", "reason": f"Unsupported payload for event type: {event_type}"}

View file

@ -1,7 +1,7 @@
"""Jira webhook handler — mirrors ``agent/webhooks/linear.py`` for Jira issues.
Helpers and constants stay in webapp.py; they are accessed through the module
object (``webapp.X``) so tests that monkeypatch them keep working.
Helpers and constants stay in common.py; they are accessed through the module
object (``common.X``) so tests that monkeypatch them keep working.
"""
from typing import Any
@ -10,7 +10,7 @@ from urllib.parse import urlparse
import httpx
from langchain_core.messages.content import create_text_block
from agent import webapp
from . import common
async def process_jira_issue( # noqa: PLR0912, PLR0915
@ -20,20 +20,20 @@ async def process_jira_issue( # noqa: PLR0912, PLR0915
Args:
issue_data: The Jira issue data from the webhook (basic info + the
triggering comment; see ``webapp.jira_webhook`` for the shape).
triggering comment; see ``jira_routes.jira_webhook`` for the shape).
repo_config: The repo configuration with owner and name.
"""
issue_key = issue_data.get("key", "")
webapp.logger.info(
common.logger.info(
"Processing Jira issue %s for repo %s/%s",
issue_key,
repo_config.get("owner"),
repo_config.get("name"),
)
thread_id = webapp.generate_thread_id_from_jira_issue(issue_key)
thread_id = common.generate_thread_id_from_jira_issue(issue_key)
full_issue = await webapp.fetch_jira_issue_details(issue_key)
full_issue = await common.fetch_jira_issue_details(issue_key)
if not full_issue:
full_issue = {}
@ -46,26 +46,26 @@ async def process_jira_issue( # noqa: PLR0912, PLR0915
user_name = comment_author.get("name") or None
user_email = actor_email
webapp.logger.info("User email for issue %s: %s", issue_key, user_email)
common.logger.info("User email for issue %s: %s", issue_key, user_email)
title = full_issue.get("title") or "No title"
description = full_issue.get("description") or "No description"
image_urls: list[str] = []
description_image_urls = webapp.extract_image_urls(description)
description_image_urls = common.extract_image_urls(description)
if description_image_urls:
image_urls.extend(description_image_urls)
webapp.logger.debug(
common.logger.debug(
"Found %d image URL(s) in issue description",
len(description_image_urls),
)
raw_comments = await webapp.fetch_jira_issue_comments(issue_key)
raw_comments = await common.fetch_jira_issue_comments(issue_key)
comments = [{**comment, "createdAt": comment.get("created", "")} for comment in raw_comments]
comments_text = ""
triggering_comment = issue_data.get("triggering_comment", "")
triggering_comment_id = issue_data.get("triggering_comment_id", "")
bot_message_prefixes = webapp._GITHUB_BOT_MESSAGE_PREFIXES
bot_message_prefixes = common._GITHUB_BOT_MESSAGE_PREFIXES
comment_ids: set[str] = set()
comment_id_to_index: dict[str, int] = {}
@ -82,22 +82,22 @@ async def process_jira_issue( # noqa: PLR0912, PLR0915
trigger_index = comment_id_to_index.get(triggering_comment_id)
if trigger_index is not None:
relevant_comments = comments[trigger_index:]
webapp.logger.debug(
common.logger.debug(
"Using triggering comment index %d to build relevant comments",
trigger_index,
)
else:
relevant_comments = webapp.get_recent_comments(comments, bot_message_prefixes)
relevant_comments = common.get_recent_comments(comments, bot_message_prefixes)
if relevant_comments:
comments_text = "\n\n## Comments:\n"
for comment in relevant_comments:
author = (comment.get("author") or {}).get("name") or "User"
body = comment.get("body", "")
body_image_urls = webapp.extract_image_urls(body)
body_image_urls = common.extract_image_urls(body)
if body_image_urls:
image_urls.extend(body_image_urls)
webapp.logger.debug(
common.logger.debug(
"Found %d image URL(s) in comment by %s",
len(body_image_urls),
author,
@ -111,16 +111,16 @@ async def process_jira_issue( # noqa: PLR0912, PLR0915
comments_text = "\n\n## Comments:\n"
trigger_author = comment_author.get("name") or "Unknown"
trigger_body = triggering_comment
trigger_image_urls = webapp.extract_image_urls(trigger_body)
trigger_image_urls = common.extract_image_urls(trigger_body)
if trigger_image_urls:
image_urls.extend(trigger_image_urls)
webapp.logger.debug(
common.logger.debug(
"Found %d image URL(s) in triggering comment by %s",
len(trigger_image_urls),
trigger_author,
)
comments_text += f"\n**{trigger_author}:** {trigger_body}\n"
webapp.logger.debug(
common.logger.debug(
"Appended triggering comment %s not present in issue comments list",
triggering_comment_id or "<missing-id>",
)
@ -152,22 +152,22 @@ async def process_jira_issue( # noqa: PLR0912, PLR0915
# user-mapping store Slack/Linear use, so PRs open *as the triggering user*
# and the thread is tagged for the dashboard. Restricted to the comment
# author so token attribution never falls back to reporter/assignee.
mapped_login = await webapp.resolve_login_from_email_async(actor_email) if actor_email else None
mapped_login = await common.resolve_login_from_email_async(actor_email) if actor_email else None
# Only attribute to an *active* user mapping; a pending/unconfirmed mapping
# must never drive PR authorship or token resolution.
if mapped_login and not webapp.is_login_mapped(mapped_login):
webapp.logger.info(
if mapped_login and not common.is_login_mapped(mapped_login):
common.logger.info(
"Jira actor login %s is not an active mapping; running unattributed", mapped_login
)
mapped_login = None
image_model_override: tuple[str, str] | None = None
if image_urls:
image_urls = webapp.dedupe_urls(image_urls)
resolved_model_id = await webapp.resolve_agent_model_id(mapped_login)
if not webapp.model_supports_images(resolved_model_id):
fallback_model_id, fallback_effort = webapp.default_vision_model_pair()
webapp.logger.info(
image_urls = common.dedupe_urls(image_urls)
resolved_model_id = await common.resolve_agent_model_id(mapped_login)
if not common.model_supports_images(resolved_model_id):
fallback_model_id, fallback_effort = common.default_vision_model_pair()
common.logger.info(
"Using vision fallback model %s for %d Jira image(s); configured model %s "
"does not support images",
fallback_model_id,
@ -176,15 +176,15 @@ async def process_jira_issue( # noqa: PLR0912, PLR0915
)
resolved_model_id = fallback_model_id
image_model_override = (fallback_model_id, fallback_effort)
webapp.logger.info("Preparing %d image(s) for multimodal content", len(image_urls))
webapp.logger.debug("Image hosts: %s", [urlparse(u).hostname for u in image_urls])
common.logger.info("Preparing %d image(s) for multimodal content", len(image_urls))
common.logger.debug("Image hosts: %s", [urlparse(u).hostname for u in image_urls])
async with httpx.AsyncClient(timeout=webapp.DEFAULT_HTTP_TIMEOUT) as client:
async with httpx.AsyncClient(timeout=common.DEFAULT_HTTP_TIMEOUT) as client:
for image_url in image_urls:
image_block = await webapp.fetch_image_block(image_url, client)
image_block = await common.fetch_image_block(image_url, client)
if image_block:
content_blocks.append(image_block)
webapp.logger.info("Built %d content block(s) for prompt", len(content_blocks))
common.logger.info("Built %d content block(s) for prompt", len(content_blocks))
configurable: dict[str, Any] = {
"repo": repo_config,
@ -205,7 +205,7 @@ async def process_jira_issue( # noqa: PLR0912, PLR0915
configurable["agent_model_id"] = image_model_override[0]
configurable["agent_effort"] = image_model_override[1]
await webapp.upsert_agent_thread_owner_metadata(
await common.upsert_agent_thread_owner_metadata(
thread_id,
source="jira",
repo_config=repo_config,
@ -215,16 +215,16 @@ async def process_jira_issue( # noqa: PLR0912, PLR0915
source_context={"jira_issue": configurable["jira_issue"]},
)
run = await webapp.dispatch_agent_run(
run = await common.dispatch_agent_run(
thread_id,
content_blocks,
configurable,
source="jira",
metadata=webapp._AGENT_VERSION_METADATA,
metadata=common._AGENT_VERSION_METADATA,
)
webapp.logger.info(
common.logger.info(
"LangGraph run dispatched for thread %s (run=%s)",
thread_id,
run.get("run_id") if isinstance(run, dict) else None,
)
await webapp.post_jira_trace_comment(issue_key, thread_id)
await common.post_jira_trace_comment(issue_key, thread_id)

View file

@ -0,0 +1,182 @@
"""Jira webhook HTTP routes."""
from fastapi import APIRouter
from . import common
from . import jira as service
router = APIRouter()
@router.post("/webhooks/jira")
async def jira_webhook( # noqa: PLR0911, PLR0912
request: common.Request, background_tasks: common.BackgroundTasks
) -> dict[str, str]:
"""Handle Jira Automation webhooks.
Triggers a new LangGraph run when a comment mentioning ``@openswe`` is
added to an issue. Unlike Linear, Jira Cloud has no native outgoing-webhook
signing, so this is fronted by a Jira **Automation** rule (trigger:
"Issue commented") with a "Send web request" action posting a custom JSON
body to this route, carrying the shared-secret token in
``X-Automation-Webhook-Token``.
Expected payload (the Automation rule's custom JSON body, built from smart
values)::
{
"issue_key": "PROJ-123",
"comment_id": "10050",
"comment_author_is_bot": false
}
``issue_key`` (validated against the Jira key format) and ``comment_id`` are
**required** — they are the only fields trusted from the unsigned body, and
only as a pointer. The triggering comment's real author and text are then
re-fetched from Jira server-side (``fetch_jira_comment``) and everything
security-relevant (identity/attribution, the ``@openswe`` trigger check, the
prompt text, repo routing) is derived from that authoritative record, never
from payload-supplied author/body fields. ``comment_author_is_bot`` is an
optional cheap early-out only. A comment that cannot be corroborated
server-side is rejected.
"""
common.logger.info("Received Jira webhook")
if not common.verify_jira_source_ip(request):
raise common.HTTPException(status_code=403, detail="Source IP not allowed")
if not common.verify_jira_secret(request.headers):
common.logger.warning("Invalid Jira webhook token")
raise common.HTTPException(status_code=401, detail="Invalid token")
body = await request.body()
if not common.verify_jira_signature(body, request.headers):
raise common.HTTPException(status_code=401, detail="Invalid signature")
try:
payload = common.json.loads(body)
except common.json.JSONDecodeError:
common.logger.exception("Failed to parse Jira webhook JSON")
return {"status": "error", "message": "Invalid JSON"}
# Cheap early-out on the (untrusted) payload before any Jira API call.
if payload.get("comment_author_is_bot"):
common.logger.debug("Ignoring webhook: comment is from a bot")
return {"status": "ignored", "reason": "Comment is from a bot"}
issue_key = payload.get("issue_key", "") or ""
if not common.is_valid_jira_issue_key(issue_key):
common.logger.debug("Ignoring webhook: missing or malformed issue key")
return {"status": "ignored", "reason": "Missing or malformed issue key"}
comment_id = payload.get("comment_id", "") or ""
if not comment_id:
common.logger.debug("Ignoring webhook: no comment id to corroborate")
return {"status": "ignored", "reason": "No comment id in payload"}
# Corroborate against the real Jira record. The webhook body is unsigned, so
# the triggering comment's author and text are read server-side (matched by
# comment_id) rather than trusted from the payload — this is what prevents a
# secret-holder from spoofing the author (to hijack another user's token) or
# injecting arbitrary agent instructions. A comment that can't be fetched
# (nonexistent issue/comment or a forged event) is rejected.
server_comment = await common.fetch_jira_comment(issue_key, comment_id)
if not server_comment:
common.logger.warning(
"Rejecting Jira webhook: comment %s on %s could not be corroborated",
comment_id,
issue_key,
)
return {"status": "ignored", "reason": "Triggering comment not found"}
author = server_comment.get("author") or {}
account_id = author.get("account_id") or ""
display_name = author.get("name") or ""
comment_body = server_comment.get("body") or ""
for prefix in common._GITHUB_BOT_MESSAGE_PREFIXES:
if comment_body.startswith(prefix):
common.logger.debug("Ignoring webhook: comment is our own bot message")
return {"status": "ignored", "reason": "Comment is our own bot message"}
if "@openswe" not in comment_body.lower():
common.logger.debug("Ignoring webhook: comment doesn't mention @openswe")
return {"status": "ignored", "reason": "Comment doesn't mention @openswe"}
# Derive the project key from the (validated, corroborated) issue key rather
# than trusting the payload's project_key for repo routing.
project_key = issue_key.split("-", 1)[0]
actor_email = await common.get_jira_user_email(account_id) if account_id else None
repo_config = common.extract_repo_from_text(
comment_body, default_owner=common.DEFAULT_REPO_OWNER
)
if repo_config:
common.logger.debug(
"Using repo from comment body: %s/%s",
repo_config["owner"],
repo_config["name"],
)
else:
try:
profile_repo = await common.get_profile_default_repo(
await common.resolve_login_from_email_async(actor_email) if actor_email else None
)
except Exception: # noqa: BLE001
common.logger.exception("Failed to apply dashboard default_repo for Jira user")
profile_repo = None
if profile_repo:
common.logger.info(
"Applying dashboard default_repo for Jira user %s: %s/%s",
account_id,
profile_repo["owner"],
profile_repo["name"],
)
repo_config = profile_repo
if not repo_config:
repo_config = common.get_repo_config_from_jira_mapping(project_key)
if not repo_config:
repo_config = await common.get_team_default_repo()
if not repo_config:
return {"status": "ignored", "reason": "No default repository configured"}
if not common._is_repo_allowed(repo_config):
common.logger.warning(
"Rejecting Jira webhook: repo '%s/%s' not in allowlist",
repo_config.get("owner"),
repo_config.get("name"),
)
return {"status": "ignored", "reason": "Repository not in allowlist"}
issue_data = {
"key": issue_key,
"project_key": project_key,
"triggering_comment": comment_body,
"triggering_comment_id": comment_id,
"comment_author": {
"account_id": account_id,
"email": actor_email,
"name": display_name,
},
}
common.logger.info(
"Accepted webhook for issue '%s', scheduling background task",
issue_key,
)
background_tasks.add_task(service.process_jira_issue, issue_data, repo_config)
return {
"status": "accepted",
"message": f"Processing issue '{issue_key}' for repo "
f"{repo_config['owner']}/{repo_config['name']}",
}
@router.get("/webhooks/jira")
async def jira_webhook_verify() -> dict[str, str]:
"""Verify endpoint for Jira webhook setup."""
return {"status": "ok", "message": "Jira webhook endpoint is active"}

View file

@ -1,7 +1,7 @@
"""Linear webhook handler — moved out of webapp.py (behavior-identical).
"""Linear webhook handler — moved out of common.py (behavior-identical).
Helpers and constants stay in webapp.py; they are accessed through the module
object (``webapp.X``) so tests that monkeypatch them keep working.
Helpers and constants stay in common.py; they are accessed through the module
object (``common.X``) so tests that monkeypatch them keep working.
"""
from typing import Any
@ -10,7 +10,7 @@ from urllib.parse import urlparse
import httpx
from langchain_core.messages.content import create_text_block
from agent import webapp
from . import common
async def process_linear_issue( # noqa: PLR0912, PLR0915
@ -23,7 +23,7 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
repo_config: The repo configuration with owner and name.
"""
issue_id = issue_data.get("id", "")
webapp.logger.info(
common.logger.info(
"Processing Linear issue %s for repo %s/%s",
issue_id,
repo_config.get("owner"),
@ -32,11 +32,11 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
triggering_comment_id = issue_data.get("triggering_comment_id", "")
if triggering_comment_id:
await webapp.react_to_linear_comment(triggering_comment_id, "👀")
await common.react_to_linear_comment(triggering_comment_id, "👀")
thread_id = webapp.generate_thread_id_from_issue(issue_id)
thread_id = common.generate_thread_id_from_issue(issue_id)
full_issue = await webapp.fetch_linear_issue_details(issue_id)
full_issue = await common.fetch_linear_issue_details(issue_id)
if not full_issue:
full_issue = issue_data
@ -64,15 +64,15 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
user_email = assignee.get("email")
user_name = user_name or assignee.get("name")
webapp.logger.info("User email for issue %s: %s", issue_id, user_email)
common.logger.info("User email for issue %s: %s", issue_id, user_email)
title = full_issue.get("title", "No title")
description = full_issue.get("description") or "No description"
image_urls: list[str] = []
description_image_urls = webapp.extract_image_urls(description)
description_image_urls = common.extract_image_urls(description)
if description_image_urls:
image_urls.extend(description_image_urls)
webapp.logger.debug(
common.logger.debug(
"Found %d image URL(s) in issue description",
len(description_image_urls),
)
@ -107,12 +107,12 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
trigger_index = comment_id_to_index.get(triggering_comment_id)
if trigger_index is not None:
relevant_comments = comments[trigger_index:]
webapp.logger.debug(
common.logger.debug(
"Using triggering comment index %d to build relevant comments",
trigger_index,
)
else:
relevant_comments = webapp.get_recent_comments(comments, bot_message_prefixes)
relevant_comments = common.get_recent_comments(comments, bot_message_prefixes)
if relevant_comments:
comments_text = "\n\n## Comments:\n"
@ -120,10 +120,10 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
user = comment.get("user") or {}
author = user.get("name", "User")
body = comment.get("body", "")
body_image_urls = webapp.extract_image_urls(body)
body_image_urls = common.extract_image_urls(body)
if body_image_urls:
image_urls.extend(body_image_urls)
webapp.logger.debug(
common.logger.debug(
"Found %d image URL(s) in comment by %s",
len(body_image_urls),
author,
@ -137,16 +137,16 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
comments_text = "\n\n## Comments:\n"
trigger_author = comment_author.get("name", "Unknown")
trigger_body = triggering_comment
trigger_image_urls = webapp.extract_image_urls(trigger_body)
trigger_image_urls = common.extract_image_urls(trigger_body)
if trigger_image_urls:
image_urls.extend(trigger_image_urls)
webapp.logger.debug(
common.logger.debug(
"Found %d image URL(s) in triggering comment by %s",
len(trigger_image_urls),
trigger_author,
)
comments_text += f"\n**{trigger_author}:** {trigger_body}\n"
webapp.logger.debug(
common.logger.debug(
"Appended triggering comment %s not present in issue comments list",
triggering_comment_id or "<missing-id>",
)
@ -183,15 +183,15 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
# user-mapping store Slack uses, so PRs open *as the triggering user* and the
# thread is tagged for the dashboard. Restricted to the comment author so
# token attribution never falls back to creator/assignee.
mapped_login = await webapp.resolve_login_from_email_async(actor_email) if actor_email else None
mapped_login = await common.resolve_login_from_email_async(actor_email) if actor_email else None
image_model_override: tuple[str, str] | None = None
if image_urls:
image_urls = webapp.dedupe_urls(image_urls)
resolved_model_id = await webapp.resolve_agent_model_id(mapped_login)
if not webapp.model_supports_images(resolved_model_id):
fallback_model_id, fallback_effort = webapp.default_vision_model_pair()
webapp.logger.info(
image_urls = common.dedupe_urls(image_urls)
resolved_model_id = await common.resolve_agent_model_id(mapped_login)
if not common.model_supports_images(resolved_model_id):
fallback_model_id, fallback_effort = common.default_vision_model_pair()
common.logger.info(
"Using vision fallback model %s for %d Linear image(s); configured model %s "
"does not support images",
fallback_model_id,
@ -200,15 +200,15 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
)
resolved_model_id = fallback_model_id
image_model_override = (fallback_model_id, fallback_effort)
webapp.logger.info("Preparing %d image(s) for multimodal content", len(image_urls))
webapp.logger.debug("Image hosts: %s", [urlparse(u).hostname for u in image_urls])
common.logger.info("Preparing %d image(s) for multimodal content", len(image_urls))
common.logger.debug("Image hosts: %s", [urlparse(u).hostname for u in image_urls])
async with httpx.AsyncClient(timeout=webapp.DEFAULT_HTTP_TIMEOUT) as client:
async with httpx.AsyncClient(timeout=common.DEFAULT_HTTP_TIMEOUT) as client:
for image_url in image_urls:
image_block = await webapp.fetch_image_block(image_url, client)
image_block = await common.fetch_image_block(image_url, client)
if image_block:
content_blocks.append(image_block)
webapp.logger.info("Built %d content block(s) for prompt", len(content_blocks))
common.logger.info("Built %d content block(s) for prompt", len(content_blocks))
linear_project_id = ""
linear_issue_number = ""
@ -237,7 +237,7 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
configurable["agent_model_id"] = image_model_override[0]
configurable["agent_effort"] = image_model_override[1]
await webapp.upsert_agent_thread_owner_metadata(
await common.upsert_agent_thread_owner_metadata(
thread_id,
source="linear",
repo_config=repo_config,
@ -247,16 +247,16 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
source_context={"linear_issue": configurable["linear_issue"]},
)
run = await webapp.dispatch_agent_run(
run = await common.dispatch_agent_run(
thread_id,
content_blocks,
configurable,
source="linear",
metadata=webapp._AGENT_VERSION_METADATA,
metadata=common._AGENT_VERSION_METADATA,
)
webapp.logger.info(
common.logger.info(
"LangGraph run dispatched for thread %s (run=%s)",
thread_id,
run.get("run_id") if isinstance(run, dict) else None,
)
await webapp.post_linear_trace_comment(issue_id, thread_id, triggering_comment_id)
await common.post_linear_trace_comment(issue_id, thread_id, triggering_comment_id)

View file

@ -0,0 +1,168 @@
"""Linear webhook HTTP routes."""
from fastapi import APIRouter
from . import common
from . import linear as service
router = APIRouter()
@router.post("/webhooks/linear")
async def linear_webhook( # noqa: PLR0911, PLR0912, PLR0915
request: common.Request, background_tasks: common.BackgroundTasks
) -> dict[str, str]:
"""Handle Linear webhooks.
Triggers a new LangGraph run when an issue gets the 'open-swe' label added.
"""
common.logger.info("Received Linear webhook")
body = await request.body()
signature = request.headers.get("Linear-Signature", "")
if not common.verify_linear_signature(body, signature, common.LINEAR_WEBHOOK_SECRET):
common.logger.warning("Invalid webhook signature")
raise common.HTTPException(status_code=401, detail="Invalid signature")
try:
payload = common.json.loads(body)
except common.json.JSONDecodeError:
common.logger.exception("Failed to parse webhook JSON")
return {"status": "error", "message": "Invalid JSON"}
if payload.get("type") != "Comment":
common.logger.debug("Ignoring webhook: not a Comment event")
return {"status": "ignored", "reason": "Not a Comment event"}
action = payload.get("action")
if action != "create":
common.logger.debug("Ignoring webhook: action is %s, not create", action)
return {
"status": "ignored",
"reason": f"Comment action is '{action}', only processing 'create'",
}
data = payload.get("data", {})
if data.get("botActor"):
common.logger.debug("Ignoring webhook: comment is from a bot")
return {"status": "ignored", "reason": "Comment is from a bot"}
comment_body = data.get("body", "")
bot_message_prefixes = [
"🔐 **GitHub Authentication Required**",
"✅ **Pull Request Created**",
"✅ **Pull Request Updated**",
"**Pull Request Created**",
"**Pull Request Updated**",
"🤖 **Agent Response**",
"❌ **Agent Error**",
]
for prefix in bot_message_prefixes:
if comment_body.startswith(prefix):
common.logger.debug("Ignoring webhook: comment is our own bot message")
return {"status": "ignored", "reason": "Comment is our own bot message"}
if "@openswe" not in comment_body.lower():
common.logger.debug("Ignoring webhook: comment doesn't mention @openswe")
return {"status": "ignored", "reason": "Comment doesn't mention @openswe"}
issue = data.get("issue", {})
if not issue:
common.logger.debug("Ignoring webhook: no issue data in comment")
return {"status": "ignored", "reason": "No issue data in comment"}
# Fetch full issue details to get project info (webhook doesn't include it)
issue_id = issue.get("id", "")
full_issue = await common.fetch_linear_issue_details(issue_id)
if not full_issue:
common.logger.warning("Failed to fetch full issue details, using webhook data")
full_issue = issue
repo_config = common.extract_repo_from_text(
comment_body, default_owner=common.DEFAULT_REPO_OWNER
)
if repo_config:
common.logger.debug(
"Using repo from comment body: %s/%s",
repo_config["owner"],
repo_config["name"],
)
else:
comment_user_email = (data.get("user") or {}).get("email")
try:
profile_repo = await common.get_profile_default_repo(
await common.resolve_login_from_email_async(comment_user_email)
)
except Exception: # noqa: BLE001
common.logger.exception("Failed to apply dashboard default_repo for Linear user")
profile_repo = None
if profile_repo:
common.logger.info(
"Applying dashboard default_repo for Linear user %s: %s/%s",
comment_user_email,
profile_repo["owner"],
profile_repo["name"],
)
repo_config = profile_repo
if not repo_config:
team = full_issue.get("team", {})
team_name = team.get("name", "") if team else ""
project = full_issue.get("project")
project_name = project.get("name", "") if project else ""
team_identifier = team_name.strip() if team_name else ""
project_key = project_name.strip() if project_name else ""
repo_config = common.get_repo_config_from_team_mapping(team_identifier, project_key)
common.logger.debug(
"Team/project lookup result",
extra={
"team_name": team_identifier,
"project_name": project_key,
"repo_config": repo_config,
},
)
if not repo_config:
repo_config = await common.get_team_default_repo()
if not repo_config:
return {"status": "ignored", "reason": "No default repository configured"}
if not common._is_repo_allowed(repo_config):
common.logger.warning(
"Rejecting Linear webhook: repo '%s/%s' not in allowlist",
repo_config.get("owner"),
repo_config.get("name"),
)
return {"status": "ignored", "reason": "Repository not in allowlist"}
repo_owner = repo_config["owner"]
repo_name = repo_config["name"]
issue["triggering_comment"] = comment_body
issue["triggering_comment_id"] = data.get("id", "")
comment_user = data.get("user", {})
if comment_user:
issue["comment_author"] = comment_user
common.logger.info(
"Accepted webhook for issue '%s' (%s), scheduling background task",
issue.get("title"),
issue.get("id"),
)
background_tasks.add_task(service.process_linear_issue, issue, repo_config)
return {
"status": "accepted",
"message": f"Processing issue '{issue.get('title')}' for repo {repo_owner}/{repo_name}",
}
@router.get("/webhooks/linear")
async def linear_webhook_verify() -> dict[str, str]:
"""Verify endpoint for Linear webhook setup."""
return {"status": "ok", "message": "Linear webhook endpoint is active"}

View file

@ -1,7 +1,7 @@
"""Slack webhook handler — moved out of webapp.py (behavior-identical).
"""Slack webhook handler — moved out of common.py (behavior-identical).
Helpers and constants stay in webapp.py; they are accessed through the module
object (``webapp.X``) so tests that monkeypatch them keep working.
Helpers and constants stay in common.py; they are accessed through the module
object (``common.X``) so tests that monkeypatch them keep working.
"""
from datetime import UTC, datetime
@ -10,7 +10,7 @@ from typing import Any
import httpx
from langchain_core.messages.content import create_text_block
from agent import webapp
from . import common
def _format_slack_thread_section(
@ -31,9 +31,9 @@ def _format_slack_thread_section(
lines.append(f"- Channel name: #{channel_name}")
lines.append(f"- Thread TS: {thread_ts}")
lines.append(f"- Context starts at: {context_source}")
channel_description = webapp.get_slack_channel_context_description(channel_context)
channel_description = common.get_slack_channel_context_description(channel_context)
if channel_description:
lines.extend(webapp.format_untrusted_channel_description(channel_description))
lines.extend(common.format_untrusted_channel_description(channel_description))
return "\n".join(lines)
@ -42,7 +42,7 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
try:
await _process_slack_mention_impl(event_data, repo_config)
except Exception: # noqa: BLE001
webapp.logger.exception("Unexpected error while processing Slack mention")
common.logger.exception("Unexpected error while processing Slack mention")
await _notify_slack_processing_error(event_data, repo_config)
@ -58,13 +58,13 @@ async def _notify_slack_processing_error(
if not channel_id or not thread_ts:
return
thread_id = webapp.generate_thread_id_from_slack_thread(channel_id, thread_ts)
thread_id = common.generate_thread_id_from_slack_thread(channel_id, thread_ts)
try:
clean_text = (
webapp.strip_bot_mention(text, bot_user_id, bot_username=webapp.SLACK_BOT_USERNAME)
common.strip_bot_mention(text, bot_user_id, bot_username=common.SLACK_BOT_USERNAME)
or "Slack request"
)
await webapp.upsert_agent_thread_owner_metadata(
await common.upsert_agent_thread_owner_metadata(
thread_id,
source="slack",
repo_config=repo_config,
@ -79,12 +79,12 @@ async def _notify_slack_processing_error(
},
)
except Exception: # noqa: BLE001
webapp.logger.warning(
common.logger.warning(
"Could not persist Slack error metadata for thread %s", thread_id, exc_info=True
)
try:
await webapp.get_client(url=webapp.LANGGRAPH_URL).threads.update(
await common.get_client(url=common.LANGGRAPH_URL).threads.update(
thread_id=thread_id,
metadata={
"latest_run_status": "error",
@ -92,14 +92,14 @@ async def _notify_slack_processing_error(
},
)
except Exception: # noqa: BLE001
webapp.logger.warning("Could not mark Slack thread %s as errored", thread_id, exc_info=True)
common.logger.warning("Could not mark Slack thread %s as errored", thread_id, exc_info=True)
try:
await webapp.set_slack_assistant_status(channel_id, thread_ts, status="")
await common.set_slack_assistant_status(channel_id, thread_ts, status="")
except Exception: # noqa: BLE001
webapp.logger.debug("Could not clear Slack assistant status", exc_info=True)
common.logger.debug("Could not clear Slack assistant status", exc_info=True)
dashboard_url = webapp.dashboard_thread_url(thread_id)
dashboard_url = common.dashboard_thread_url(thread_id)
message = (
"⚠️ I hit an unexpected error while handling this Slack thread. "
"Send another message and I'll try again."
@ -107,9 +107,9 @@ async def _notify_slack_processing_error(
if dashboard_url:
message += f" You can view the error in <{dashboard_url}|Open SWE Web>."
try:
await webapp.post_slack_thread_reply(channel_id, thread_ts, message)
await common.post_slack_thread_reply(channel_id, thread_ts, message)
except Exception: # noqa: BLE001
webapp.logger.warning(
common.logger.warning(
"Could not post Slack error notification for thread %s", thread_id, exc_info=True
)
@ -127,11 +127,11 @@ async def _process_slack_mention_impl(
channel_context = (
channel_context_raw
if isinstance(channel_context_raw, dict)
else webapp.normalize_slack_channel_context(channel_id, None)
else common.normalize_slack_channel_context(channel_id, None)
)
if not channel_id or not thread_ts or not event_ts:
webapp.logger.warning(
common.logger.warning(
"Missing Slack event fields (channel_id=%s, thread_ts=%s, event_ts=%s)",
channel_id,
thread_ts,
@ -139,20 +139,20 @@ async def _process_slack_mention_impl(
)
return
await webapp.set_slack_assistant_status(channel_id, thread_ts)
await common.set_slack_assistant_status(channel_id, thread_ts)
thread_id = webapp.generate_thread_id_from_slack_thread(channel_id, thread_ts)
thread_id = common.generate_thread_id_from_slack_thread(channel_id, thread_ts)
# Prime the user-mapping cache so login/email/slack-id lookups below are warm.
try:
await webapp.refresh_user_mapping_cache()
await common.refresh_user_mapping_cache()
except Exception: # noqa: BLE001
webapp.logger.debug("Could not refresh user mapping cache for Slack mention", exc_info=True)
common.logger.debug("Could not refresh user mapping cache for Slack mention", exc_info=True)
user_email = None
user_name = ""
if user_id:
slack_user = await webapp.get_slack_user_info(user_id)
slack_user = await common.get_slack_user_info(user_id)
if slack_user:
profile = slack_user.get("profile", {})
if isinstance(profile, dict):
@ -165,26 +165,26 @@ async def _process_slack_mention_impl(
or ""
)
thread_messages = await webapp.fetch_slack_thread_messages(channel_id, thread_ts)
thread_messages = await common.fetch_slack_thread_messages(channel_id, thread_ts)
if not any(str(message.get("ts")) == str(event_ts) for message in thread_messages):
thread_messages.append({"ts": event_ts, "text": text, "user": user_id})
context_messages, context_mode = webapp.select_slack_context_messages(
thread_messages, event_ts, bot_user_id, webapp.SLACK_BOT_USERNAME
context_messages, context_mode = common.select_slack_context_messages(
thread_messages, event_ts, bot_user_id, common.SLACK_BOT_USERNAME
)
context_user_ids = [
value
for value in (message.get("user") for message in context_messages)
if isinstance(value, str) and value
]
user_names_by_id = await webapp.get_slack_user_names(context_user_ids)
user_names_by_id = await common.get_slack_user_names(context_user_ids)
if user_id and user_name and user_id not in user_names_by_id:
user_names_by_id[user_id] = user_name
context_text = webapp.format_slack_messages_for_prompt(
context_text = common.format_slack_messages_for_prompt(
context_messages,
user_names_by_id,
bot_user_id=bot_user_id,
bot_username=webapp.SLACK_BOT_USERNAME,
bot_username=common.SLACK_BOT_USERNAME,
)
context_source = (
"the previous message where I was tagged"
@ -192,13 +192,13 @@ async def _process_slack_mention_impl(
else "the beginning of the thread"
)
clean_text = (
webapp.strip_bot_mention(text, bot_user_id, bot_username=webapp.SLACK_BOT_USERNAME)
common.strip_bot_mention(text, bot_user_id, bot_username=common.SLACK_BOT_USERNAME)
or "(no text in mention)"
)
trigger_user = user_name or (f"<@{user_id}>" if user_id else "Unknown user")
# Auto-resolve cross-posted Slack message links in context
resolved_links_section, image_urls_from_links = await webapp.resolve_slack_links_in_context(
resolved_links_section, image_urls_from_links = await common.resolve_slack_links_in_context(
context_messages, user_names_by_id
)
@ -223,8 +223,8 @@ async def _process_slack_mention_impl(
)
content_blocks: list[dict[str, Any]] = [create_text_block(prompt)]
image_urls = webapp.dedupe_urls(
[url for msg in context_messages for url in webapp.extract_image_urls(msg.get("text", ""))]
image_urls = common.dedupe_urls(
[url for msg in context_messages for url in common.extract_image_urls(msg.get("text", ""))]
+ [
f["url_private"]
for msg in context_messages
@ -236,16 +236,16 @@ async def _process_slack_mention_impl(
+ image_urls_from_links
)
mapped_login = await webapp.login_for_slack_id(user_id)
mapped_login = await common.login_for_slack_id(user_id)
if not mapped_login and user_email:
mapped_login = await webapp.login_for_email(user_email)
mapped_login = await common.login_for_email(user_email)
image_model_override: tuple[str, str] | None = None
if image_urls:
resolved_model_id = await webapp.resolve_agent_model_id(mapped_login)
if not webapp.model_supports_images(resolved_model_id):
fallback_model_id, fallback_effort = webapp.default_vision_model_pair()
webapp.logger.info(
resolved_model_id = await common.resolve_agent_model_id(mapped_login)
if not common.model_supports_images(resolved_model_id):
fallback_model_id, fallback_effort = common.default_vision_model_pair()
common.logger.info(
"Using vision fallback model %s for %d Slack image(s); configured model %s "
"does not support images",
fallback_model_id,
@ -254,10 +254,10 @@ async def _process_slack_mention_impl(
)
resolved_model_id = fallback_model_id
image_model_override = (fallback_model_id, fallback_effort)
webapp.logger.info("Preparing %d image(s) for Slack mention", len(image_urls))
async with httpx.AsyncClient(timeout=webapp.DEFAULT_HTTP_TIMEOUT) as http_client:
common.logger.info("Preparing %d image(s) for Slack mention", len(image_urls))
async with httpx.AsyncClient(timeout=common.DEFAULT_HTTP_TIMEOUT) as http_client:
for image_url in image_urls:
image_block = await webapp.fetch_image_block(image_url, http_client)
image_block = await common.fetch_image_block(image_url, http_client)
if image_block:
content_blocks.append(image_block)
@ -269,9 +269,9 @@ async def _process_slack_mention_impl(
user_token: str | None = None
if mapped_login:
try:
user_token = await webapp.get_valid_access_token(mapped_login)
user_token = await common.get_valid_access_token(mapped_login)
except Exception: # noqa: BLE001
webapp.logger.debug(
common.logger.debug(
"Failed to resolve GitHub token for %s; treating as unauthenticated",
mapped_login,
exc_info=True,
@ -279,7 +279,7 @@ async def _process_slack_mention_impl(
user_token = None
has_valid_user_token = bool(user_token)
if not has_valid_user_token and not webapp.is_bot_token_only_mode():
if not has_valid_user_token and not common.is_bot_token_only_mode():
# A stored-but-unusable token means "sign in again"; no record at all
# means the user has never connected GitHub + Slack via the dashboard.
# Guard the store read like token resolution above so a transient
@ -287,24 +287,24 @@ async def _process_slack_mention_impl(
has_token_record = False
if mapped_login:
try:
has_token_record = await webapp.has_access_token_record(mapped_login)
has_token_record = await common.has_access_token_record(mapped_login)
except Exception: # noqa: BLE001
webapp.logger.debug(
common.logger.debug(
"Failed to check GitHub token record for %s; prompting sign-in",
mapped_login,
exc_info=True,
)
reason = "revoked" if has_token_record else "unlinked"
webapp.logger.info(
common.logger.info(
"Blocking Slack run for thread %s: no valid user GitHub token (%s)",
thread_id,
reason,
)
if user_id:
await webapp._post_account_link_prompt(
await common._post_account_link_prompt(
channel_id, thread_ts, user_id, user_email, reason=reason
)
await webapp.set_slack_assistant_status(channel_id, thread_ts, status="")
await common.set_slack_assistant_status(channel_id, thread_ts, status="")
return
configurable: dict[str, Any] = {
@ -327,17 +327,17 @@ async def _process_slack_mention_impl(
configurable["agent_model_id"] = image_model_override[0]
configurable["agent_effort"] = image_model_override[1]
thread_plan_mode = await webapp._get_thread_plan_mode(thread_id)
thread_plan_mode = await common._get_thread_plan_mode(thread_id)
if thread_plan_mode is not None:
configurable["plan_mode"] = thread_plan_mode
langgraph_client = webapp.get_client(url=webapp.LANGGRAPH_URL)
is_first_mention = not await webapp._thread_exists(thread_id)
await webapp._upsert_slack_thread_repo_metadata(thread_id, repo_config, langgraph_client)
langgraph_client = common.get_client(url=common.LANGGRAPH_URL)
is_first_mention = not await common._thread_exists(thread_id)
await common._upsert_slack_thread_repo_metadata(thread_id, repo_config, langgraph_client)
# Pass the login resolved above (from the stable Slack user id) so the thread is
# always tagged with github_login — the key the dashboard searches by. Without
# it, upsert re-resolves from the Slack profile email, which can miss.
await webapp.upsert_agent_thread_owner_metadata(
await common.upsert_agent_thread_owner_metadata(
thread_id,
source="slack",
repo_config=repo_config,
@ -347,25 +347,25 @@ async def _process_slack_mention_impl(
source_context={"slack_thread": configurable["slack_thread"]},
)
run = await webapp.dispatch_agent_run(
run = await common.dispatch_agent_run(
thread_id,
content_blocks,
configurable,
source="slack",
metadata=webapp._AGENT_VERSION_METADATA,
metadata=common._AGENT_VERSION_METADATA,
client=langgraph_client,
)
webapp.logger.info(
common.logger.info(
"Slack LangGraph run %s dispatched for thread %s",
webapp._run_id_for_logging(run),
common._run_id_for_logging(run),
thread_id,
)
run_id = run.get("run_id")
if is_first_mention:
trace_message_ts = await webapp.post_slack_trace_reply(channel_id, thread_ts, thread_id)
await webapp.set_slack_assistant_status(channel_id, thread_ts)
trace_message_ts = await common.post_slack_trace_reply(channel_id, thread_ts, thread_id)
await common.set_slack_assistant_status(channel_id, thread_ts)
if isinstance(run_id, str) and run_id:
await webapp.store_slack_run_mapping(
await common.store_slack_run_mapping(
langgraph_client,
channel_id,
thread_ts,
@ -375,12 +375,12 @@ async def _process_slack_mention_impl(
triggering_user_id=user_id,
)
else:
webapp.logger.info(
common.logger.info(
"Skipping Slack trace reply for thread %s — agent will reply when run completes",
thread_id,
)
if isinstance(run_id, str) and run_id:
await webapp.store_slack_run_mapping(
await common.store_slack_run_mapping(
langgraph_client,
channel_id,
thread_ts,

View file

@ -0,0 +1,347 @@
"""Slack webhook HTTP routes."""
from fastapi import APIRouter
from . import common
from . import slack as service
router = APIRouter()
@router.post("/webhooks/slack")
async def slack_webhook(
request: common.Request, background_tasks: common.BackgroundTasks
) -> dict[str, str]:
"""Handle Slack Event API webhooks for app mentions."""
body = await request.body()
signature = request.headers.get("X-Slack-Signature", "")
timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
if not common.verify_slack_signature(
body=body,
timestamp=timestamp,
signature=signature,
secret=common.SLACK_SIGNING_SECRET,
):
common.logger.warning("Invalid Slack signature")
raise common.HTTPException(status_code=401, detail="Invalid signature")
try:
payload = common.json.loads(body)
except common.json.JSONDecodeError:
common.logger.exception("Failed to parse Slack webhook JSON")
return {"status": "error", "message": "Invalid JSON"}
if payload.get("type") == "url_verification":
challenge = payload.get("challenge", "")
return {"challenge": challenge}
if payload.get("type") != "event_callback":
return {"status": "ignored", "reason": "Not an event callback"}
event = payload.get("event", {})
if event.get("type") == "reaction_added":
reaction = event.get("reaction")
if reaction in common.FEEDBACK_REACTIONS:
background_tasks.add_task(
common.process_slack_reaction_added, event, payload.get("event_id", "")
)
return {"status": "accepted", "message": "Reaction feedback queued"}
return {"status": "ignored", "reason": "Reaction not tracked for feedback"}
if event.get("type") == "reaction_removed":
reaction = event.get("reaction")
if reaction in common.FEEDBACK_REACTIONS:
background_tasks.add_task(
common.process_slack_reaction_removed, event, payload.get("event_id", "")
)
return {"status": "accepted", "message": "Reaction removal queued"}
return {"status": "ignored", "reason": "Reaction not tracked for feedback"}
if event.get("type") != "app_mention":
message_text = event.get("text", "")
has_username_mention = bool(
event.get("type") == "message"
and common.SLACK_BOT_USERNAME
and f"@{common.SLACK_BOT_USERNAME}" in message_text
)
has_id_mention = bool(
event.get("type") == "message"
and common.SLACK_BOT_USER_ID
and f"<@{common.SLACK_BOT_USER_ID}>" in message_text
)
if not (has_username_mention or has_id_mention):
return {"status": "ignored", "reason": "Not an app_mention event"}
if event.get("subtype") == "bot_message" or event.get("bot_id"):
return {"status": "ignored", "reason": "Event from a bot"}
channel_id = event.get("channel", "")
event_ts = event.get("ts", "")
thread_ts = event.get("thread_ts") or event_ts
user_id = event.get("user", "")
text = event.get("text", "")
if not channel_id or not event_ts or not thread_ts:
return {"status": "ignored", "reason": "Missing channel/thread timestamp"}
bot_user_id = common.SLACK_BOT_USER_ID
if not bot_user_id:
authorizations = payload.get("authorizations", [])
if isinstance(authorizations, list) and authorizations:
auth_user_id = authorizations[0].get("user_id")
if isinstance(auth_user_id, str):
bot_user_id = auth_user_id
if not bot_user_id:
authed_users = payload.get("authed_users", [])
if isinstance(authed_users, list) and authed_users:
first_user = authed_users[0]
if isinstance(first_user, str):
bot_user_id = first_user
if bot_user_id and user_id == bot_user_id:
return {"status": "ignored", "reason": "Event from this bot user"}
channel_context = await common._get_slack_channel_context(channel_id)
if await common._is_docs_plz_slack_channel(channel_id, channel_context):
background_tasks.add_task(
common.post_slack_thread_reply,
channel_id,
thread_ts,
common.DOCS_PLZ_SLACK_GATE_REPLY,
)
return {"status": "accepted", "message": "Slack mention gated for docs-plz"}
event_data = {
"channel_id": channel_id,
"channel_context": channel_context,
"thread_ts": thread_ts,
"event_ts": event_ts,
"user_id": user_id,
"text": text,
"bot_user_id": bot_user_id,
}
repo_config = await common.get_slack_repo_config(
channel_id, thread_ts, slack_user_id=user_id, channel_context=channel_context
)
background_tasks.add_task(service.process_slack_mention, event_data, repo_config)
return {"status": "accepted", "message": "Slack mention queued"}
@router.post("/webhooks/slack/interactivity")
async def slack_interactivity(
request: common.Request, background_tasks: common.BackgroundTasks
) -> dict[str, str]:
"""Handle Slack Block Kit interactions."""
body = await request.body()
signature = request.headers.get("X-Slack-Signature", "")
timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
if not common.verify_slack_signature(
body=body,
timestamp=timestamp,
signature=signature,
secret=common.SLACK_SIGNING_SECRET,
):
common.logger.warning("Invalid Slack interactivity signature")
raise common.HTTPException(status_code=401, detail="Invalid signature")
form = common.parse_qs(body.decode("utf-8"))
payload_raw = (form.get("payload") or [""])[0]
try:
payload = common.json.loads(payload_raw)
except common.json.JSONDecodeError:
common.logger.exception("Failed to parse Slack interactivity payload")
return {"status": "error", "message": "Invalid payload"}
action = _first_open_swe_option_action(payload.get("actions"))
if action is None:
return {"status": "ignored", "reason": "No Open SWE action"}
try:
action_value = common.json.loads(str(action.get("value") or "{}"))
except common.json.JSONDecodeError:
return {"status": "ignored", "reason": "Invalid action value"}
if action_value.get("type") == "workflow_push_approval":
workflow_action = str(action_value.get("action") or "").strip()
fingerprint = str(action_value.get("fingerprint") or "").strip()
channel = payload.get("channel") if isinstance(payload.get("channel"), dict) else {}
message = payload.get("message") if isinstance(payload.get("message"), dict) else {}
container = payload.get("container") if isinstance(payload.get("container"), dict) else {}
user = payload.get("user") if isinstance(payload.get("user"), dict) else {}
channel_id = str(channel.get("id") or container.get("channel_id") or "")
thread_ts = str(
message.get("thread_ts") or message.get("ts") or container.get("thread_ts") or ""
)
user_id = str(user.get("id") or "")
if not channel_id or not thread_ts or not fingerprint:
return {"status": "ignored", "reason": "Missing workflow approval context"}
thread_id = common.generate_thread_id_from_slack_thread(channel_id, thread_ts)
if not await common._slack_user_is_thread_owner(thread_id, user_id):
await common.post_slack_thread_reply(
channel_id=channel_id,
thread_ts=thread_ts,
text="Only the person who requested this run can approve workflow file pushes.",
)
return {"status": "ignored", "reason": "approver is not the thread owner"}
if workflow_action not in {"approve", "reject"}:
return {"status": "ignored", "reason": "Unknown workflow approval action"}
approved = workflow_action == "approve"
record = await common.decide_workflow_push_approval(
thread_id, fingerprint, approved=approved, actor=user_id
)
if record is None:
await common.post_slack_thread_reply(
channel_id=channel_id,
thread_ts=thread_ts,
text="I couldn't find that workflow approval request. Trigger the push again to create a fresh approval.",
)
return {"status": "ignored", "reason": "workflow approval not found"}
if not approved:
await common.post_slack_thread_reply(
channel_id=channel_id,
thread_ts=thread_ts,
text=f"Workflow push rejected for fingerprint `{fingerprint}`. No workflow files will be pushed.",
)
return {"status": "accepted", "message": "Workflow push rejected"}
await common.post_slack_thread_reply(
channel_id=channel_id,
thread_ts=thread_ts,
text=f"Workflow push approved for fingerprint `{fingerprint}`. Open SWE will retry the blocked push.",
)
channel_context = await common._get_slack_channel_context(channel_id)
repo_config = await common.get_slack_repo_config(
channel_id, thread_ts, slack_user_id=user_id, channel_context=channel_context
)
background_tasks.add_task(
service.process_slack_mention,
{
"channel_id": channel_id,
"channel_context": channel_context,
"thread_ts": thread_ts,
"event_ts": str(message.get("ts") or ""),
"user_id": user_id,
"text": (
"The workflow-file push approval was approved. Retry the blocked "
"git push now; do not alter workflow files before pushing."
),
"bot_user_id": common.SLACK_BOT_USER_ID,
},
repo_config,
)
return {"status": "accepted", "message": "Workflow push approved, retry queued"}
if action_value.get("type") == "plan_approval":
plan_action = str(action_value.get("action") or "").strip()
channel = payload.get("channel") if isinstance(payload.get("channel"), dict) else {}
message = payload.get("message") if isinstance(payload.get("message"), dict) else {}
container = payload.get("container") if isinstance(payload.get("container"), dict) else {}
user = payload.get("user") if isinstance(payload.get("user"), dict) else {}
channel_id = str(channel.get("id") or container.get("channel_id") or "")
thread_ts = str(
message.get("thread_ts") or message.get("ts") or container.get("thread_ts") or ""
)
user_id = str(user.get("id") or "")
if not channel_id or not thread_ts:
return {"status": "ignored", "reason": "Missing Slack action context"}
thread_id = common.generate_thread_id_from_slack_thread(channel_id, thread_ts)
if plan_action == "cancel":
await common.post_slack_thread_reply(
channel_id=channel_id,
thread_ts=thread_ts,
text="Plan cancelled. No changes will be made.",
)
return {"status": "accepted", "message": "Plan cancelled"}
if plan_action == "approve":
if not await common._slack_user_is_thread_owner(thread_id, user_id):
await common.post_slack_thread_reply(
channel_id=channel_id,
thread_ts=thread_ts,
text="Only the person who requested this plan can approve it. Anyone can reply with feedback or use *Revise Plan*.",
)
return {"status": "ignored", "reason": "approver is not the thread owner"}
await common._set_thread_plan_mode(thread_id, False)
channel_context = await common._get_slack_channel_context(channel_id)
repo_config = await common.get_slack_repo_config(
channel_id, thread_ts, slack_user_id=user_id, channel_context=channel_context
)
background_tasks.add_task(
service.process_slack_mention,
{
"channel_id": channel_id,
"channel_context": channel_context,
"thread_ts": thread_ts,
"event_ts": str(message.get("ts") or ""),
"user_id": user_id,
"text": "Proceed with the approved plan. Implement the changes as described in the plan.",
"bot_user_id": common.SLACK_BOT_USER_ID,
},
repo_config,
)
return {"status": "accepted", "message": "Plan approved, starting implementation"}
return {"status": "accepted", "message": "Reply to revise the plan"}
if action_value.get("type") != "open_swe_option":
return {"status": "ignored", "reason": "Unknown action type"}
response = str(action_value.get("response") or "").strip()
if not response:
return {"status": "ignored", "reason": "Empty response"}
channel = payload.get("channel") if isinstance(payload.get("channel"), dict) else {}
message = payload.get("message") if isinstance(payload.get("message"), dict) else {}
container = payload.get("container") if isinstance(payload.get("container"), dict) else {}
user = payload.get("user") if isinstance(payload.get("user"), dict) else {}
channel_id = str(channel.get("id") or container.get("channel_id") or "")
event_ts = str(
action.get("action_ts") or message.get("ts") or container.get("message_ts") or ""
)
thread_ts = str(
message.get("thread_ts") or message.get("ts") or container.get("thread_ts") or event_ts
)
user_id = str(user.get("id") or "")
if not channel_id or not thread_ts or not event_ts or not user_id:
return {"status": "ignored", "reason": "Missing Slack action context"}
channel_context = await common._get_slack_channel_context(channel_id)
repo_config = await common.get_slack_repo_config(
channel_id, thread_ts, slack_user_id=user_id, channel_context=channel_context
)
background_tasks.add_task(
service.process_slack_mention,
{
"channel_id": channel_id,
"channel_context": channel_context,
"thread_ts": thread_ts,
"event_ts": event_ts,
"user_id": user_id,
"text": response,
"bot_user_id": common.SLACK_BOT_USER_ID,
},
repo_config,
)
return {"status": "accepted", "message": "Slack option queued"}
def _first_open_swe_option_action(actions: common.Any) -> dict[str, common.Any] | None:
if not isinstance(actions, list):
return None
for action in actions:
if isinstance(action, dict) and action.get("action_id") == "open_swe_option_select":
return action
return None
@router.get("/webhooks/slack")
async def slack_webhook_verify() -> dict[str, str]:
"""Verify endpoint for Slack webhook setup."""
return {"status": "ok", "message": "Slack webhook endpoint is active"}

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -439,7 +439,7 @@ Open SWE supports a `default_prompt.md` file for org-level instructions that app
The file is loaded at agent startup and injected into the system prompt between the task overview and repository setup sections.
**Location:** [`default_prompt.md`](./default_prompt.md) in the project root.
**Location:** [`agent/resources/default_prompt.md`](../agent/resources/default_prompt.md) for the bundled default.
**Override:** Set the `DEFAULT_PROMPT_PATH` environment variable to use a different file:

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,322 @@
A .env.example
A .githooks/_reject_guard.sh
A .githooks/commit-msg
A .githooks/post-commit
A .githooks/pre-push
A .githooks/prepare-commit-msg
A .github/CODEOWNERS
A .github/ISSUE_TEMPLATE/bug.yml
A .github/ISSUE_TEMPLATE/config.yml
A .github/ISSUE_TEMPLATE/feature.yml
A .github/ISSUE_TEMPLATE/task.yml
A .github/PULL_REQUEST_TEMPLATE.md
M .github/dependabot.yml
A .github/scripts/check-dev-green.sh
M .github/workflows/ci.yml
A .github/workflows/dependency-review.yml
A .github/workflows/labeler.yml
M .github/workflows/pr_lint.yml
A .github/workflows/promote-to-main.yml
D .github/workflows/promote_main_to_prod.yml
R096 .github/workflows/reviewer_eval.yml .github/workflows/reviewer-eval.yml
A .github/workflows/upstream-ledger-sync.yml
M .gitignore
A .nvmrc
A .security-review/suppressions.json
M AGENTS.md
M CLAUDE.md
M CUSTOMIZATION.md
M Dockerfile
M INSTALLATION.md
M Makefile
M README.md
M SECURITY.md
M agent/analyzer.py
M agent/chat.py
M agent/ci_autofix.py
A agent/completion.py
M agent/dashboard/__init__.py
M agent/dashboard/agent_overrides.py
M agent/dashboard/eval_jobs.py
M agent/dashboard/oauth.py
M agent/dashboard/options.py
M agent/dashboard/plan_api.py
M agent/dashboard/plan_store.py
M agent/dashboard/repo_access.py
M agent/dashboard/review_api.py
M agent/dashboard/review_chat_api.py
M agent/dashboard/routes.py
M agent/dashboard/schedules.py
M agent/dashboard/slack_oauth.py
M agent/dashboard/team_settings.py
M agent/dashboard/thread_api.py
M agent/dashboard/user_mappings.py
A agent/dashboard/workflow_approval.py
A agent/dashboard/workflow_approval_api.py
A agent/dispatch.py
A agent/integrations/e2b.py
M agent/integrations/local.py
M agent/middleware/__init__.py
M agent/middleware/check_message_queue.py
A agent/middleware/ensure_no_empty_msg.py
M agent/middleware/model_fallback.py
M agent/middleware/plan_mode.py
A agent/middleware/pr_creation_guard.py
M agent/middleware/refresh_slack_status.py
A agent/middleware/sanitize_fireworks_messages.py
A agent/middleware/sanitize_openai_responses.py
M agent/middleware/sanitize_thinking_blocks.py
A agent/middleware/subdir_agents.py
A agent/middleware/task_retry.py
A agent/middleware/timeout_wrapup.py
A agent/middleware/workflow_push_guard.py
M agent/prompt.py
A agent/reconcile.py
M agent/reviewer.py
M agent/reviewer_findings.py
M agent/reviewer_publish.py
M agent/reviewer_reconcile.py
A agent/reviewer_trace_context.py
M agent/scheduler.py
M agent/server.py
M agent/tools/__init__.py
M agent/tools/add_finding.py
A agent/tools/confluence_comment.py
A agent/tools/confluence_create_page.py
A agent/tools/confluence_get_page.py
A agent/tools/confluence_search.py
A agent/tools/confluence_update_page.py
M agent/tools/enter_plan_mode.py
M agent/tools/fetch_url.py
M agent/tools/http_request.py
A agent/tools/jira_comment.py
A agent/tools/jira_create_issue.py
A agent/tools/jira_get_issue.py
A agent/tools/jira_get_issue_comments.py
A agent/tools/jira_list_projects.py
A agent/tools/jira_update_issue.py
M agent/tools/linear_comment.py
M agent/tools/linear_create_issue.py
M agent/tools/linear_delete_issue.py
M agent/tools/linear_get_issue.py
M agent/tools/linear_get_issue_comments.py
M agent/tools/linear_list_teams.py
M agent/tools/linear_update_issue.py
M agent/tools/list_findings.py
M agent/tools/list_review_findings.py
M agent/tools/open_pull_request.py
M agent/tools/publish_review.py
M agent/tools/read_repo_file.py
M agent/tools/reply_to_finding_thread.py
A agent/tools/report_platform_issue.py
M agent/tools/request_pr_review.py
M agent/tools/resolve_finding_thread.py
M agent/tools/save_plan.py
M agent/tools/save_review_style.py
M agent/tools/schedule_thread_wakeup.py
M agent/tools/search_repo_code.py
A agent/tools/slack_add_reaction.py
M agent/tools/slack_read_thread_messages.py
A agent/tools/slack_start_new_thread.py
M agent/tools/slack_thread_reply.py
M agent/tools/update_finding.py
M agent/tools/web_search.py
A agent/utils/adf.py
M agent/utils/analyzer_skills.py
A agent/utils/atlassian_connect.py
M agent/utils/auth.py
M agent/utils/authorship.py
A agent/utils/confluence.py
A agent/utils/confluence_space_repo_map.py
A agent/utils/dashboard_handoff.py
M agent/utils/dashboard_links.py
A agent/utils/deferred_model.py
A agent/utils/gateway.py
M agent/utils/github_app.py
M agent/utils/github_comments.py
M agent/utils/github_org_membership.py
M agent/utils/github_proxy.py
M agent/utils/github_token.py
A agent/utils/http.py
A agent/utils/jira.py
A agent/utils/jira_project_repo_map.py
M agent/utils/linear.py
M agent/utils/model.py
M agent/utils/multimodal.py
M agent/utils/sandbox.py
M agent/utils/sandbox_state.py
M agent/utils/slack.py
A agent/utils/thread_ids.py
M agent/utils/thread_ops.py
A agent/utils/url_safety.py
M agent/webapp.py
A agent/webhooks/__init__.py
A agent/webhooks/confluence.py
A agent/webhooks/github.py
A agent/webhooks/jira.py
A agent/webhooks/linear.py
A agent/webhooks/slack.py
M default_prompt.md
A deploy/MIGRATION.md
A docs/repo-conventions/AGENTS.md.aws
A docs/repo-conventions/AGENTS.md.cdk
A docs/repo-conventions/AGENTS.md.ec2
A docs/repo-conventions/AGENTS.md.sam
A docs/repo-conventions/README.md
A docs/upstream-sync/cherry-pick-hook-plan.md
A docs/upstream-sync/cherry-pick-runbook.md
A docs/upstream-sync/triage.jsonl
A docs/upstream-sync/triage.md
M evals/reviewer/README.md
M evals/reviewer/config.toml
M evals/reviewer/judge.py
M evals/reviewer/run_eval.py
M evals/reviewer/target.py
M pyproject.toml
A scripts/git-cp
A scripts/install-hooks.sh
A scripts/purge_wakeup_crons.py
A scripts/triage.py
M tests/conftest.py
M tests/e2e/README.md
M tests/e2e/fake_llm.py
M tests/e2e/fakes.py
M tests/e2e/harness.py
M tests/e2e/package-lock.json
M tests/e2e/package.json
M tests/e2e/patches.py
A tests/e2e/screenshots/queued-messages-dashboard.png
M tests/e2e/static/slack.html
M tests/e2e/tests/dashboard.spec.ts
M tests/e2e/tests/full_flow.spec.ts
M tests/e2e/tests/plan_review.spec.ts
A tests/e2e/tests/sandbox_id.spec.ts
A tests/test_agent_assembly_context.py
M tests/test_agent_schedules.py
M tests/test_agent_subagent_models.py
A tests/test_anthropic_model.py
A tests/test_app_bot_identity.py
A tests/test_atlassian_connect.py
A tests/test_auth_error_leak.py
M tests/test_auth_sources.py
M tests/test_authorship.py
M tests/test_autofix_webhook.py
M tests/test_ci_autofix.py
A tests/test_completion_webhook.py
A tests/test_confluence_utils.py
A tests/test_confluence_webhook.py
M tests/test_corridor_mcp.py
A tests/test_dashboard_oauth_redirect.py
M tests/test_dashboard_thread_api.py
M tests/test_dashboard_web_handoff.py
A tests/test_dispatch.py
A tests/test_e2b_integration.py
A tests/test_ensure_no_empty_msg.py
M tests/test_fireworks_model.py
A tests/test_gateway.py
M tests/test_github_app.py
M tests/test_github_comment_prompts.py
M tests/test_github_issue_webhook.py
M tests/test_github_proxy_refresh.py
M tests/test_github_token_ttl.py
D tests/test_google_model.py
M tests/test_http_security.py
A tests/test_jira_utils.py
A tests/test_jira_webhook_author.py
A tests/test_jira_webhook_corroboration.py
A tests/test_jira_webhook_replay.py
A tests/test_linear_webhook_author.py
A tests/test_linear_webhook_replay.py
M tests/test_local_integration.py
M tests/test_model_fallback_middleware.py
M tests/test_model_fallback_resolution.py
M tests/test_open_pull_request.py
M tests/test_plan_mode.py
M tests/test_plan_review.py
A tests/test_pr_creation_guard.py
M tests/test_pr_ready_auto_review.py
M tests/test_proxy_auth.py
A tests/test_reconcile_sweep.py
A tests/test_repo_binding_isolation.py
A tests/test_report_platform_issue_tool.py
M tests/test_review_chat.py
M tests/test_reviewer.py
A tests/test_reviewer_eval_judge.py
M tests/test_reviewer_eval_target.py
M tests/test_reviewer_findings.py
M tests/test_reviewer_publish.py
M tests/test_reviewer_reconcile.py
M tests/test_reviewer_tools.py
A tests/test_reviewer_trace_context.py
M tests/test_reviewer_watch.py
A tests/test_sanitize_fireworks_messages.py
A tests/test_sanitize_openai_responses.py
M tests/test_sanitize_thinking_blocks.py
M tests/test_schedule_thread_wakeup.py
A tests/test_slack_add_reaction_tool.py
M tests/test_slack_assistants_status.py
M tests/test_slack_context.py
A tests/test_slack_start_new_thread_tool.py
M tests/test_slack_thread_reply_tool.py
A tests/test_slack_webhook_errors.py
A tests/test_subdir_agents_middleware.py
A tests/test_team_settings_fable.py
M tests/test_team_settings_grouping.py
M tests/test_team_settings_org_guidelines.py
A tests/test_workflow_push_guard.py
A ui/AGENTS.md
M ui/bun.lock
M ui/package.json
A ui/pnpm-workspace.yaml
M ui/src/client.tsx
A ui/src/components/PwaUpdateToast.tsx
M ui/src/components/agents/AgentGitPanel.tsx
M ui/src/components/agents/AgentThreadView.tsx
M ui/src/components/agents/AgentsSidebar.tsx
M ui/src/components/agents/AutomationEditor.tsx
M ui/src/components/agents/PlanReview.tsx
A ui/src/components/agents/PrHeader.tsx
M ui/src/components/agents/ReviewMainBody.tsx
M ui/src/components/agents/ReviewSidebar.tsx
A ui/src/components/agents/SidebarFilterMenu.tsx
A ui/src/components/agents/WorkflowApprovalCard.tsx
M ui/src/components/agents/messages/AgentMessage.tsx
A ui/src/components/agents/messages/MessageTimestamp.tsx
M ui/src/components/agents/messages/Messages.tsx
M ui/src/components/agents/messages/UserMessage.tsx
M ui/src/components/agents/messages/types.ts
M ui/src/components/agents/utils/diffUtils.ts
M ui/src/lib/agents/api.ts
A ui/src/lib/agents/messageTimestamps.ts
M ui/src/lib/agents/provider/useSubmitAgentMessage.ts
M ui/src/lib/agents/queries.ts
A ui/src/lib/agents/sidebarFilter.test.ts
A ui/src/lib/agents/sidebarFilter.ts
A ui/src/lib/agents/sidebarPrefs.ts
M ui/src/lib/agents/streamMessagesToUi.ts
M ui/src/lib/agents/types.ts
M ui/src/lib/api.ts
A ui/src/lib/auth-redirect-core.ts
A ui/src/lib/auth-redirect.test.ts
A ui/src/lib/auth-redirect.tsx
M ui/src/lib/plan.ts
M ui/src/routeTree.gen.ts
A ui/src/routes/$owner.$repo.pull.$number.tsx
M ui/src/routes/admin.tsx
M ui/src/routes/admin_.evals.tsx
M ui/src/routes/agents.tsx
M ui/src/routes/agents/$threadId_.plan.tsx
M ui/src/routes/agents/reviews/$owner.$repo.$number.tsx
M ui/src/routes/agents_.instructions.tsx
M ui/src/routes/agents_.snapshots.tsx
M ui/src/routes/cloud-agents.tsx
M ui/src/routes/login.tsx
M ui/src/routes/my-settings.tsx
M ui/src/routes/review.tsx
M ui/src/routes/review_.repositories.$owner.tsx
M ui/src/routes/review_.styles.tsx
M ui/src/routes/usage.tsx
M ui/vercel.json
M ui/vite.config.ts
D ui/yarn.lock
M uv.lock

View file

@ -0,0 +1 @@
{"true_fork_only": [".env.example", ".githooks/_reject_guard.sh", ".githooks/commit-msg", ".githooks/post-commit", ".githooks/pre-push", ".githooks/prepare-commit-msg", ".github/CODEOWNERS", ".github/ISSUE_TEMPLATE/bug.yml", ".github/ISSUE_TEMPLATE/config.yml", ".github/ISSUE_TEMPLATE/feature.yml", ".github/ISSUE_TEMPLATE/task.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/scripts/check-dev-green.sh", ".github/workflows/dependency-review.yml", ".github/workflows/labeler.yml", ".github/workflows/promote-to-main.yml", ".github/workflows/reviewer-eval.yml", ".github/workflows/upstream-ledger-sync.yml", ".nvmrc", ".security-review/suppressions.json", "agent/tools/confluence_comment.py", "agent/tools/confluence_create_page.py", "agent/tools/confluence_get_page.py", "agent/tools/confluence_search.py", "agent/tools/confluence_update_page.py", "agent/tools/jira_comment.py", "agent/tools/jira_create_issue.py", "agent/tools/jira_get_issue.py", "agent/tools/jira_get_issue_comments.py", "agent/tools/jira_list_projects.py", "agent/tools/jira_update_issue.py", "agent/utils/adf.py", "agent/utils/atlassian_connect.py", "agent/utils/confluence.py", "agent/utils/confluence_space_repo_map.py", "agent/utils/jira.py", "agent/utils/jira_project_repo_map.py", "agent/webhooks/confluence.py", "agent/webhooks/jira.py", "deploy/MIGRATION.md", "docs/repo-conventions/AGENTS.md.aws", "docs/repo-conventions/AGENTS.md.cdk", "docs/repo-conventions/AGENTS.md.ec2", "docs/repo-conventions/AGENTS.md.sam", "docs/repo-conventions/README.md", "docs/upstream-sync/cherry-pick-hook-plan.md", "docs/upstream-sync/cherry-pick-runbook.md", "docs/upstream-sync/triage.jsonl", "docs/upstream-sync/triage.md", "scripts/git-cp", "scripts/install-hooks.sh", "scripts/triage.py", "tests/test_app_bot_identity.py", "tests/test_atlassian_connect.py", "tests/test_auth_error_leak.py", "tests/test_confluence_utils.py", "tests/test_confluence_webhook.py", "tests/test_jira_utils.py", "tests/test_jira_webhook_author.py", "tests/test_jira_webhook_corroboration.py", "tests/test_jira_webhook_replay.py", "tests/test_linear_webhook_replay.py", "tests/test_repo_binding_isolation.py", "ui/src/components/PwaUpdateToast.tsx"], "shared_pre": ["agent/completion.py", "agent/dashboard/workflow_approval.py", "agent/dashboard/workflow_approval_api.py", "agent/dispatch.py", "agent/integrations/e2b.py", "agent/middleware/ensure_no_empty_msg.py", "agent/middleware/pr_creation_guard.py", "agent/middleware/sanitize_fireworks_messages.py", "agent/middleware/sanitize_openai_responses.py", "agent/middleware/subdir_agents.py", "agent/middleware/task_retry.py", "agent/middleware/timeout_wrapup.py", "agent/middleware/workflow_push_guard.py", "agent/reconcile.py", "agent/reviewer_trace_context.py", "agent/tools/report_platform_issue.py", "agent/tools/slack_add_reaction.py", "agent/tools/slack_start_new_thread.py", "agent/utils/dashboard_handoff.py", "agent/utils/deferred_model.py", "agent/utils/gateway.py", "agent/utils/http.py", "agent/utils/thread_ids.py", "agent/utils/url_safety.py", "agent/webhooks/__init__.py", "agent/webhooks/github.py", "agent/webhooks/linear.py", "agent/webhooks/slack.py", "scripts/purge_wakeup_crons.py", "tests/e2e/screenshots/queued-messages-dashboard.png", "tests/e2e/tests/sandbox_id.spec.ts", "tests/test_agent_assembly_context.py", "tests/test_anthropic_model.py", "tests/test_completion_webhook.py", "tests/test_dashboard_oauth_redirect.py", "tests/test_dispatch.py", "tests/test_e2b_integration.py", "tests/test_ensure_no_empty_msg.py", "tests/test_gateway.py", "tests/test_linear_webhook_author.py", "tests/test_pr_creation_guard.py", "tests/test_reconcile_sweep.py", "tests/test_report_platform_issue_tool.py", "tests/test_reviewer_eval_judge.py", "tests/test_reviewer_trace_context.py", "tests/test_sanitize_fireworks_messages.py", "tests/test_sanitize_openai_responses.py", "tests/test_slack_add_reaction_tool.py", "tests/test_slack_start_new_thread_tool.py", "tests/test_slack_webhook_errors.py", "tests/test_subdir_agents_middleware.py", "tests/test_team_settings_fable.py", "tests/test_workflow_push_guard.py", "ui/AGENTS.md", "ui/pnpm-workspace.yaml", "ui/src/components/agents/PrHeader.tsx", "ui/src/components/agents/SidebarFilterMenu.tsx", "ui/src/components/agents/WorkflowApprovalCard.tsx", "ui/src/components/agents/messages/MessageTimestamp.tsx", "ui/src/lib/agents/messageTimestamps.ts", "ui/src/lib/agents/sidebarFilter.test.ts", "ui/src/lib/agents/sidebarFilter.ts", "ui/src/lib/agents/sidebarPrefs.ts", "ui/src/lib/auth-redirect-core.ts", "ui/src/lib/auth-redirect.test.ts", "ui/src/lib/auth-redirect.tsx", "ui/src/routes/$owner.$repo.pull.$number.tsx"], "shared_post": []}

View file

@ -0,0 +1,589 @@
.codespellignore
.dockerignore
.env.example
.githooks/_reject_guard.sh
.githooks/commit-msg
.githooks/post-commit
.githooks/pre-push
.githooks/prepare-commit-msg
.github/CODEOWNERS
.github/ISSUE_TEMPLATE/bug.yml
.github/ISSUE_TEMPLATE/config.yml
.github/ISSUE_TEMPLATE/feature.yml
.github/ISSUE_TEMPLATE/task.yml
.github/PULL_REQUEST_TEMPLATE.md
.github/dependabot.yml
.github/scripts/check-dev-green.sh
.github/workflows/ci.yml
.github/workflows/dependency-review.yml
.github/workflows/labeler.yml
.github/workflows/pr_lint.yml
.github/workflows/promote-to-main.yml
.github/workflows/reviewer-eval.yml
.github/workflows/upstream-ledger-sync.yml
.gitignore
.nvmrc
.security-review/suppressions.json
.vscode/settings.json
AGENTS.md
CLAUDE.md
CUSTOMIZATION.md
Dockerfile
INSTALLATION.md
LICENSE
Makefile
README.md
SECURITY.md
agent/analyzer.py
agent/chat.py
agent/ci_autofix.py
agent/ci_monitor.py
agent/completion.py
agent/dashboard/__init__.py
agent/dashboard/admin.py
agent/dashboard/agent_instructions.py
agent/dashboard/agent_overrides.py
agent/dashboard/agent_usage.py
agent/dashboard/analyzer_cron.py
agent/dashboard/autofix_state.py
agent/dashboard/enabled_repos.py
agent/dashboard/eval_jobs.py
agent/dashboard/notion_oauth.py
agent/dashboard/oauth.py
agent/dashboard/options.py
agent/dashboard/plan_api.py
agent/dashboard/plan_store.py
agent/dashboard/pr_diff.py
agent/dashboard/profiles.py
agent/dashboard/repo_access.py
agent/dashboard/repo_snapshots.py
agent/dashboard/review_api.py
agent/dashboard/review_chat_api.py
agent/dashboard/review_style_jobs.py
agent/dashboard/review_styles.py
agent/dashboard/routes.py
agent/dashboard/schedules.py
agent/dashboard/slack_oauth.py
agent/dashboard/team_credentials.py
agent/dashboard/team_settings.py
agent/dashboard/thread_api.py
agent/dashboard/user_credentials.py
agent/dashboard/user_mappings.py
agent/dashboard/workflow_approval.py
agent/dashboard/workflow_approval_api.py
agent/dispatch.py
agent/encryption.py
agent/integrations/__init__.py
agent/integrations/corridor_mcp.py
agent/integrations/currents_tools.py
agent/integrations/datadog_mcp.py
agent/integrations/daytona.py
agent/integrations/e2b.py
agent/integrations/langsmith.py
agent/integrations/langsmith_tools.py
agent/integrations/local.py
agent/integrations/modal.py
agent/integrations/notion_mcp.py
agent/integrations/runloop.py
agent/middleware/__init__.py
agent/middleware/check_message_queue.py
agent/middleware/ensure_no_empty_msg.py
agent/middleware/exclude_tools.py
agent/middleware/model_fallback.py
agent/middleware/notify_step_limit.py
agent/middleware/plan_mode.py
agent/middleware/pr_creation_guard.py
agent/middleware/refresh_github_proxy.py
agent/middleware/refresh_slack_status.py
agent/middleware/repair_orphaned_tool_calls.py
agent/middleware/sandbox_circuit_breaker.py
agent/middleware/sanitize_fireworks_messages.py
agent/middleware/sanitize_openai_responses.py
agent/middleware/sanitize_thinking_blocks.py
agent/middleware/sanitize_tool_inputs.py
agent/middleware/settle_review_check.py
agent/middleware/subdir_agents.py
agent/middleware/task_retry.py
agent/middleware/timeout_wrapup.py
agent/middleware/tool_artifact.py
agent/middleware/tool_error_handler.py
agent/middleware/workflow_push_guard.py
agent/prompt.py
agent/reconcile.py
agent/review_style_collector.py
agent/review_style_guidance.py
agent/reviewer.py
agent/reviewer_diff.py
agent/reviewer_eval_store.py
agent/reviewer_findings.py
agent/reviewer_groups.py
agent/reviewer_publish.py
agent/reviewer_reconcile.py
agent/reviewer_trace_context.py
agent/scheduler.py
agent/server.py
agent/skills/bootstrap-repo-analysis/SKILL.md
agent/skills/continual-learning/SKILL.md
agent/tools/__init__.py
agent/tools/add_finding.py
agent/tools/confluence_comment.py
agent/tools/confluence_create_page.py
agent/tools/confluence_get_page.py
agent/tools/confluence_search.py
agent/tools/confluence_update_page.py
agent/tools/enter_plan_mode.py
agent/tools/fetch_url.py
agent/tools/http_request.py
agent/tools/jira_comment.py
agent/tools/jira_create_issue.py
agent/tools/jira_get_issue.py
agent/tools/jira_get_issue_comments.py
agent/tools/jira_list_projects.py
agent/tools/jira_update_issue.py
agent/tools/linear_comment.py
agent/tools/linear_create_issue.py
agent/tools/linear_delete_issue.py
agent/tools/linear_get_issue.py
agent/tools/linear_get_issue_comments.py
agent/tools/linear_list_teams.py
agent/tools/linear_update_issue.py
agent/tools/list_findings.py
agent/tools/list_review_findings.py
agent/tools/open_pull_request.py
agent/tools/publish_review.py
agent/tools/read_finding_outcomes.py
agent/tools/read_repo_file.py
agent/tools/reply_to_finding_thread.py
agent/tools/report_platform_issue.py
agent/tools/request_pr_review.py
agent/tools/resolve_finding_thread.py
agent/tools/save_plan.py
agent/tools/save_review_style.py
agent/tools/schedule_thread_wakeup.py
agent/tools/search_repo_code.py
agent/tools/slack_add_reaction.py
agent/tools/slack_read_thread_messages.py
agent/tools/slack_start_new_thread.py
agent/tools/slack_thread_reply.py
agent/tools/update_finding.py
agent/tools/web_search.py
agent/utils/adf.py
agent/utils/agents_md.py
agent/utils/analyzer_skills.py
agent/utils/api_standards_skill.py
agent/utils/atlassian_connect.py
agent/utils/auth.py
agent/utils/authorship.py
agent/utils/comments.py
agent/utils/confluence.py
agent/utils/confluence_space_repo_map.py
agent/utils/dashboard_handoff.py
agent/utils/dashboard_links.py
agent/utils/deferred_model.py
agent/utils/gateway.py
agent/utils/github_app.py
agent/utils/github_checks.py
agent/utils/github_ci.py
agent/utils/github_comments.py
agent/utils/github_feedback.py
agent/utils/github_http.py
agent/utils/github_org_membership.py
agent/utils/github_proxy.py
agent/utils/github_token.py
agent/utils/http.py
agent/utils/jira.py
agent/utils/jira_project_repo_map.py
agent/utils/langsmith.py
agent/utils/linear.py
agent/utils/linear_team_repo_map.py
agent/utils/model.py
agent/utils/multimodal.py
agent/utils/repo.py
agent/utils/repo_prep.py
agent/utils/reviewer_outcomes.py
agent/utils/sandbox.py
agent/utils/sandbox_paths.py
agent/utils/sandbox_state.py
agent/utils/slack.py
agent/utils/slack_feedback.py
agent/utils/thread_ids.py
agent/utils/thread_ops.py
agent/utils/tracing.py
agent/utils/url_safety.py
agent/webapp.py
agent/webhooks/__init__.py
agent/webhooks/confluence.py
agent/webhooks/github.py
agent/webhooks/jira.py
agent/webhooks/linear.py
agent/webhooks/slack.py
default_prompt.md
deploy/MIGRATION.md
docs/repo-conventions/AGENTS.md.aws
docs/repo-conventions/AGENTS.md.cdk
docs/repo-conventions/AGENTS.md.ec2
docs/repo-conventions/AGENTS.md.sam
docs/repo-conventions/README.md
docs/upstream-sync/cherry-pick-hook-plan.md
docs/upstream-sync/cherry-pick-runbook.md
docs/upstream-sync/triage.jsonl
docs/upstream-sync/triage.md
evals/reviewer/README.md
evals/reviewer/build_dataset.py
evals/reviewer/config.toml
evals/reviewer/golden_comments/cal_dot_com.json
evals/reviewer/golden_comments/discourse.json
evals/reviewer/golden_comments/grafana.json
evals/reviewer/golden_comments/keycloak.json
evals/reviewer/golden_comments/sentry.json
evals/reviewer/judge.py
evals/reviewer/run_eval.py
evals/reviewer/store_reporter.py
evals/reviewer/target.py
langgraph.json
package.json
pyproject.toml
scripts/__init__.py
scripts/check_pr_merge_status.py
scripts/create_sandbox_snapshot.py
scripts/git-cp
scripts/install-hooks.sh
scripts/list_snapshots.py
scripts/purge_wakeup_crons.py
scripts/triage.py
static/dark.svg
static/light.svg
tests/conftest.py
tests/e2e/.gitignore
tests/e2e/README.md
tests/e2e/agent_entrypoint.py
tests/e2e/dev-mock.sh
tests/e2e/e2e_env.py
tests/e2e/fake_llm.py
tests/e2e/fakes.py
tests/e2e/global-setup.ts
tests/e2e/harness.py
tests/e2e/langgraph.e2e.json
tests/e2e/package-lock.json
tests/e2e/package.json
tests/e2e/patches.py
tests/e2e/playwright.config.ts
tests/e2e/screenshots/queued-messages-dashboard.png
tests/e2e/static/github.html
tests/e2e/static/slack.html
tests/e2e/tests/dashboard.spec.ts
tests/e2e/tests/full_flow.spec.ts
tests/e2e/tests/plan_review.spec.ts
tests/e2e/tests/sandbox_id.spec.ts
tests/middleware/test_sandbox_recovery.py
tests/test_account_link.py
tests/test_agent_assembly_context.py
tests/test_agent_instructions.py
tests/test_agent_schedules.py
tests/test_agent_subagent_models.py
tests/test_agent_thread_pr_state.py
tests/test_agent_usage.py
tests/test_agents_md.py
tests/test_analyzer_cron.py
tests/test_analyzer_skills.py
tests/test_anthropic_effort.py
tests/test_anthropic_model.py
tests/test_api_standards_skill.py
tests/test_app_bot_identity.py
tests/test_atlassian_connect.py
tests/test_auth_error_leak.py
tests/test_auth_sources.py
tests/test_authorship.py
tests/test_autofix_state.py
tests/test_autofix_webhook.py
tests/test_check_message_queue.py
tests/test_ci_autofix.py
tests/test_completion_webhook.py
tests/test_confluence_utils.py
tests/test_confluence_webhook.py
tests/test_corridor_mcp.py
tests/test_currents_tools.py
tests/test_dashboard_admin.py
tests/test_dashboard_csrf.py
tests/test_dashboard_links.py
tests/test_dashboard_oauth_redirect.py
tests/test_dashboard_org_login_gate.py
tests/test_dashboard_repo_optional.py
tests/test_dashboard_repos.py
tests/test_dashboard_reviews.py
tests/test_dashboard_run_email.py
tests/test_dashboard_thread_api.py
tests/test_dashboard_thread_api_activity.py
tests/test_dashboard_web_handoff.py
tests/test_daytona_integration.py
tests/test_dispatch.py
tests/test_e2b_integration.py
tests/test_encryption.py
tests/test_ensure_no_empty_msg.py
tests/test_eval_jobs.py
tests/test_eval_store_reporter.py
tests/test_fireworks_model.py
tests/test_gateway.py
tests/test_github_app.py
tests/test_github_checks.py
tests/test_github_ci.py
tests/test_github_comment_prompts.py
tests/test_github_feedback.py
tests/test_github_http.py
tests/test_github_issue_webhook.py
tests/test_github_oauth_refresh.py
tests/test_github_proxy_refresh.py
tests/test_github_token_ttl.py
tests/test_http_security.py
tests/test_jira_utils.py
tests/test_jira_webhook_author.py
tests/test_jira_webhook_corroboration.py
tests/test_jira_webhook_replay.py
tests/test_langsmith_sandbox_config.py
tests/test_langsmith_sandbox_timeout.py
tests/test_langsmith_trace_url.py
tests/test_linear_webhook_author.py
tests/test_linear_webhook_replay.py
tests/test_local_integration.py
tests/test_model_fallback_middleware.py
tests/test_model_fallback_resolution.py
tests/test_multimodal.py
tests/test_normalize_repo.py
tests/test_notify_step_limit_middleware.py
tests/test_notion_oauth.py
tests/test_observability_tools.py
tests/test_open_pull_request.py
tests/test_plan_mode.py
tests/test_plan_review.py
tests/test_pr_creation_guard.py
tests/test_pr_ready_auto_review.py
tests/test_prompt_default_repo.py
tests/test_proxy_auth.py
tests/test_public_repo_org_gate.py
tests/test_recent_comments.py
tests/test_reconcile_sweep.py
tests/test_refresh_slack_status_middleware.py
tests/test_repair_orphaned_tool_calls.py
tests/test_repo_binding_isolation.py
tests/test_repo_extraction.py
tests/test_repo_prep.py
tests/test_repo_snapshots.py
tests/test_report_platform_issue_tool.py
tests/test_review_api.py
tests/test_review_chat.py
tests/test_review_style_collector.py
tests/test_review_style_sync.py
tests/test_review_styles_store.py
tests/test_reviewer.py
tests/test_reviewer_diff.py
tests/test_reviewer_eval_judge.py
tests/test_reviewer_eval_run.py
tests/test_reviewer_eval_target.py
tests/test_reviewer_findings.py
tests/test_reviewer_groups.py
tests/test_reviewer_outcomes.py
tests/test_reviewer_publish.py
tests/test_reviewer_reconcile.py
tests/test_reviewer_tools.py
tests/test_reviewer_trace_context.py
tests/test_reviewer_watch.py
tests/test_sandbox_paths.py
tests/test_sanitize_fireworks_messages.py
tests/test_sanitize_openai_responses.py
tests/test_sanitize_thinking_blocks.py
tests/test_sanitize_tool_inputs.py
tests/test_schedule_thread_wakeup.py
tests/test_slack_add_reaction_tool.py
tests/test_slack_assistants_status.py
tests/test_slack_context.py
tests/test_slack_feedback.py
tests/test_slack_oauth.py
tests/test_slack_start_new_thread_tool.py
tests/test_slack_thread_reply_tool.py
tests/test_slack_webhook_errors.py
tests/test_stale_sandbox_creating.py
tests/test_subdir_agents_middleware.py
tests/test_team_credentials.py
tests/test_team_settings_fable.py
tests/test_team_settings_grouping.py
tests/test_team_settings_org_guidelines.py
tests/test_tool_artifact_middleware.py
tests/test_user_credentials.py
tests/test_user_mappings.py
tests/test_workflow_push_guard.py
ui/.cta.json
ui/.gitignore
ui/.prettierignore
ui/.prettierrc
ui/AGENTS.md
ui/README.md
ui/assets/LangChain_Symbol_LightBlue.png
ui/assets/ic_launcher_round.png
ui/assets/playstore-icon.png
ui/bun.lock
ui/components.json
ui/eslint.config.js
ui/package.json
ui/pnpm-workspace.yaml
ui/public/apple-touch-icon.png
ui/public/favicon.png
ui/public/gh-open-in-open-swe-dark.svg
ui/public/gh-open-in-open-swe-light.svg
ui/public/logo-mark.png
ui/public/logo512.png
ui/public/robots.txt
ui/src/client.tsx
ui/src/components/AgentInstructionsPanel.tsx
ui/src/components/AppShell.tsx
ui/src/components/AppSidebar.tsx
ui/src/components/InstructionsEditor.tsx
ui/src/components/PwaUpdateToast.tsx
ui/src/components/RepoSnapshotsPanel.tsx
ui/src/components/ReviewStylesPanel.tsx
ui/src/components/SidebarUserMenu.tsx
ui/src/components/agents/AgentGitPanel.tsx
ui/src/components/agents/AgentPromptBar.tsx
ui/src/components/agents/AgentRunCard.tsx
ui/src/components/agents/AgentThreadView.tsx
ui/src/components/agents/AgentsHome.tsx
ui/src/components/agents/AgentsSidebar.tsx
ui/src/components/agents/AgentsThreadsPage.tsx
ui/src/components/agents/AutomationEditor.tsx
ui/src/components/agents/AutomationTemplates.tsx
ui/src/components/agents/AutomationsList.tsx
ui/src/components/agents/OnboardingDialog.tsx
ui/src/components/agents/PlanReview.tsx
ui/src/components/agents/PrHeader.tsx
ui/src/components/agents/RepoSelector.tsx
ui/src/components/agents/ReviewChat.tsx
ui/src/components/agents/ReviewCommentsMenu.tsx
ui/src/components/agents/ReviewMainBody.tsx
ui/src/components/agents/ReviewSidebar.tsx
ui/src/components/agents/ReviewTab.tsx
ui/src/components/agents/ScheduleTriggerPicker.tsx
ui/src/components/agents/SidebarFilterMenu.tsx
ui/src/components/agents/WorkflowApprovalCard.tsx
ui/src/components/agents/messages/AgentMessage.tsx
ui/src/components/agents/messages/ChunkRenderer.tsx
ui/src/components/agents/messages/MessageTimestamp.tsx
ui/src/components/agents/messages/Messages.tsx
ui/src/components/agents/messages/ReasoningBlock.tsx
ui/src/components/agents/messages/ThinkingSpinner.tsx
ui/src/components/agents/messages/TurnChangedFilesCard.tsx
ui/src/components/agents/messages/UserMessage.tsx
ui/src/components/agents/messages/WorkSummary.tsx
ui/src/components/agents/messages/index.ts
ui/src/components/agents/messages/renderItems.ts
ui/src/components/agents/messages/summarizeChangedFiles.ts
ui/src/components/agents/messages/types.ts
ui/src/components/agents/ported/BranchSelector.tsx
ui/src/components/agents/ported/BubblePrefix.tsx
ui/src/components/agents/ported/ChatView.tsx
ui/src/components/agents/ported/CloudPromptBar.tsx
ui/src/components/agents/ported/CodeBlock.tsx
ui/src/components/agents/ported/CompactingIndicator.tsx
ui/src/components/agents/ported/ContextIndicator.tsx
ui/src/components/agents/ported/DiffView.tsx
ui/src/components/agents/ported/Footer.tsx
ui/src/components/agents/ported/HeaderBar.tsx
ui/src/components/agents/ported/Logo.tsx
ui/src/components/agents/ported/Markdown.tsx
ui/src/components/agents/ported/MarkdownTable.tsx
ui/src/components/agents/ported/PanelResizeHandle.tsx
ui/src/components/agents/ported/PromptBar.tsx
ui/src/components/agents/ported/ReplyCard.tsx
ui/src/components/agents/ported/ShellCommand.tsx
ui/src/components/agents/ported/SourceControlPanel.tsx
ui/src/components/agents/ported/SourceControlTile.tsx
ui/src/components/agents/ported/ThreadPicker.tsx
ui/src/components/agents/ported/TodoList.tsx
ui/src/components/agents/ported/ToolExecution.tsx
ui/src/components/agents/ported/index.ts
ui/src/components/agents/subagents/SubagentActivity.tsx
ui/src/components/agents/subagents/SubagentCard.tsx
ui/src/components/agents/subagents/SubagentGroup.tsx
ui/src/components/agents/subagents/index.ts
ui/src/components/agents/utils/diffStats.ts
ui/src/components/agents/utils/diffUtils.test.ts
ui/src/components/agents/utils/diffUtils.ts
ui/src/components/agents/z-index.ts
ui/src/components/sidebar-layout.tsx
ui/src/components/ui/avatar.tsx
ui/src/components/ui/badge.tsx
ui/src/components/ui/button.tsx
ui/src/components/ui/card.tsx
ui/src/components/ui/combobox.tsx
ui/src/components/ui/input-group.tsx
ui/src/components/ui/input.tsx
ui/src/components/ui/label.tsx
ui/src/components/ui/select.tsx
ui/src/components/ui/separator.tsx
ui/src/components/ui/skeleton.tsx
ui/src/components/ui/switch.tsx
ui/src/components/ui/textarea.tsx
ui/src/lib/agents/AgentThreadStreamProvider.tsx
ui/src/lib/agents/api.ts
ui/src/lib/agents/automation-templates.ts
ui/src/lib/agents/cron.ts
ui/src/lib/agents/messageTimestamps.ts
ui/src/lib/agents/provider/useIsInAgentThreadStream.tsx
ui/src/lib/agents/provider/useLiveMarkdownMessageId.ts
ui/src/lib/agents/provider/useModelOptions.ts
ui/src/lib/agents/provider/useSubmitAgentMessage.ts
ui/src/lib/agents/queries.ts
ui/src/lib/agents/sidebarFilter.test.ts
ui/src/lib/agents/sidebarFilter.ts
ui/src/lib/agents/sidebarPrefs.ts
ui/src/lib/agents/streamMessagesToUi.ts
ui/src/lib/agents/types.ts
ui/src/lib/agents/useRunCompletionNotifier.ts
ui/src/lib/api.ts
ui/src/lib/auth-redirect-core.ts
ui/src/lib/auth-redirect.test.ts
ui/src/lib/auth-redirect.tsx
ui/src/lib/hotkeys.ts
ui/src/lib/notifications.ts
ui/src/lib/plan.ts
ui/src/lib/profile.ts
ui/src/lib/query.ts
ui/src/lib/repo.test.ts
ui/src/lib/repo.ts
ui/src/lib/reviewImage.test.ts
ui/src/lib/session.ts
ui/src/lib/theme.ts
ui/src/lib/useIsMobile.ts
ui/src/lib/utils.ts
ui/src/logo.svg
ui/src/routeTree.gen.ts
ui/src/router.tsx
ui/src/routes/$owner.$repo.pull.$number.tsx
ui/src/routes/__root.tsx
ui/src/routes/admin.tsx
ui/src/routes/admin_.evals.tsx
ui/src/routes/agents.tsx
ui/src/routes/agents/$threadId.tsx
ui/src/routes/agents/$threadId_.plan.tsx
ui/src/routes/agents/automations/$scheduleId.tsx
ui/src/routes/agents/automations/index.tsx
ui/src/routes/agents/automations/new.tsx
ui/src/routes/agents/index.tsx
ui/src/routes/agents/reviews/$owner.$repo.$number.tsx
ui/src/routes/agents/reviews/index.tsx
ui/src/routes/agents/threads.tsx
ui/src/routes/agents_.instructions.tsx
ui/src/routes/agents_.snapshots.tsx
ui/src/routes/cloud-agents.tsx
ui/src/routes/index.tsx
ui/src/routes/integrations.tsx
ui/src/routes/login.tsx
ui/src/routes/my-settings.tsx
ui/src/routes/review.tsx
ui/src/routes/review_.repositories.$owner.tsx
ui/src/routes/review_.styles.tsx
ui/src/routes/usage.tsx
ui/src/styles.css
ui/src/styles/agents.css
ui/tsconfig.json
ui/vercel.json
ui/vite.config.ts
uv.lock

View file

@ -0,0 +1,435 @@
2e93de8920fab3ba052901838106fa250708e1f0
.github/workflows/ci.yml
.github/workflows/promote_main_to_prod.yml
.github/workflows/reviewer-eval.yml
AGENTS.md
CLAUDE.md
Dockerfile
Makefile
README.md
agent/analyzer.py
agent/chat.py
agent/ci_autofix.py
agent/completion.py
agent/dashboard/__init__.py
agent/dashboard/eval_jobs.py
agent/dashboard/options.py
agent/dashboard/plan_api.py
agent/dashboard/review_api.py
agent/dashboard/review_chat_api.py
agent/dashboard/schedules.py
agent/dashboard/team_settings.py
agent/dashboard/thread_api.py
agent/dashboard/workflow_approval.py
agent/dashboard/workflow_approval_api.py
agent/dispatch.py
agent/middleware/__init__.py
agent/middleware/ensure_no_empty_msg.py
agent/middleware/model_fallback.py
agent/middleware/pr_creation_guard.py
agent/middleware/sanitize_fireworks_messages.py
agent/middleware/subdir_agents.py
agent/middleware/timeout_wrapup.py
agent/middleware/workflow_push_guard.py
agent/prompt.py
agent/review/findings.py
agent/reviewer.py
agent/server.py
agent/tools/__init__.py
agent/tools/add_finding.py
agent/tools/fetch_url.py
agent/tools/http_request.py
agent/tools/open_pull_request.py
agent/tools/publish_review.py
agent/tools/request_pr_review.py
agent/tools/slack_start_new_thread.py
agent/tools/slack_thread_reply.py
agent/tools/update_finding.py
agent/tools/web_search.py
agent/utils/analyzer_skills.py
agent/utils/auth.py
agent/utils/deferred_model.py
agent/utils/gateway.py
agent/utils/github_app.py
agent/utils/github_proxy.py
agent/utils/github_token.py
agent/utils/model.py
agent/utils/multimodal.py
agent/utils/sandbox.py
agent/utils/sandbox_state.py
agent/utils/slack.py
agent/utils/url_safety.py
agent/webapp.py
agent/webhooks/github.py
agent/webhooks/linear.py
agent/webhooks/slack.py
docs/INSTALLATION.md
evals/reviewer/config.toml
evals/reviewer/judge.py
evals/reviewer/run_eval.py
evals/reviewer/target.py
pyproject.toml
tests/auth/test_auth_sources.py
tests/auth/test_github_token_ttl.py
tests/conftest.py
tests/dashboard/test_dashboard_thread_api.py
tests/dashboard/test_dashboard_web_handoff.py
tests/dashboard/test_team_settings_grouping.py
tests/dashboard/test_team_settings_org_guidelines.py
tests/e2e/fake_llm.py
tests/e2e/harness.py
tests/e2e/tests/dashboard.spec.ts
tests/e2e/tests/full_flow.spec.ts
tests/github/test_github_comment_prompts.py
tests/github/test_github_issue_webhook.py
tests/github/test_open_pull_request.py
tests/middleware/test_model_fallback_middleware.py
tests/middleware/test_sanitize_thinking_blocks.py
tests/models/test_agent_subagent_models.py
tests/models/test_google_model.py
tests/reviewer/test_pr_ready_auto_review.py
tests/reviewer/test_reviewer.py
tests/reviewer/test_reviewer_findings.py
tests/reviewer/test_reviewer_publish.py
tests/reviewer/test_reviewer_tools.py
tests/reviewer/test_reviewer_watch.py
tests/sandbox/test_local_integration.py
tests/sandbox/test_proxy_auth.py
tests/slack/test_slack_context.py
tests/slack/test_slack_thread_reply_tool.py
tests/test_autofix_webhook.py
tests/test_ci_autofix.py
tests/test_http_security.py
tests/test_model_fallback_resolution.py
tests/test_plan_review.py
ui/AGENTS.md
ui/bun.lock
ui/package.json
ui/src/client.tsx
ui/src/components/agents/PlanReview.tsx
ui/src/features/agents/components/AgentGitPanel.tsx
ui/src/features/agents/components/AgentThreadView.tsx
ui/src/features/agents/components/AgentsSidebar.tsx
ui/src/features/agents/components/PrHeader.tsx
ui/src/features/agents/components/SidebarFilterMenu.tsx
ui/src/features/agents/components/WorkflowApprovalCard.tsx
ui/src/features/agents/components/messages/AgentMessage.tsx
ui/src/features/agents/components/messages/MessageTimestamp.tsx
ui/src/features/agents/components/messages/UserMessage.tsx
ui/src/features/agents/components/messages/types.ts
ui/src/features/agents/lib/api.ts
ui/src/features/agents/lib/messageTimestamps.ts
ui/src/features/agents/lib/queries.ts
ui/src/features/agents/lib/sidebarFilter.test.ts
ui/src/features/agents/lib/sidebarFilter.ts
ui/src/features/agents/lib/sidebarPrefs.ts
ui/src/features/agents/lib/streamMessagesToUi.ts
ui/src/features/agents/lib/types.ts
ui/src/features/reviews/components/ReviewMainBody.tsx
ui/src/lib/agents/provider/useSubmitAgentMessage.ts
ui/src/lib/auth-redirect-core.ts
ui/src/lib/auth-redirect.test.ts
ui/src/routes/admin.tsx
ui/src/routes/agents.tsx
ui/src/routes/agents/$threadId_.plan.tsx
ui/vercel.json
ui/vite.config.ts
uv.lock
Auto-merging .github/workflows/ci.yml
CONFLICT (content): Merge conflict in .github/workflows/ci.yml
CONFLICT (modify/delete): .github/workflows/promote_main_to_prod.yml deleted in HEAD and modified in upstream/main. Version upstream/main of .github/workflows/promote_main_to_prod.yml left in tree.
Auto-merging .github/workflows/reviewer-eval.yml
CONFLICT (content): Merge conflict in .github/workflows/reviewer-eval.yml
Auto-merging .gitignore
Auto-merging AGENTS.md
CONFLICT (content): Merge conflict in AGENTS.md
Auto-merging CLAUDE.md
CONFLICT (content): Merge conflict in CLAUDE.md
Auto-merging Dockerfile
CONFLICT (content): Merge conflict in Dockerfile
Auto-merging Makefile
CONFLICT (content): Merge conflict in Makefile
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Auto-merging agent/analyzer.py
CONFLICT (content): Merge conflict in agent/analyzer.py
Auto-merging agent/chat.py
CONFLICT (content): Merge conflict in agent/chat.py
CONFLICT (modify/delete): agent/ci_autofix.py deleted in upstream/main and modified in HEAD. Version HEAD of agent/ci_autofix.py left in tree.
Auto-merging agent/completion.py
CONFLICT (add/add): Merge conflict in agent/completion.py
Auto-merging agent/dashboard/__init__.py
CONFLICT (content): Merge conflict in agent/dashboard/__init__.py
Auto-merging agent/dashboard/eval_jobs.py
CONFLICT (content): Merge conflict in agent/dashboard/eval_jobs.py
Auto-merging agent/dashboard/options.py
CONFLICT (content): Merge conflict in agent/dashboard/options.py
Auto-merging agent/dashboard/plan_api.py
CONFLICT (content): Merge conflict in agent/dashboard/plan_api.py
Auto-merging agent/dashboard/review_api.py
CONFLICT (content): Merge conflict in agent/dashboard/review_api.py
Auto-merging agent/dashboard/review_chat_api.py
CONFLICT (content): Merge conflict in agent/dashboard/review_chat_api.py
Auto-merging agent/dashboard/routes.py
Auto-merging agent/dashboard/schedules.py
CONFLICT (content): Merge conflict in agent/dashboard/schedules.py
Auto-merging agent/dashboard/team_settings.py
CONFLICT (content): Merge conflict in agent/dashboard/team_settings.py
Auto-merging agent/dashboard/thread_api.py
CONFLICT (content): Merge conflict in agent/dashboard/thread_api.py
Auto-merging agent/dashboard/workflow_approval.py
CONFLICT (add/add): Merge conflict in agent/dashboard/workflow_approval.py
Auto-merging agent/dashboard/workflow_approval_api.py
CONFLICT (add/add): Merge conflict in agent/dashboard/workflow_approval_api.py
Auto-merging agent/dispatch.py
CONFLICT (add/add): Merge conflict in agent/dispatch.py
Auto-merging agent/middleware/__init__.py
CONFLICT (content): Merge conflict in agent/middleware/__init__.py
Auto-merging agent/middleware/check_message_queue.py
Auto-merging agent/middleware/ensure_no_empty_msg.py
CONFLICT (add/add): Merge conflict in agent/middleware/ensure_no_empty_msg.py
Auto-merging agent/middleware/model_fallback.py
CONFLICT (content): Merge conflict in agent/middleware/model_fallback.py
Auto-merging agent/middleware/plan_mode.py
Auto-merging agent/middleware/pr_creation_guard.py
CONFLICT (add/add): Merge conflict in agent/middleware/pr_creation_guard.py
Auto-merging agent/middleware/refresh_slack_status.py
Auto-merging agent/middleware/sanitize_fireworks_messages.py
CONFLICT (add/add): Merge conflict in agent/middleware/sanitize_fireworks_messages.py
Auto-merging agent/middleware/sanitize_thinking_blocks.py
Auto-merging agent/middleware/subdir_agents.py
CONFLICT (add/add): Merge conflict in agent/middleware/subdir_agents.py
Auto-merging agent/middleware/timeout_wrapup.py
CONFLICT (add/add): Merge conflict in agent/middleware/timeout_wrapup.py
Auto-merging agent/middleware/workflow_push_guard.py
CONFLICT (add/add): Merge conflict in agent/middleware/workflow_push_guard.py
Auto-merging agent/prompt.py
CONFLICT (content): Merge conflict in agent/prompt.py
Auto-merging agent/review/findings.py
CONFLICT (content): Merge conflict in agent/review/findings.py
Auto-merging agent/review/publish.py
Auto-merging agent/review/reconcile.py
Auto-merging agent/reviewer.py
CONFLICT (content): Merge conflict in agent/reviewer.py
Auto-merging agent/server.py
CONFLICT (content): Merge conflict in agent/server.py
Auto-merging agent/tools/__init__.py
CONFLICT (content): Merge conflict in agent/tools/__init__.py
Auto-merging agent/tools/add_finding.py
CONFLICT (content): Merge conflict in agent/tools/add_finding.py
Auto-merging agent/tools/fetch_url.py
CONFLICT (content): Merge conflict in agent/tools/fetch_url.py
Auto-merging agent/tools/http_request.py
CONFLICT (content): Merge conflict in agent/tools/http_request.py
Auto-merging agent/tools/list_findings.py
Auto-merging agent/tools/list_review_findings.py
Auto-merging agent/tools/open_pull_request.py
CONFLICT (content): Merge conflict in agent/tools/open_pull_request.py
Auto-merging agent/tools/publish_review.py
CONFLICT (content): Merge conflict in agent/tools/publish_review.py
Auto-merging agent/tools/reply_to_finding_thread.py
Auto-merging agent/tools/request_pr_review.py
CONFLICT (content): Merge conflict in agent/tools/request_pr_review.py
Auto-merging agent/tools/resolve_finding_thread.py
Auto-merging agent/tools/schedule_thread_wakeup.py
Auto-merging agent/tools/slack_read_thread_messages.py
Auto-merging agent/tools/slack_start_new_thread.py
CONFLICT (add/add): Merge conflict in agent/tools/slack_start_new_thread.py
Auto-merging agent/tools/slack_thread_reply.py
CONFLICT (content): Merge conflict in agent/tools/slack_thread_reply.py
Auto-merging agent/tools/update_finding.py
CONFLICT (content): Merge conflict in agent/tools/update_finding.py
Auto-merging agent/tools/web_search.py
CONFLICT (content): Merge conflict in agent/tools/web_search.py
Auto-merging agent/utils/analyzer_skills.py
CONFLICT (content): Merge conflict in agent/utils/analyzer_skills.py
Auto-merging agent/utils/auth.py
CONFLICT (content): Merge conflict in agent/utils/auth.py
Auto-merging agent/utils/deferred_model.py
CONFLICT (add/add): Merge conflict in agent/utils/deferred_model.py
Auto-merging agent/utils/gateway.py
CONFLICT (add/add): Merge conflict in agent/utils/gateway.py
Auto-merging agent/utils/github_app.py
CONFLICT (content): Merge conflict in agent/utils/github_app.py
Auto-merging agent/utils/github_comments.py
Auto-merging agent/utils/github_org_membership.py
Auto-merging agent/utils/github_proxy.py
CONFLICT (content): Merge conflict in agent/utils/github_proxy.py
Auto-merging agent/utils/github_token.py
CONFLICT (content): Merge conflict in agent/utils/github_token.py
Auto-merging agent/utils/linear.py
Auto-merging agent/utils/model.py
CONFLICT (content): Merge conflict in agent/utils/model.py
Auto-merging agent/utils/multimodal.py
CONFLICT (content): Merge conflict in agent/utils/multimodal.py
Auto-merging agent/utils/sandbox.py
CONFLICT (content): Merge conflict in agent/utils/sandbox.py
Auto-merging agent/utils/sandbox_state.py
CONFLICT (content): Merge conflict in agent/utils/sandbox_state.py
Auto-merging agent/utils/slack.py
CONFLICT (content): Merge conflict in agent/utils/slack.py
Auto-merging agent/utils/url_safety.py
CONFLICT (add/add): Merge conflict in agent/utils/url_safety.py
Auto-merging agent/webapp.py
CONFLICT (content): Merge conflict in agent/webapp.py
Auto-merging agent/webhooks/github.py
CONFLICT (add/add): Merge conflict in agent/webhooks/github.py
Auto-merging agent/webhooks/linear.py
CONFLICT (add/add): Merge conflict in agent/webhooks/linear.py
Auto-merging agent/webhooks/slack.py
CONFLICT (add/add): Merge conflict in agent/webhooks/slack.py
Auto-merging docs/CUSTOMIZATION.md
Auto-merging docs/INSTALLATION.md
CONFLICT (content): Merge conflict in docs/INSTALLATION.md
Auto-merging evals/reviewer/README.md
Auto-merging evals/reviewer/config.toml
CONFLICT (content): Merge conflict in evals/reviewer/config.toml
Auto-merging evals/reviewer/judge.py
CONFLICT (content): Merge conflict in evals/reviewer/judge.py
Auto-merging evals/reviewer/run_eval.py
CONFLICT (content): Merge conflict in evals/reviewer/run_eval.py
Auto-merging evals/reviewer/target.py
CONFLICT (content): Merge conflict in evals/reviewer/target.py
Auto-merging pyproject.toml
CONFLICT (content): Merge conflict in pyproject.toml
Auto-merging tests/agent/test_agent_schedules.py
Auto-merging tests/agent/test_plan_mode.py
Auto-merging tests/auth/test_auth_sources.py
CONFLICT (content): Merge conflict in tests/auth/test_auth_sources.py
Auto-merging tests/auth/test_github_token_ttl.py
CONFLICT (content): Merge conflict in tests/auth/test_github_token_ttl.py
Auto-merging tests/conftest.py
CONFLICT (content): Merge conflict in tests/conftest.py
Auto-merging tests/dashboard/test_dashboard_thread_api.py
CONFLICT (content): Merge conflict in tests/dashboard/test_dashboard_thread_api.py
Auto-merging tests/dashboard/test_dashboard_web_handoff.py
CONFLICT (content): Merge conflict in tests/dashboard/test_dashboard_web_handoff.py
Auto-merging tests/dashboard/test_team_settings_grouping.py
CONFLICT (content): Merge conflict in tests/dashboard/test_team_settings_grouping.py
Auto-merging tests/dashboard/test_team_settings_org_guidelines.py
CONFLICT (content): Merge conflict in tests/dashboard/test_team_settings_org_guidelines.py
Auto-merging tests/e2e/fake_llm.py
CONFLICT (content): Merge conflict in tests/e2e/fake_llm.py
Auto-merging tests/e2e/harness.py
CONFLICT (content): Merge conflict in tests/e2e/harness.py
Auto-merging tests/e2e/patches.py
Auto-merging tests/e2e/tests/dashboard.spec.ts
CONFLICT (content): Merge conflict in tests/e2e/tests/dashboard.spec.ts
Auto-merging tests/e2e/tests/full_flow.spec.ts
CONFLICT (content): Merge conflict in tests/e2e/tests/full_flow.spec.ts
Auto-merging tests/e2e/tests/plan_review.spec.ts
Auto-merging tests/github/test_github_app.py
Auto-merging tests/github/test_github_comment_prompts.py
CONFLICT (content): Merge conflict in tests/github/test_github_comment_prompts.py
Auto-merging tests/github/test_github_issue_webhook.py
CONFLICT (content): Merge conflict in tests/github/test_github_issue_webhook.py
Auto-merging tests/github/test_github_proxy_refresh.py
Auto-merging tests/github/test_open_pull_request.py
CONFLICT (content): Merge conflict in tests/github/test_open_pull_request.py
Auto-merging tests/middleware/test_model_fallback_middleware.py
CONFLICT (content): Merge conflict in tests/middleware/test_model_fallback_middleware.py
Auto-merging tests/middleware/test_sanitize_thinking_blocks.py
CONFLICT (content): Merge conflict in tests/middleware/test_sanitize_thinking_blocks.py
Auto-merging tests/models/test_agent_subagent_models.py
CONFLICT (content): Merge conflict in tests/models/test_agent_subagent_models.py
Auto-merging tests/models/test_fireworks_model.py
CONFLICT (rename/delete): tests/test_google_model.py renamed to tests/models/test_google_model.py in upstream/main, but deleted in HEAD.
CONFLICT (modify/delete): tests/models/test_google_model.py deleted in HEAD and modified in upstream/main. Version upstream/main of tests/models/test_google_model.py left in tree.
Auto-merging tests/reviewer/test_pr_ready_auto_review.py
CONFLICT (content): Merge conflict in tests/reviewer/test_pr_ready_auto_review.py
Auto-merging tests/reviewer/test_review_chat.py
Auto-merging tests/reviewer/test_reviewer.py
CONFLICT (content): Merge conflict in tests/reviewer/test_reviewer.py
Auto-merging tests/reviewer/test_reviewer_eval_target.py
Auto-merging tests/reviewer/test_reviewer_findings.py
CONFLICT (content): Merge conflict in tests/reviewer/test_reviewer_findings.py
Auto-merging tests/reviewer/test_reviewer_publish.py
CONFLICT (content): Merge conflict in tests/reviewer/test_reviewer_publish.py
Auto-merging tests/reviewer/test_reviewer_reconcile.py
Auto-merging tests/reviewer/test_reviewer_tools.py
CONFLICT (content): Merge conflict in tests/reviewer/test_reviewer_tools.py
Auto-merging tests/reviewer/test_reviewer_watch.py
CONFLICT (content): Merge conflict in tests/reviewer/test_reviewer_watch.py
Auto-merging tests/sandbox/test_local_integration.py
CONFLICT (content): Merge conflict in tests/sandbox/test_local_integration.py
Auto-merging tests/sandbox/test_proxy_auth.py
CONFLICT (content): Merge conflict in tests/sandbox/test_proxy_auth.py
Auto-merging tests/slack/test_slack_assistants_status.py
Auto-merging tests/slack/test_slack_context.py
CONFLICT (content): Merge conflict in tests/slack/test_slack_context.py
Auto-merging tests/slack/test_slack_thread_reply_tool.py
CONFLICT (content): Merge conflict in tests/slack/test_slack_thread_reply_tool.py
CONFLICT (modify/delete): tests/test_autofix_webhook.py deleted in upstream/main and modified in HEAD. Version HEAD of tests/test_autofix_webhook.py left in tree.
CONFLICT (modify/delete): tests/test_ci_autofix.py deleted in upstream/main and modified in HEAD. Version HEAD of tests/test_ci_autofix.py left in tree.
CONFLICT (modify/delete): tests/test_http_security.py deleted in upstream/main and modified in HEAD. Version HEAD of tests/test_http_security.py left in tree.
CONFLICT (modify/delete): tests/test_model_fallback_resolution.py deleted in upstream/main and modified in HEAD. Version HEAD of tests/test_model_fallback_resolution.py left in tree.
CONFLICT (modify/delete): tests/test_plan_review.py deleted in upstream/main and modified in HEAD. Version HEAD of tests/test_plan_review.py left in tree.
Auto-merging tests/tools/test_corridor_mcp.py
Auto-merging ui/AGENTS.md
CONFLICT (add/add): Merge conflict in ui/AGENTS.md
CONFLICT (modify/delete): ui/bun.lock deleted in upstream/main and modified in HEAD. Version HEAD of ui/bun.lock left in tree.
Auto-merging ui/package.json
CONFLICT (content): Merge conflict in ui/package.json
Auto-merging ui/src/client.tsx
CONFLICT (content): Merge conflict in ui/src/client.tsx
CONFLICT (modify/delete): ui/src/components/agents/PlanReview.tsx deleted in upstream/main and modified in HEAD. Version HEAD of ui/src/components/agents/PlanReview.tsx left in tree.
Auto-merging ui/src/features/agents/components/AgentGitPanel.tsx
CONFLICT (content): Merge conflict in ui/src/features/agents/components/AgentGitPanel.tsx
Auto-merging ui/src/features/agents/components/AgentThreadView.tsx
CONFLICT (content): Merge conflict in ui/src/features/agents/components/AgentThreadView.tsx
Auto-merging ui/src/features/agents/components/AgentsSidebar.tsx
CONFLICT (content): Merge conflict in ui/src/features/agents/components/AgentsSidebar.tsx
CONFLICT (file location): ui/src/components/agents/PrHeader.tsx added in HEAD inside a directory that was renamed in upstream/main, suggesting it should perhaps be moved to ui/src/features/agents/components/PrHeader.tsx.
CONFLICT (file location): ui/src/components/agents/SidebarFilterMenu.tsx added in HEAD inside a directory that was renamed in upstream/main, suggesting it should perhaps be moved to ui/src/features/agents/components/SidebarFilterMenu.tsx.
Auto-merging ui/src/features/agents/components/SidebarFilterMenu.tsx
CONFLICT (add/add): Merge conflict in ui/src/features/agents/components/SidebarFilterMenu.tsx
CONFLICT (file location): ui/src/components/agents/WorkflowApprovalCard.tsx added in HEAD inside a directory that was renamed in upstream/main, suggesting it should perhaps be moved to ui/src/features/agents/components/WorkflowApprovalCard.tsx.
Auto-merging ui/src/features/agents/components/WorkflowApprovalCard.tsx
CONFLICT (add/add): Merge conflict in ui/src/features/agents/components/WorkflowApprovalCard.tsx
Auto-merging ui/src/features/agents/components/messages/AgentMessage.tsx
CONFLICT (content): Merge conflict in ui/src/features/agents/components/messages/AgentMessage.tsx
CONFLICT (file location): ui/src/components/agents/messages/MessageTimestamp.tsx added in HEAD inside a directory that was renamed in upstream/main, suggesting it should perhaps be moved to ui/src/features/agents/components/messages/MessageTimestamp.tsx.
Auto-merging ui/src/features/agents/components/messages/Messages.tsx
Auto-merging ui/src/features/agents/components/messages/UserMessage.tsx
CONFLICT (content): Merge conflict in ui/src/features/agents/components/messages/UserMessage.tsx
Auto-merging ui/src/features/agents/components/messages/types.ts
CONFLICT (content): Merge conflict in ui/src/features/agents/components/messages/types.ts
Auto-merging ui/src/features/agents/lib/api.ts
CONFLICT (content): Merge conflict in ui/src/features/agents/lib/api.ts
CONFLICT (file location): ui/src/lib/agents/messageTimestamps.ts added in HEAD inside a directory that was renamed in upstream/main, suggesting it should perhaps be moved to ui/src/features/agents/lib/messageTimestamps.ts.
Auto-merging ui/src/features/agents/lib/queries.ts
CONFLICT (content): Merge conflict in ui/src/features/agents/lib/queries.ts
CONFLICT (file location): ui/src/lib/agents/sidebarFilter.test.ts added in HEAD inside a directory that was renamed in upstream/main, suggesting it should perhaps be moved to ui/src/features/agents/lib/sidebarFilter.test.ts.
CONFLICT (file location): ui/src/lib/agents/sidebarFilter.ts added in HEAD inside a directory that was renamed in upstream/main, suggesting it should perhaps be moved to ui/src/features/agents/lib/sidebarFilter.ts.
CONFLICT (file location): ui/src/lib/agents/sidebarPrefs.ts added in HEAD inside a directory that was renamed in upstream/main, suggesting it should perhaps be moved to ui/src/features/agents/lib/sidebarPrefs.ts.
Auto-merging ui/src/features/agents/lib/sidebarPrefs.ts
CONFLICT (add/add): Merge conflict in ui/src/features/agents/lib/sidebarPrefs.ts
Auto-merging ui/src/features/agents/lib/streamMessagesToUi.ts
CONFLICT (content): Merge conflict in ui/src/features/agents/lib/streamMessagesToUi.ts
Auto-merging ui/src/features/agents/lib/types.ts
CONFLICT (content): Merge conflict in ui/src/features/agents/lib/types.ts
Auto-merging ui/src/features/automations/components/AutomationEditor.tsx
Auto-merging ui/src/features/reviews/components/ReviewMainBody.tsx
CONFLICT (content): Merge conflict in ui/src/features/reviews/components/ReviewMainBody.tsx
Auto-merging ui/src/features/reviews/components/ReviewSidebar.tsx
CONFLICT (modify/delete): ui/src/lib/agents/provider/useSubmitAgentMessage.ts deleted in upstream/main and modified in HEAD. Version HEAD of ui/src/lib/agents/provider/useSubmitAgentMessage.ts left in tree.
Auto-merging ui/src/lib/api.ts
Auto-merging ui/src/lib/auth-redirect-core.ts
CONFLICT (add/add): Merge conflict in ui/src/lib/auth-redirect-core.ts
Auto-merging ui/src/lib/auth-redirect.test.ts
CONFLICT (add/add): Merge conflict in ui/src/lib/auth-redirect.test.ts
Auto-merging ui/src/routes/admin.tsx
CONFLICT (content): Merge conflict in ui/src/routes/admin.tsx
Auto-merging ui/src/routes/agents.tsx
CONFLICT (content): Merge conflict in ui/src/routes/agents.tsx
Auto-merging ui/src/routes/agents/$threadId_.plan.tsx
CONFLICT (content): Merge conflict in ui/src/routes/agents/$threadId_.plan.tsx
Auto-merging ui/src/routes/agents/reviews/$owner.$repo.$number.tsx
Auto-merging ui/src/routes/cloud-agents.tsx
Auto-merging ui/vercel.json
CONFLICT (content): Merge conflict in ui/vercel.json
Auto-merging ui/vite.config.ts
CONFLICT (content): Merge conflict in ui/vite.config.ts
Auto-merging uv.lock
CONFLICT (content): Merge conflict in uv.lock

View file

@ -0,0 +1,298 @@
M AGENTS.md
M CLAUDE.md
M README.md
M agent/analyzer.py
A agent/api/__init__.py
A agent/api/app.py
A agent/api/health.py
M agent/chat.py
M agent/dashboard/agent_usage.py
M agent/dashboard/eval_jobs.py
M agent/dashboard/review_api.py
M agent/dashboard/review_chat_api.py
M agent/dashboard/review_style_jobs.py
A agent/graphs/__init__.py
A agent/graphs/agent.py
A agent/graphs/analyzer.py
A agent/graphs/chat.py
A agent/graphs/reviewer.py
A agent/graphs/scheduler.py
M agent/middleware/settle_review_check.py
M agent/prompt.py
A agent/providers/__init__.py
A agent/resources/__init__.py
R100 default_prompt.md agent/resources/default_prompt.md
A agent/review/__init__.py
R098 agent/reviewer_diff.py agent/review/diff.py
R100 agent/reviewer_eval_store.py agent/review/eval_store.py
R100 agent/reviewer_findings.py agent/review/findings.py
R098 agent/reviewer_groups.py agent/review/groups.py
R099 agent/reviewer_publish.py agent/review/publish.py
R099 agent/reviewer_reconcile.py agent/review/reconcile.py
R100 agent/review_style_collector.py agent/review/style_collector.py
R100 agent/review_style_guidance.py agent/review/style_guidance.py
R098 agent/reviewer_trace_context.py agent/review/trace_context.py
M agent/reviewer.py
A agent/runtime/__init__.py
A agent/runtime/constants.py
A agent/runtime/execution.py
A agent/runtime/sandbox.py
M agent/server.py
M agent/tools/add_finding.py
M agent/tools/list_findings.py
M agent/tools/list_review_findings.py
M agent/tools/publish_review.py
M agent/tools/reply_to_finding_thread.py
M agent/tools/request_pr_review.py
M agent/tools/resolve_finding_thread.py
M agent/tools/update_finding.py
M agent/utils/github_feedback.py
M agent/utils/github_org_membership.py
M agent/webapp.py
A agent/webhooks/common.py
M agent/webhooks/github.py
A agent/webhooks/github_routes.py
M agent/webhooks/linear.py
A agent/webhooks/linear_routes.py
M agent/webhooks/slack.py
A agent/webhooks/slack_routes.py
R100 static/dark.svg assets/dark.svg
R100 static/light.svg assets/light.svg
R099 CUSTOMIZATION.md docs/CUSTOMIZATION.md
R100 INSTALLATION.md docs/INSTALLATION.md
M evals/reviewer/judge.py
M evals/reviewer/run_eval.py
M evals/reviewer/store_reporter.py
M evals/reviewer/target.py
M langgraph.json
R100 tests/test_agent_assembly_context.py tests/agent/test_agent_assembly_context.py
R100 tests/test_agent_instructions.py tests/agent/test_agent_instructions.py
R100 tests/test_agent_schedules.py tests/agent/test_agent_schedules.py
R068 tests/test_agent_thread_pr_state.py tests/agent/test_agent_thread_pr_state.py
R100 tests/test_agent_usage.py tests/agent/test_agent_usage.py
R100 tests/test_agents_md.py tests/agent/test_agents_md.py
R100 tests/test_api_standards_skill.py tests/agent/test_api_standards_skill.py
R100 tests/test_dispatch.py tests/agent/test_dispatch.py
R100 tests/test_import_hygiene.py tests/agent/test_import_hygiene.py
R100 tests/test_langsmith_trace_url.py tests/agent/test_langsmith_trace_url.py
R100 tests/test_multimodal.py tests/agent/test_multimodal.py
R100 tests/test_plan_mode.py tests/agent/test_plan_mode.py
R100 tests/test_plan_review.py tests/agent/test_plan_review.py
R100 tests/test_prompt_default_repo.py tests/agent/test_prompt_default_repo.py
R100 tests/test_repo_prep.py tests/agent/test_repo_prep.py
R100 tests/test_task_retry.py tests/agent/test_task_retry.py
R100 tests/test_timeout_wrapup.py tests/agent/test_timeout_wrapup.py
R100 tests/test_workflow_push_guard.py tests/agent/test_workflow_push_guard.py
R100 tests/test_analyzer_cron.py tests/analyzer/test_analyzer_cron.py
R100 tests/test_analyzer_skills.py tests/analyzer/test_analyzer_skills.py
R100 tests/test_eval_jobs.py tests/analyzer/test_eval_jobs.py
R100 tests/test_eval_store_reporter.py tests/analyzer/test_eval_store_reporter.py
R100 tests/test_auth_sources.py tests/auth/test_auth_sources.py
R100 tests/test_authorship.py tests/auth/test_authorship.py
R100 tests/test_encryption.py tests/auth/test_encryption.py
R100 tests/test_github_oauth_refresh.py tests/auth/test_github_oauth_refresh.py
R093 tests/test_github_token_ttl.py tests/auth/test_github_token_ttl.py
R100 tests/test_notion_oauth.py tests/auth/test_notion_oauth.py
R100 tests/test_slack_oauth.py tests/auth/test_slack_oauth.py
R100 tests/test_user_credentials.py tests/auth/test_user_credentials.py
M tests/conftest.py
R073 tests/test_account_link.py tests/dashboard/test_account_link.py
R100 tests/test_autofix_state.py tests/dashboard/test_autofix_state.py
R100 tests/test_dashboard_admin.py tests/dashboard/test_dashboard_admin.py
R100 tests/test_dashboard_csrf.py tests/dashboard/test_dashboard_csrf.py
R100 tests/test_dashboard_links.py tests/dashboard/test_dashboard_links.py
R100 tests/test_dashboard_oauth_redirect.py tests/dashboard/test_dashboard_oauth_redirect.py
R100 tests/test_dashboard_org_login_gate.py tests/dashboard/test_dashboard_org_login_gate.py
R100 tests/test_dashboard_repo_optional.py tests/dashboard/test_dashboard_repo_optional.py
R100 tests/test_dashboard_repos.py tests/dashboard/test_dashboard_repos.py
R098 tests/test_dashboard_reviews.py tests/dashboard/test_dashboard_reviews.py
R100 tests/test_dashboard_run_email.py tests/dashboard/test_dashboard_run_email.py
R100 tests/test_dashboard_thread_api.py tests/dashboard/test_dashboard_thread_api.py
R100 tests/test_dashboard_thread_api_activity.py tests/dashboard/test_dashboard_thread_api_activity.py
R100 tests/test_dashboard_web_handoff.py tests/dashboard/test_dashboard_web_handoff.py
R089 tests/test_public_repo_org_gate.py tests/dashboard/test_public_repo_org_gate.py
R100 tests/test_team_credentials.py tests/dashboard/test_team_credentials.py
R100 tests/test_team_settings_fable.py tests/dashboard/test_team_settings_fable.py
R100 tests/test_team_settings_grouping.py tests/dashboard/test_team_settings_grouping.py
R100 tests/test_team_settings_org_guidelines.py tests/dashboard/test_team_settings_org_guidelines.py
R100 tests/test_user_mappings.py tests/dashboard/test_user_mappings.py
R100 tests/test_workflow_approval_api.py tests/dashboard/test_workflow_approval_api.py
M tests/e2e/harness.py
R100 tests/test_github_app.py tests/github/test_github_app.py
R099 tests/test_github_checks.py tests/github/test_github_checks.py
R100 tests/test_github_ci.py tests/github/test_github_ci.py
R099 tests/test_github_comment_prompts.py tests/github/test_github_comment_prompts.py
R095 tests/test_github_feedback.py tests/github/test_github_feedback.py
R100 tests/test_github_http.py tests/github/test_github_http.py
R078 tests/test_github_issue_webhook.py tests/github/test_github_issue_webhook.py
R100 tests/test_github_proxy_refresh.py tests/github/test_github_proxy_refresh.py
R100 tests/test_normalize_repo.py tests/github/test_normalize_repo.py
R100 tests/test_open_pull_request.py tests/github/test_open_pull_request.py
R100 tests/test_pr_creation_guard.py tests/github/test_pr_creation_guard.py
R091 tests/test_repo_extraction.py tests/github/test_repo_extraction.py
R100 tests/test_check_message_queue.py tests/middleware/test_check_message_queue.py
R100 tests/test_ensure_no_empty_msg.py tests/middleware/test_ensure_no_empty_msg.py
R100 tests/test_model_fallback_middleware.py tests/middleware/test_model_fallback_middleware.py
R100 tests/test_notify_step_limit_middleware.py tests/middleware/test_notify_step_limit_middleware.py
R100 tests/test_prepare_run_middleware.py tests/middleware/test_prepare_run_middleware.py
R100 tests/test_repair_orphaned_tool_calls.py tests/middleware/test_repair_orphaned_tool_calls.py
R100 tests/test_sanitize_fireworks_messages.py tests/middleware/test_sanitize_fireworks_messages.py
R100 tests/test_sanitize_openai_responses.py tests/middleware/test_sanitize_openai_responses.py
R100 tests/test_sanitize_thinking_blocks.py tests/middleware/test_sanitize_thinking_blocks.py
R100 tests/test_sanitize_tool_inputs.py tests/middleware/test_sanitize_tool_inputs.py
R100 tests/test_subdir_agents_middleware.py tests/middleware/test_subdir_agents_middleware.py
R100 tests/test_tool_artifact_middleware.py tests/middleware/test_tool_artifact_middleware.py
R100 tests/test_agent_subagent_models.py tests/models/test_agent_subagent_models.py
R100 tests/test_anthropic_effort.py tests/models/test_anthropic_effort.py
R100 tests/test_anthropic_model.py tests/models/test_anthropic_model.py
R100 tests/test_deferred_model.py tests/models/test_deferred_model.py
R100 tests/test_fireworks_model.py tests/models/test_fireworks_model.py
R100 tests/test_google_model.py tests/models/test_google_model.py
R100 tests/test_model_fallback_resolution.py tests/models/test_model_fallback_resolution.py
R062 tests/test_pr_ready_auto_review.py tests/reviewer/test_pr_ready_auto_review.py
R100 tests/test_recent_comments.py tests/reviewer/test_recent_comments.py
R100 tests/test_reconcile_sweep.py tests/reviewer/test_reconcile_sweep.py
R099 tests/test_review_api.py tests/reviewer/test_review_api.py
R100 tests/test_review_chat.py tests/reviewer/test_review_chat.py
R094 tests/test_review_style_collector.py tests/reviewer/test_review_style_collector.py
R100 tests/test_review_style_sync.py tests/reviewer/test_review_style_sync.py
R100 tests/test_review_styles_store.py tests/reviewer/test_review_styles_store.py
R100 tests/test_reviewer.py tests/reviewer/test_reviewer.py
R096 tests/test_reviewer_diff.py tests/reviewer/test_reviewer_diff.py
R100 tests/test_reviewer_eval_judge.py tests/reviewer/test_reviewer_eval_judge.py
R100 tests/test_reviewer_eval_run.py tests/reviewer/test_reviewer_eval_run.py
R099 tests/test_reviewer_eval_target.py tests/reviewer/test_reviewer_eval_target.py
R088 tests/test_reviewer_findings.py tests/reviewer/test_reviewer_findings.py
R092 tests/test_reviewer_groups.py tests/reviewer/test_reviewer_groups.py
R100 tests/test_reviewer_outcomes.py tests/reviewer/test_reviewer_outcomes.py
R098 tests/test_reviewer_publish.py tests/reviewer/test_reviewer_publish.py
R087 tests/test_reviewer_reconcile.py tests/reviewer/test_reviewer_reconcile.py
R099 tests/test_reviewer_tools.py tests/reviewer/test_reviewer_tools.py
R096 tests/test_reviewer_trace_context.py tests/reviewer/test_reviewer_trace_context.py
R064 tests/test_reviewer_watch.py tests/reviewer/test_reviewer_watch.py
R098 tests/test_daytona_integration.py tests/sandbox/test_daytona_integration.py
R098 tests/test_e2b_integration.py tests/sandbox/test_e2b_integration.py
R100 tests/test_gateway.py tests/sandbox/test_gateway.py
R100 tests/test_langsmith_sandbox_config.py tests/sandbox/test_langsmith_sandbox_config.py
R100 tests/test_langsmith_sandbox_timeout.py tests/sandbox/test_langsmith_sandbox_timeout.py
R100 tests/test_local_integration.py tests/sandbox/test_local_integration.py
R100 tests/test_proxy_auth.py tests/sandbox/test_proxy_auth.py
R100 tests/test_repo_snapshots.py tests/sandbox/test_repo_snapshots.py
R100 tests/test_sandbox_paths.py tests/sandbox/test_sandbox_paths.py
R100 tests/middleware/test_sandbox_recovery.py tests/sandbox/test_sandbox_recovery.py
R100 tests/test_sandbox_state.py tests/sandbox/test_sandbox_state.py
R100 tests/test_stale_sandbox_creating.py tests/sandbox/test_stale_sandbox_creating.py
R100 tests/test_refresh_slack_status_middleware.py tests/slack/test_refresh_slack_status_middleware.py
R100 tests/test_slack_add_reaction_tool.py tests/slack/test_slack_add_reaction_tool.py
R100 tests/test_slack_assistants_status.py tests/slack/test_slack_assistants_status.py
R082 tests/test_slack_context.py tests/slack/test_slack_context.py
R090 tests/test_slack_feedback.py tests/slack/test_slack_feedback.py
R100 tests/test_slack_start_new_thread_tool.py tests/slack/test_slack_start_new_thread_tool.py
R100 tests/test_slack_thread_reply_tool.py tests/slack/test_slack_thread_reply_tool.py
R082 tests/test_slack_webhook_errors.py tests/slack/test_slack_webhook_errors.py
R100 tests/test_corridor_mcp.py tests/tools/test_corridor_mcp.py
R100 tests/test_currents_tools.py tests/tools/test_currents_tools.py
R100 tests/test_http_security.py tests/tools/test_http_security.py
R100 tests/test_observability_tools.py tests/tools/test_observability_tools.py
R100 tests/test_report_platform_issue_tool.py tests/tools/test_report_platform_issue_tool.py
R100 tests/test_schedule_thread_wakeup.py tests/tools/test_schedule_thread_wakeup.py
R100 tests/test_stagehand_browser.py tests/tools/test_stagehand_browser.py
R100 tests/test_completion_webhook.py tests/webhooks/test_completion_webhook.py
R091 tests/test_linear_webhook_author.py tests/webhooks/test_linear_webhook_author.py
M ui/eslint.config.js
D ui/src/components/agents/AgentPromptBar.tsx
R097 ui/src/components/agents/AgentGitPanel.tsx ui/src/features/agents/components/AgentGitPanel.tsx
A ui/src/features/agents/components/AgentPromptBar.tsx
R098 ui/src/components/agents/AgentRunCard.tsx ui/src/features/agents/components/AgentRunCard.tsx
R092 ui/src/components/agents/AgentThreadView.tsx ui/src/features/agents/components/AgentThreadView.tsx
R089 ui/src/components/agents/AgentsHome.tsx ui/src/features/agents/components/AgentsHome.tsx
R098 ui/src/components/agents/AgentsSidebar.tsx ui/src/features/agents/components/AgentsSidebar.tsx
R098 ui/src/components/agents/AgentsThreadsPage.tsx ui/src/features/agents/components/AgentsThreadsPage.tsx
R100 ui/src/components/agents/OnboardingDialog.tsx ui/src/features/agents/components/OnboardingDialog.tsx
R099 ui/src/components/agents/PlanReview.tsx ui/src/features/agents/components/PlanReview.tsx
R097 ui/src/components/agents/SidebarFilterMenu.tsx ui/src/features/agents/components/SidebarFilterMenu.tsx
R098 ui/src/components/agents/WorkflowApprovalCard.tsx ui/src/features/agents/components/WorkflowApprovalCard.tsx
R097 ui/src/components/agents/ported/CloudPromptBar.tsx ui/src/features/agents/components/chat/CloudPromptBar.tsx
R100 ui/src/components/agents/ported/CodeBlock.tsx ui/src/features/agents/components/chat/CodeBlock.tsx
R088 ui/src/components/agents/ported/DiffView.tsx ui/src/features/agents/components/chat/DiffView.tsx
R100 ui/src/components/agents/ported/Logo.tsx ui/src/features/agents/components/chat/Logo.tsx
R100 ui/src/components/agents/ported/Markdown.tsx ui/src/features/agents/components/chat/Markdown.tsx
R098 ui/src/components/agents/ported/ReplyCard.tsx ui/src/features/agents/components/chat/ReplyCard.tsx
R098 ui/src/components/agents/ported/ShellCommand.tsx ui/src/features/agents/components/chat/ShellCommand.tsx
R097 ui/src/components/agents/ported/ToolExecution.tsx ui/src/features/agents/components/chat/ToolExecution.tsx
R096 ui/src/components/agents/messages/AgentMessage.tsx ui/src/features/agents/components/messages/AgentMessage.tsx
R083 ui/src/components/agents/messages/ChunkRenderer.tsx ui/src/features/agents/components/messages/ChunkRenderer.tsx
R100 ui/src/components/agents/messages/MessageTimestamp.tsx ui/src/features/agents/components/messages/MessageTimestamp.tsx
R098 ui/src/components/agents/messages/Messages.tsx ui/src/features/agents/components/messages/Messages.tsx
R100 ui/src/components/agents/messages/ReasoningBlock.tsx ui/src/features/agents/components/messages/ReasoningBlock.tsx
R100 ui/src/components/agents/messages/ThinkingSpinner.tsx ui/src/features/agents/components/messages/ThinkingSpinner.tsx
R097 ui/src/components/agents/messages/TurnChangedFilesCard.tsx ui/src/features/agents/components/messages/TurnChangedFilesCard.tsx
R097 ui/src/components/agents/messages/UserMessage.tsx ui/src/features/agents/components/messages/UserMessage.tsx
R100 ui/src/components/agents/messages/WorkSummary.tsx ui/src/features/agents/components/messages/WorkSummary.tsx
R100 ui/src/components/agents/messages/index.ts ui/src/features/agents/components/messages/index.ts
R098 ui/src/components/agents/messages/renderItems.ts ui/src/features/agents/components/messages/renderItems.ts
R092 ui/src/components/agents/messages/summarizeChangedFiles.ts ui/src/features/agents/components/messages/summarizeChangedFiles.ts
R094 ui/src/components/agents/messages/types.ts ui/src/features/agents/components/messages/types.ts
R100 ui/src/components/agents/subagents/SubagentActivity.tsx ui/src/features/agents/components/subagents/SubagentActivity.tsx
R092 ui/src/components/agents/subagents/SubagentCard.tsx ui/src/features/agents/components/subagents/SubagentCard.tsx
R091 ui/src/components/agents/subagents/SubagentGroup.tsx ui/src/features/agents/components/subagents/SubagentGroup.tsx
R100 ui/src/components/agents/subagents/index.ts ui/src/features/agents/components/subagents/index.ts
R100 ui/src/components/agents/z-index.ts ui/src/features/agents/components/z-index.ts
R100 ui/src/components/agents/ported/BranchSelector.tsx ui/src/features/agents/experiments/BranchSelector.tsx
R089 ui/src/components/agents/ported/BubblePrefix.tsx ui/src/features/agents/experiments/BubblePrefix.tsx
R099 ui/src/components/agents/ported/ChatView.tsx ui/src/features/agents/experiments/ChatView.tsx
R100 ui/src/components/agents/ported/CompactingIndicator.tsx ui/src/features/agents/experiments/CompactingIndicator.tsx
R100 ui/src/components/agents/ported/ContextIndicator.tsx ui/src/features/agents/experiments/ContextIndicator.tsx
R100 ui/src/components/agents/ported/Footer.tsx ui/src/features/agents/experiments/Footer.tsx
R100 ui/src/components/agents/ported/HeaderBar.tsx ui/src/features/agents/experiments/HeaderBar.tsx
R100 ui/src/components/agents/ported/MarkdownTable.tsx ui/src/features/agents/experiments/MarkdownTable.tsx
R100 ui/src/components/agents/ported/PanelResizeHandle.tsx ui/src/features/agents/experiments/PanelResizeHandle.tsx
R099 ui/src/components/agents/ported/PromptBar.tsx ui/src/features/agents/experiments/PromptBar.tsx
R100 ui/src/components/agents/ported/SourceControlPanel.tsx ui/src/features/agents/experiments/SourceControlPanel.tsx
R099 ui/src/components/agents/ported/SourceControlTile.tsx ui/src/features/agents/experiments/SourceControlTile.tsx
R099 ui/src/components/agents/ported/ThreadPicker.tsx ui/src/features/agents/experiments/ThreadPicker.tsx
R098 ui/src/components/agents/ported/TodoList.tsx ui/src/features/agents/experiments/TodoList.tsx
R093 ui/src/components/agents/ported/index.ts ui/src/features/agents/experiments/index.ts
R100 ui/src/lib/agents/AgentThreadStreamProvider.tsx ui/src/features/agents/lib/AgentThreadStreamProvider.tsx
R100 ui/src/lib/agents/api.ts ui/src/features/agents/lib/api.ts
R100 ui/src/lib/agents/messageTimestamps.ts ui/src/features/agents/lib/messageTimestamps.ts
R100 ui/src/lib/agents/provider/useIsInAgentThreadStream.tsx ui/src/features/agents/lib/provider/useIsInAgentThreadStream.tsx
R096 ui/src/lib/agents/provider/useLiveMarkdownMessageId.ts ui/src/features/agents/lib/provider/useLiveMarkdownMessageId.ts
R100 ui/src/lib/agents/provider/useModelOptions.ts ui/src/features/agents/lib/provider/useModelOptions.ts
R095 ui/src/lib/agents/provider/useSubmitAgentMessage.ts ui/src/features/agents/lib/provider/useSubmitAgentMessage.ts
R100 ui/src/lib/agents/queries.ts ui/src/features/agents/lib/queries.ts
R100 ui/src/lib/agents/sidebarFilter.test.ts ui/src/features/agents/lib/sidebarFilter.test.ts
R100 ui/src/lib/agents/sidebarFilter.ts ui/src/features/agents/lib/sidebarFilter.ts
R096 ui/src/lib/agents/sidebarPrefs.ts ui/src/features/agents/lib/sidebarPrefs.ts
R100 ui/src/lib/agents/streamMessagesToUi.ts ui/src/features/agents/lib/streamMessagesToUi.ts
R100 ui/src/lib/agents/types.ts ui/src/features/agents/lib/types.ts
R095 ui/src/lib/agents/useRunCompletionNotifier.ts ui/src/features/agents/lib/useRunCompletionNotifier.ts
R100 ui/src/components/agents/utils/diffStats.ts ui/src/features/agents/utils/diffStats.ts
R100 ui/src/components/agents/utils/diffUtils.test.ts ui/src/features/agents/utils/diffUtils.test.ts
R100 ui/src/components/agents/utils/diffUtils.ts ui/src/features/agents/utils/diffUtils.ts
R095 ui/src/components/agents/AutomationEditor.tsx ui/src/features/automations/components/AutomationEditor.tsx
R091 ui/src/components/agents/AutomationTemplates.tsx ui/src/features/automations/components/AutomationTemplates.tsx
R096 ui/src/components/agents/AutomationsList.tsx ui/src/features/automations/components/AutomationsList.tsx
R097 ui/src/components/agents/ScheduleTriggerPicker.tsx ui/src/features/automations/components/ScheduleTriggerPicker.tsx
R100 ui/src/lib/agents/automation-templates.ts ui/src/features/automations/lib/automation-templates.ts
R100 ui/src/lib/agents/cron.ts ui/src/features/automations/lib/cron.ts
R100 ui/src/components/agents/PrHeader.tsx ui/src/features/reviews/components/PrHeader.tsx
R099 ui/src/components/agents/ReviewChat.tsx ui/src/features/reviews/components/ReviewChat.tsx
R100 ui/src/components/agents/ReviewCommentsMenu.tsx ui/src/features/reviews/components/ReviewCommentsMenu.tsx
R099 ui/src/components/agents/ReviewMainBody.tsx ui/src/features/reviews/components/ReviewMainBody.tsx
R099 ui/src/components/agents/ReviewSidebar.tsx ui/src/features/reviews/components/ReviewSidebar.tsx
R093 ui/src/components/agents/ReviewTab.tsx ui/src/features/reviews/components/ReviewTab.tsx
R100 ui/src/components/agents/RepoSelector.tsx ui/src/features/settings/components/RepoSelector.tsx
M ui/src/lib/notifications.ts
M ui/src/routes/agents.tsx
M ui/src/routes/agents/$threadId.tsx
M ui/src/routes/agents/$threadId_.plan.tsx
M ui/src/routes/agents/automations/$scheduleId.tsx
M ui/src/routes/agents/automations/index.tsx
M ui/src/routes/agents/automations/new.tsx
M ui/src/routes/agents/index.tsx
M ui/src/routes/agents/reviews/$owner.$repo.$number.tsx
M ui/src/routes/agents/threads.tsx
M ui/src/routes/cloud-agents.tsx
M ui/tsconfig.json
M ui/vite.config.ts

View file

@ -0,0 +1,66 @@
# Build plan — adopt upstream domain reorg `8356eb34` (#1726) into the Sea Haven open-swe fork
Date: 2026-07-17. Base: `dev` (clean, `a518a129`). Scoping report: `reorg-scoping-report.md` (same directory: `docs/upstream-sync/domain-reorg/`).
## Goal & strategy
Adopt upstream's 298-file domain reorg as a **file-move exercise applied to the fork's tree**: fork file contents, upstream layout. Never take upstream file contents except the 21 verified thin structural "A" shims (`agent/graphs/*`, `agent/runtime/*`, `agent/api/*`, `agent/webhooks/{common,*_routes}.py` skeletons, package `__init__.py`s). Rationale: the fork has settled permanent divergences (sync sandbox lifecycle + 4-case `__creating__` sentinel, workflow-token security model, bun toolchain, Atlassian integration, plan-mode #130, TID-COLLIDE-01 guard) — adoption is layout convergence, not content convergence.
Payoff: kills the per-pick path-remap tax on all future upstream cherry-picks; 8 of 19 deferred ledger rows become clean/near-clean, including priority security pick #1736 (written against `agent/webhooks/common.py`, which only exists after this exercise).
## Approved decisions (Adam, 2026-07-17)
1. **FastAPI/webapp split: APPROVED.** Fork's 2,590-line `agent/webapp.py` splits into `agent/webhooks/common.py` (shared verify/dispatch helpers), `agent/api/{app,health}.py`, and per-source `{github,linear,slack,jira,confluence}_routes.py`; `webapp.py` remains a compatibility shim. `/sh-security-review` required on that commit before merge.
2. **Atlassian placement (re-confirmed by Adam 2026-07-17):** fork-only `jira_routes.py` + `confluence_routes.py` mirroring upstream's per-source pattern; **Atlassian Connect lifecycle/descriptor routes (`/connect/*`) fold into `confluence_routes.py`** (Connect is the Confluence trigger; Jira uses Automation webhooks). JWT/qsh machinery stays in `agent/utils/atlassian_connect.py` (unmoved). Split out a `connect_routes.py` later only if Jira migrates to Connect.
3. `langgraph.json` graph entrypoints retarget to `agent.graphs.*` shims; add fork-only `agent/graphs/ci_monitor.py` shim for symmetry. `http.app` stays `agent.webapp:app`.
4. Reviewer modules → `agent/review/` package (9 pure moves; fork reviewer content rides along).
5. CI-autofix cluster (upstream-deleted): keep-baseline; wire its webhook triggers through the split `github.py`/`common.py`; tests → `tests/github/`.
6. UI `ported/` → `features/agents/experiments/` + single tsconfig/eslint exclude glob; bun stays; delete stray `ui/pnpm-workspace.yaml`.
7. Sequencing: reorg lands **before** #1736.
## Hard rules (from fork-maintenance playbook + scoping hazards)
- **Content-source discipline:** upstream content only for the 21 "A" shims. `agent/webhooks/{github,linear,slack}.py`, `tests/conftest.py`, `tests/e2e/harness.py` exist on both sides with different content — always fork content + replicated import rewires; never `git checkout upstream` on them.
- Handlers keep **module-attribute access style** (`common.X`, `service.X`) exactly as upstream did, so the 240 `monkeypatch.setattr(webapp, ...)` test sites retarget cleanly and no re-import rebind hazard is introduced.
- Every moved test keeps its impl-side pairing (impls mostly don't move; 13 fork-only tests get the placements from scoping §2c: Atlassian webhook tests → `tests/webhooks/`, `test_atlassian_connect.py` → `tests/auth/`, client-util tests → `tests/tools/`, `test_repo_binding_isolation.py` → `tests/sandbox/`, `test_auth_error_leak.py` → `tests/auth/`, `test_app_bot_identity.py`/autofix tests → `tests/github/`).
- Verify wheel packaging ships `agent/resources/default_prompt.md` (hatchling `packages=["agent"]` should carry it — build and inspect the wheel in C1; add explicit include only if missing).
- No secrets/config placement changes anywhere in this exercise (no new env vars, no Secrets Manager/SSM changes).
- CI ladder per commit: `ruff check` + `ruff format --check` → `pytest --co -q` → full unit → E2E (Playwright + real LangGraph dev server) where the commit touches wiring; `bunx tsc --noEmit` + bun build for UI commits. `make dev` boot-check for C3 (all graphs + FastAPI app load).
## Jira linkage
No Jira issue tracks this work; per handbook `git-workflow.md`, the branch omits the issue key and uses a plain description. (If Adam cuts a ticket, rename branch to `refactor/<KEY>-domain-reorg-adoption` before the PR opens.)
## Commit sequence (branch `refactor/domain-reorg-adoption` off dev; one PR; **merge-commit merge ONLY** — squash and rebase are explicitly forbidden for this PR to preserve per-cascade bisectability; dev ruleset allows merge commits)
- **C1 — docs/resources/assets** (~1h): `git mv` `INSTALLATION.md`/`CUSTOMIZATION.md` → `docs/`, `static/` → `assets/` (fix README refs), `default_prompt.md` → `agent/resources/` + apply the `importlib.resources` loader hunk to fork's `prompt.py` + `__init__.py`. Gate: ruff + unit + **wheel build inspection**.
- **C2 — `agent/review/` package** (~half day): mv 9 reviewer/style modules per map, rewrite 38 importer files, `agent/review/__init__.py` with fork's export surface. Gate: ruff + `pytest --co` + reviewer unit suite.
- **C3 — graphs/runtime/providers shims + `langgraph.json` + CI hardening** (~2h): adopt the 21 upstream A-shims (verify each delegates to fork modules), add `agent/graphs/ci_monitor.py`, retarget `langgraph.json`; **pin `bun-version` in `.github/workflows/ci.yml`** (removes the setup-bun latest-resolution flake documented in project memory). Gate: `pytest --co` + `make dev` boots all graphs (incl. `ci_monitor`) + FastAPI app.
- **C4 — FastAPI split** (~1–1.5 days, critical): split fork `webapp.py` per approved decisions 1–2; sed handler modules `webapp.` → `common.`; retarget the 240 monkeypatch sites, `tests/conftest.py`, `tests/e2e/harness.py`; keep `webapp.py` shim. Gate: full unit + **full E2E** (the only layer catching wiring drift) + **residual-importer sweep**: `git grep -l 'agent\.webapp\|from agent import webapp'` across the whole tree must return only the shim and intentional compat references, so the shim cannot silently mask missed rewires. **`/sh-security-review` on this commit's diff. Failure path: findings are addressed as NEW commits on top of the branch (e.g. "C4a: address security-review findings") — no history rewrite or force-push once any review (security or PR) has started, preserving the review/CI trail; the PR must not merge until the review passes — no exceptions.**
- **HARD GATE:** C5 must not begin until C4 has full unit + E2E green **and** `/sh-security-review` passed. (C1–C3 may proceed independently before C4.)
- **C5 — `tests/<domain>/` moves** (~half day): mechanical map moves + remaining R<100 monkeypatch retargets + the 13 fork-only placements. Gate: `pytest --co` + full unit.
- **C6 — UI `features/` moves** (~half day–1 day): `git mv` per map incl. `ported/` → `experiments|chat`, rewrite `@/` imports (54 importers + moved files), exclude-glob swap, AgentPromptBar shim retarget, delete `ui/pnpm-workspace.yaml`. Gate: `bunx tsc --noEmit` + bun build + Playwright E2E.
- **C7 — docs + ledger + memory** (~2h): update fork `CLAUDE.md`/`README.md`/`AGENTS.md` path references (CLAUDE.md architecture sections name `agent/webapp.py` and old test paths); flip `8356eb34` → Landed in `triage.jsonl` + re-triage notes on the 8 unblocked rows + `make triage-render`. **Documentation/memory obligations (owned by the C7 executor, i.e. Claude in this conversation):** (a) update the `open-swe-upstream-triage-status` and project memory entries **including a summary of the new module layout** (`agent/{graphs,runtime,api,review,resources,webhooks/*_routes}`, `tests/<domain>/`, `ui/src/features/`); (b) perform the Confluence check — search IT-space pages for any repo module-map documentation; update it if found, and **record the outcome either way ("updated page <id>" or "no Confluence change required — no IT page documents the module map") in the PR description and in project memory**. The AWS Architecture Map is unaffected (no AWS resource changes). **Fallback per standing policy: if Confluence is inaccessible, state so explicitly in the PR description and project memory — the documentation update is then outstanding, not silently skipped.** C7's doc/ledger changes ship inside the PR, so they are covered by the PR review; memory-file updates are summarized in the PR description for the same visibility.
## Merge & deployment
- PR from `refactor/domain-reorg-adoption` → `dev`; CI green (all layers) → `@openswe` review (or Adam-delegated review) → **merge commit only** (no squash/rebase). PR review scope must explicitly name the C2 reviewer-module moves (review-automation surface) and the C4 auth-surface split so the reviewer examines both.
- `/sh-security-review` must have run on C4 with confirmed critical/highs resolved (or machine-suppressed with justification) before merge — see C4 failure path. GPT-4.1 cross-family review: not strictly triggered (no IAM/policy or Lambda-signature change — `publish_review`-style tool contracts untouched; `langgraph.json` strings are deployment manifest, not IAM), but run as **opt-in on the C4 diff** given webhook-verification code movement.
- **Post-deploy verification checklist (explicit, before declaring done):** LangGraph deployment redeploys with the retargeted `langgraph.json`; all graphs (agent, reviewer, analyzer, chat, scheduler, ci_monitor) registered; `/health` OK; `GET /connect/atlassian-connect.json` serves the descriptor; one webhook round-trip per source (GitHub PR comment, Slack mention, Linear, Jira Automation test-fire, Confluence comment) where feasible. Note: external registrations (GitHub App webhook URL, Slack event URL, Jira Automation rule, Connect descriptor URL) are verified UNCHANGED by this exercise — the HTTP surface and all routes keep their paths — so no external system updates are required; the checklist confirms this empirically rather than assuming it.
- **Rollback:** revert the merge commit restores the pre-reorg layout wholesale; because the HTTP surface and external registrations are unchanged, a rollback requires NO external-system reverts — re-verify with the same checklist after any rollback. C1–C3 independently revertible pre-merge; C5/C6 path-only.
- **Post-merge cleanup (git-workflow convention):** delete `refactor/domain-reorg-adoption` on origin after merge; delete local copies and prune worktrees.
- **Verification records:** post-deploy checklist results are recorded in the PR description (or a comment) as the traceability artifact — **responsibility: the C7 executor (Claude in this conversation)**. After any rollback, normal operation does not resume until the same checklist fully passes.
- **Confluence-outstanding follow-up:** if Confluence is inaccessible at merge time, the outstanding update is recorded in project memory with an explicit "outstanding as of <date>" marker so it surfaces at the next session on this project — it does not defer indefinitely.
- **Severity clarification for the C4 failure path:** fixes are stacked as new commits regardless of severity, full stop. If the security review reveals a *fundamental design flaw* in the split (not a fixable finding), the response is not a history rewrite — the PR is closed, the plan returns to the drafting stage, and a revised plan goes back through `/sh-plan-review` on a fresh branch.
- **Shim lifetime:** `webapp.py` (and the `agent/graphs/*` shims) mirror upstream's own permanent compatibility shims — they are kept for as long as upstream keeps theirs, since layout parity is the goal; revisit only if upstream removes them.
- **bun-version pin:** permanent policy (version bumped deliberately like any dependency), not a temporary workaround — resolves the setup-bun latest-resolution flake class permanently.
## Post-merge follow-ups (separate work, not this PR)
1. Pick #1736 (GitHub token binding) — near-clean post-split; still gated by its ledger row: GPT-4.1 cross-family review + `/sh-security-review`.
2. Re-triage the 5 now-clean picks (#1732, #1761, #1744, #1748, #1742) — cheap batch.
3. Update `sh-openswe` deployment notes/memory if the redeploy surfaces anything.
## Rollback story
Single revert of the merge commit restores pre-reorg layout wholesale. Before merge: each cascade commit is independently droppable except C4→C5 ordering (C5 assumes post-split monkeypatch targets). Working artifacts for the build agents: `docs/upstream-sync/domain-reorg/{movemap-m50.txt, cross.json, forkonly.json}`.

View file

@ -0,0 +1,163 @@
# Scoping report — adopting upstream domain reorg `8356eb34` (#1726) as a file-move exercise
Date: 2026-07-17. Repo: `/Users/adammoussa/Documents/repositories/seahaven/open-swe` (branch `dev`, clean).
Strategy scoped: **fork file contents, upstream layout** — replay the reorg's *moves* onto the fork's own tree; never take upstream file contents.
Merge-base with `upstream/main`: `f6c215ff`. Reorg commit sits 91 upstream commits past the merge-base.
Current truthful conflict surface (`git merge-tree --write-tree --name-only HEAD upstream/main`): **140 conflicted paths** (`docs/upstream-sync/domain-reorg/merge-tree-now.txt`).
---
## 1. Move-map (`git diff-tree -r -M50 --name-status 8356eb34^ 8356eb34`)
298 entries total (raw file: `docs/upstream-sync/domain-reorg/movemap-m50.txt`):
| class | count | meaning |
|---|---|---|
| **R100** pure renames | **150** | byte-identical moves (mostly `tests/*` → `tests/<domain>/`, `ui/src/{components,lib}/agents/*` → `ui/src/features/*`, `agent/reviewer_*`/`review_style_*` → `agent/review/`) |
| **R<100** rename+edit | **77** | scores R062–R099; **every sampled edit is an import-path / monkeypatch-target rewrite only** (see §Spot-checks) |
| **M** in-place edits | **49** | shim-ification (`webapp.py` 2007-line reduction, `server.py` constant extraction), import rewires in `agent/tools/*`, `agent/webhooks/*`, `evals/`, `ui/src/routes/*`, config files |
| **A** true adds | **21** | all thin structure: `agent/graphs/{agent,analyzer,chat,reviewer,scheduler}.py` (re-export shims), `agent/runtime/{constants,execution,sandbox}.py` (constants + delegating wrappers around `agent.server`), `agent/api/{app,health}.py`, `agent/webhooks/{common,github_routes,linear_routes,slack_routes}.py`, `agent/{providers,resources,review,runtime,graphs,api}/__init__.py` |
| **D** true deletes | **1** | `ui/src/components/agents/AgentPromptBar.tsx` — a 2-line re-export shim, re-added at `ui/src/features/agents/components/AgentPromptBar.tsx` with the path retargeted (A/D pair is cosmetic, not a content loss) |
Content-consolidations (real redistribution rather than move): effectively **one** — the FastAPI layer.
`agent/webapp.py` (upstream pre-reorg ~2k lines) is split into `agent/webhooks/common.py` (shared dispatch/verify helpers), `agent/api/app.py` (composition), `agent/api/health.py` (`/health` + `/webhooks/run-complete`), and per-source `*_routes.py`; `webapp.py` becomes a 4-line compatibility shim (`from .api.app import app`). Secondary micro-extractions: `server.py` → `runtime/constants.py` (`DEFAULT_LLM_MODEL_ID`, `DEFAULT_LLM_MAX_TOKENS`, `DEFAULT_RECURSION_LIMIT`, `MODEL_CALL_RECURSION_LIMIT`) and `runtime/execution.py` (`graph_loaded_for_execution`); `prompt.py` → `default_prompt.md` loaded via `importlib.resources` from `agent/resources/`.
The "consolidate reviewer modules" sub-commit is **not** a content merge — it is nine 1:1 module moves into `agent/review/` (R098–R100) plus import rewires.
`langgraph.json` **does change**: graph entrypoints retarget from `agent.server:traced_agent` / `agent.reviewer:...` / `agent.analyzer:...` / `agent.chat:...` / `agent.scheduler:...` to `agent.graphs.<name>:<same symbol>`. The targets are pure re-export shims (e.g. `agent/graphs/agent.py` = `from agent.server import get_agent, traced_agent`), so *behavior* is unchanged, but the deployment manifest strings are not. `http.app` stays `agent.webapp:app` (via the compat shim). See §Hazards (d).
---
## 2. Collision set (move-map × fork divergence)
Fork divergence since merge-base: 322 paths (130 A / 188 M / 3 D / 1 R; raw: `docs/upstream-sync/domain-reorg/fork-divergence-status.txt`). Fork-added files split against upstream trees: **67 are shared** (cherry-picked, exist upstream pre-reorg → move-map applies) and **64 are TRUE fork-only** (exist nowhere upstream).
Headline classification:
| bucket | count |
|---|---|
| MECHANICAL — fork-touched, R100 pure rename → `git mv` fork's version | **57** |
| MECHANICAL-trivial — moved by reorg, fork never touched → `git mv` (identical either way) | **127** |
| HAND-CARRY (light) — fork-touched, R<100 → `git mv` + replicate upstream's import-rewrite pattern on fork content | **34** |
| HAND-CARRY (real) — fork-modified AND reorg-modified-in-place (M×M) | **34** (≈10 substantive, rest are 1–3-line import seds) |
| FORK-ONLY placement decisions | **64** files, of which **13 test files** need explicit new homes; nearly all fork-only *source* stays put (reorg does not move `agent/tools/`, `agent/utils/`, `agent/middleware/`, `agent/integrations/`, `agent/dashboard/`, `agent/webhooks/` handler modules) |
| moved upstream but absent from fork (added in the 91 interim commits — no-op for us) | 9 |
### 2a. MECHANICAL (fork-touched, pure rename — the bulk)
`git mv` fork's file to the new path; no content change. 57 files:
- Root/docs/resources: `INSTALLATION.md → docs/INSTALLATION.md`, `default_prompt.md → agent/resources/default_prompt.md` (pairs with the `prompt.py` loader hunk — see HAND-CARRY), `agent/reviewer_findings.py → agent/review/findings.py`.
- Tests (46): the full `tests/test_*.py → tests/<domain>/test_*.py` set, e.g. `tests/test_dispatch.py → tests/agent/`, `tests/test_workflow_push_guard.py → tests/agent/`, `tests/test_plan_mode.py`, `tests/test_plan_review.py → tests/agent/`, `tests/test_auth_sources.py`, `tests/test_authorship.py`, `tests/test_encryption*→ tests/auth/`, `tests/test_github_app.py`, `tests/test_open_pull_request.py`, `tests/test_pr_creation_guard.py → tests/github/`, `tests/test_gateway.py`, `tests/test_proxy_auth.py`, `tests/test_local_integration.py → tests/sandbox/`, sanitize/middleware tests → `tests/middleware/`, model tests → `tests/models/`, Slack tool tests → `tests/slack/`, dashboard/team-settings tests → `tests/dashboard/`, `tests/test_completion_webhook.py → tests/webhooks/`, etc. (full list in `docs/upstream-sync/domain-reorg/cross.json`, key `mech`).
- UI (10): `ui/src/lib/agents/{api,queries,types,streamMessagesToUi,sidebarFilter,messageTimestamps}.ts → ui/src/features/agents/lib/`, `ui/src/components/agents/PrHeader.tsx → ui/src/features/reviews/components/`, `MessageTimestamp.tsx`, `diffUtils.ts`.
Plus the 127 fork-untouched moved files (same treatment, zero divergence risk).
### 2b. HAND-CARRY
**(i) R<100 moved files fork also touched — 34 files.** Upstream's edits are exclusively path rewires; replicate the same rewires on the fork's content after `git mv`:
- Python: `agent/reviewer_publish.py → agent/review/publish.py` (R099: `from .reviewer_findings import` → `from .findings import`, `from .utils.X` → `from ..utils.X`), same for `reviewer_reconcile.py`, `reviewer_trace_context.py`; reviewer test files R062–R099 retarget `webapp` → `webhook_common` / `github_webhooks` monkeypatch modules; `tests/test_e2b_integration.py → tests/sandbox/` (fork re-implemented E2B sync — keep fork content, fix imports only).
- UI: 20 `.tsx/.ts` files where the only edits are `@/components/agents/...` → `@/features/agents/components/...` and `@/lib/agents/...` → `@/features/agents/lib/...` (verified zero non-import edits on the worst-looking case, `AgentThreadView.tsx` R092). Fork-diverged files in this set that must keep fork content: `PlanReview.tsx` (plan-mode #130), `WorkflowApprovalCard.tsx`, `AgentsSidebar.tsx`, `SidebarFilterMenu.tsx`, `AgentGitPanel.tsx`, `AutomationEditor.tsx`.
**(ii) M×M in-place overlap — 34 files, of which the substantive ten:**
| file | upstream's reorg edit | fork action |
|---|---|---|
| `agent/webapp.py` (fork: **2,590 lines**) | gutted to 4-line shim; content → `webhooks/common.py` + `api/app.py` + `api/health.py` + `*_routes.py` | **the big one** — split the fork's monolith the same way, carrying fork-only Jira/Confluence/Connect routes (see §3) |
| `agent/server.py` | extract 4 constants → `runtime/constants.py`, `graph_loaded_for_execution` → `runtime/execution.py`; adds public aliases `get_cached_sandbox_backend` etc. | replicate extraction on fork content; fork's sync sandbox lifecycle stays in `server.py`/`utils/sandbox_state.py` untouched — `runtime/sandbox.py` shim delegates to `agent.server` (upstream's own shim does exactly this, so it wraps fork code cleanly) |
| `agent/prompt.py` | `DEFAULT_PROMPT_PATH` env-only + `importlib.resources` load of `agent/resources/default_prompt.md` | apply this one hunk by hand to fork's heavily customized `prompt.py`; **verify the wheel packages the .md** (upstream did *not* touch `pyproject.toml`; `packages=["agent"]` under hatchling should carry it — build and check) |
| `agent/reviewer.py` / `agent/analyzer.py` / `agent/chat.py` | import rewires to `agent.review.*` / small | sed-level on fork content |
| `agent/webhooks/{github,linear,slack}.py` | `from agent import webapp` → `from . import common`; `webapp.X` → `common.X` (module-attribute style preserved so monkeypatching keeps working) | **caution:** these exist on *both* sides with different content (fork built its own split earlier). No `git mv` applies — sed the fork's files; never adopt upstream's |
| `tests/conftest.py` | `from agent import webapp` → `from agent.webhooks import common as webhook_common` (auto-review fixture) | same sed on fork's conftest |
| `tests/e2e/harness.py` | `from agent.webapp import app, generate_thread_id_from_slack_thread` → `from agent.api.app import app` + `from agent.utils.thread_ids import ...` | fork already has `agent/utils/thread_ids.py` — same retarget |
| `ui/tsconfig.json` / `ui/eslint.config.js` | replace the 8-file `ported/` exclude lists with `src/features/agents/experiments/**` | adopt the glob (simplification, no behavior change); `ui/vite.config.ts` upstream edit is cosmetic churn on the fork-only mock-harness plugin — skip |
| `langgraph.json` | entrypoints → `agent.graphs.*` | retarget fork's file **and add `ci_monitor": "agent.graphs.ci_monitor:get_ci_monitor"` shim** for the fork-only graph (upstream deleted its ci-autofix cluster; fork keeps it) |
| `AGENTS.md`/`CLAUDE.md`/`README.md` | doc-path updates | fork-rewritten docs — update path references manually (CLAUDE.md architecture sections name `agent/webapp.py`, tool/test paths) |
The remaining ~24 M×M files (`agent/tools/add_finding.py` and the other reviewer tools, `agent/dashboard/{eval_jobs,review_api,review_chat_api}.py`, `evals/reviewer/*`, `ui/src/routes/*.tsx`, `agent/utils/github_org_membership.py`, `agent/middleware/settle_review_check.py`) take 1–3-line import seds (`..reviewer_findings` → `..review.findings`, `@/components/agents` → `@/features/...`).
**Blast-radius counts (fork tree):** 38 Python files import `reviewer_*`/`review_style_*` modules; 12 Python files import `agent.webapp`; **240 `monkeypatch.setattr(webapp, ...)` sites across 11 test files** must retarget to `webhook_common`/handler modules; 54 UI files import `@/components/agents` or `@/lib/agents`.
### 2c. FORK-ONLY placement proposals (64 true fork-only files)
Upstream's domain logic: tools stay flat in `agent/tools/`; util clients in `agent/utils/`; webhook *handlers* in `agent/webhooks/<source>.py` with HTTP routes in `agent/webhooks/<source>_routes.py`; tests in `tests/<domain>/`.
**Source — stays put (no move needed):**
- Atlassian tools `agent/tools/{jira_*,confluence_*}.py` (11 files) → stay (`agent/tools/` unmoved by reorg).
- `agent/utils/{jira,confluence,adf,atlassian_connect,jira_project_repo_map,confluence_space_repo_map}.py` → stay.
- Fork middleware, `agent/integrations/e2b.py`, dashboard additions, `.githooks/`, `.github/`, `scripts/`, `docs/upstream-sync/`, `docs/repo-conventions/`, `deploy/` → stay.
**Source — new homes created by the split (part of the webapp-split commit):**
- `agent/webhooks/jira.py`, `agent/webhooks/confluence.py` → stay as handler modules; **add fork-only `agent/webhooks/jira_routes.py` and `agent/webhooks/confluence_routes.py`** mirroring upstream's `github_routes.py` pattern (routes access helpers via `common.X`).
- Atlassian Connect lifecycle/descriptor routes (`/connect/*`, `GET /connect/atlassian-connect.json`) → propose `agent/webhooks/confluence_routes.py` (they are Confluence-trigger plumbing) or a dedicated `agent/webhooks/connect_routes.py` if cleaner; register in fork's `agent/api/app.py`.
**Tests — 13 fork-only/uncovered files:**
| file | proposed home |
|---|---|
| `tests/test_jira_webhook_{author,corroboration,replay}.py`, `tests/test_confluence_webhook.py`, `tests/test_linear_webhook_replay.py` | `tests/webhooks/` |
| `tests/test_atlassian_connect.py` | `tests/auth/` (JWT/qsh verification surface; alternative `tests/webhooks/`) |
| `tests/test_jira_utils.py`, `tests/test_confluence_utils.py` | `tests/tools/` (client layer backing the Jira/Confluence tools) |
| `tests/test_repo_binding_isolation.py` (TID-COLLIDE-01 guard) | `tests/sandbox/` (it exercises the sandbox/GitHub-token per-thread caches; alternative `tests/webhooks/`) |
| `tests/test_auth_error_leak.py` | `tests/auth/` |
| `tests/test_app_bot_identity.py` | `tests/github/` (bot authorship/PR attribution) |
| `tests/test_ci_autofix.py`, `tests/test_autofix_webhook.py` | `tests/github/` (upstream deleted the impl cluster; fork keeps `agent/ci_autofix.py`, `agent/ci_monitor.py` in place) |
UI fork-only: `ui/src/components/PwaUpdateToast.tsx` → `ui/src/components/` stays (not an agents-feature file); `ui/src/lib/auth-redirect*.{ts,tsx}` → stay in `ui/src/lib/` (upstream kept non-agents lib files there); `ui/src/routes/$owner.$repo.pull.$number.tsx` → stays (`routes/` not moved). `tests/e2e/**` (fork harness + specs) → stays; upstream kept `tests/e2e/` in place.
---
## 3. Consolidations / decision list for Adam
1. **FastAPI layer split (the only true consolidation) — FLAG-HUMAN, auth surface.** Fork's 2,590-line `agent/webapp.py` carries fork-only webhook auth: `verify_jira_secret` (+ optional HMAC/timestamp), Atlassian Connect JWT/qsh handling, the GitHub/Slack/Linear signature paths, token-attribution gating, TID-COLLIDE-01 thread-binding. Splitting it into `webhooks/common.py` + `api/` + `*_routes.py` moves signature-verification and dispatch code across files. Recommendation: **adopt the split** (it is behavior-preserving by construction — upstream kept module-attribute access precisely so monkeypatched tests stay valid, and the fork's webhook tests are the safety net), but treat the split commit as a sensitive change: **/sh-security-review required** (auth/webhook surface), and per the fork playbook surface the resolution choice to you before merge. No GPT-4.1 cross-family review strictly triggered (no IAM/Lambda-signature change), but the split of webhook verification code is a reasonable opt-in candidate.
2. **`langgraph.json` entrypoint retarget** — deployment-manifest change (graphs → `agent.graphs.*` shims; `http.app` unchanged). Verify the LangGraph deployment redeploys cleanly; decide whether fork-only `ci_monitor` gets a `agent/graphs/ci_monitor.py` shim (recommended, for symmetry) or keeps its old path (inconsistent).
3. **Reviewer consolidation** → `agent/review/` package: adopt. It is 9 pure moves + import rewires; fork's reviewer customizations (#1713 reconciliation, findings model, publish flow) ride along untouched. No content decision needed.
4. **CI-autofix cluster** (upstream deleted `agent/ci_autofix.py`; fork keeps it): confirm keep-baseline, and approve its post-split wiring point (its webhook triggers live in the split `github.py`/`common.py`) + test placement (`tests/github/`).
5. **UI `experiments/` adoption**: upstream renames `ported/` → `features/agents/experiments/` + `chat/` and swaps the 8-file tsconfig/eslint exclude lists for one glob. Adopt (pure simplification). Fork-diverged `DiffView.tsx`, `CloudPromptBar.tsx`, etc. keep fork content.
6. **Atlassian placements** (§2c): approve `jira_routes.py`/`confluence_routes.py` naming and where Connect lifecycle routes land — this is fork-only auth surface; your call on `connect_routes.py` vs folding into `confluence_routes.py`.
7. **Sequencing vs #1736 (bind cached GitHub tokens to users — priority security pick)**: recommend adopting the reorg **first**, because #1736 is written against `agent/webhooks/common.py`, which only exists after the split; it then lands near-clean (still gated by cross-review + /sh-security-review per its ledger row).
## 4. Hazards check (CLAUDE.md playbook)
- **(a) Thin-router re-import rebinds:** post-reorg upstream is *clean* — `git grep 'from \.webhooks\.' 8356eb34 -- agent/webapp.py agent/api agent/webhooks/common.py` returns nothing; routes access handlers via `from . import github as service` + `service.X`/`common.X` module-attribute style, so the rebind hazard the playbook documents is designed out. Residual risk only if a hand-carry file drifts toward upstream *content*: `agent/webhooks/{github,linear,slack}.py` exist on both sides with different content (fork versions carry the fixes) — taking upstream's file here silently drops fork behavior. Same for `tests/conftest.py` and `tests/e2e/harness.py`. Rule for the exercise: **upstream content may only be taken for the 21 "A" structural shims**, everything else moves fork content.
- **(b) Paired tests move with impls:** the map's test moves are 1:1 with unmoved impls (impl modules stay importable at old or shimmed paths), so no vertical splits arise; the 13 uncovered fork tests get the placements in §2c. Keep `tests/test_e2b_integration.py` (fork's sync E2B) as fork content when moving to `tests/sandbox/`. `tests/middleware/test_sandbox_recovery.py` → `tests/sandbox/` (fork has `tests/middleware/`? — it exists only as the upstream pre-path; fork's copy sits wherever it was cherry-picked; include in the mechanical sweep).
- **(c) Tooling path references:** verified —
- `Makefile`: only generic `tests/` (`TEST_FILE ?= tests/`) — unaffected; `make triage-render/-check` scripts don't reference moved paths.
- `pyproject.toml`: `packages = ["agent"]`, `testpaths = ["tests"]` — both recursive, unaffected; **verify wheel includes `agent/resources/default_prompt.md`** (only real packaging check).
- `.github/workflows/ci.yml`: references `tests/e2e/**` (unmoved) and `ui/` bun checks — unaffected; `reviewer-eval.yml` → `evals/` (unmoved).
- `langgraph.json` — changes (see decision 2); fork-only `tests/e2e/langgraph.e2e.json` uses its own `agent_entrypoint.py` (imports `agent.server:traced_agent`, which remains valid) — no change needed.
- UI: `@` alias root (`src/`) unchanged; `vite.config.ts`/`tsconfig.json`/`eslint.config.js` need only the exclude-glob swap; **bun stays** — do not import upstream's pnpm bits (fork oddly carries a `ui/pnpm-workspace.yaml`; consider deleting it in this exercise as lockfile hygiene, per playbook rule 7).
- `scripts/`, `.githooks/`: no moved-path references found.
- **(d) Public entrypoints:** upstream's claim "without changing its public entrypoints" is true **behaviorally, false textually** — `langgraph.json` graph strings all change to `agent.graphs.*` (verified by diff). HTTP surface (`agent.webapp:app`) and all webhook URLs unchanged. Fork must mirror the graph-string change plus its `ci_monitor` extra.
## 5. Payoff — the 20 deferred ledger rows (docs/upstream-sync/triage.md)
Of the 19 deferred rows besides `8356eb34` itself:
- **Clean picks after adoption (path remap was the blocker): 5** — #1732 dashboard label rendering, #1761 capitalize labels, #1744 collapse git panel, #1748 Linear issue search (test-path remap), #1742 defensive copy.
- **Near-clean after adoption (small content deltas or review gates remain): 3** — **#1736 GitHub token binding** (biggest beneficiary: written against `webhooks/common.py`; auth gates still apply), #1758 sandbox-create JSON env, #1760 separate LangSmith key/endpoint (both additive to diverged `integrations/langsmith.py` — small reconciles).
- **Still content reconciliation for fork-divergence reasons: 9** — #1724 (Slack NL plan approvals), #1731 (OpenAI Responses history vs #155 middleware), #1734 (Slack DMs), #1737 (reviewer diff bounds), #1741 (SSRF image fetches), #1733 (todos default-off vs fork prompt), #1735 (untagged two-party Slack), #1747 (Slack run links), #1719 (web-tool offload vs sync sandbox lifecycle). Adoption still removes their test/UI path-remap tax.
- **Not path-related (product/deps decisions): 2** — #1727 (GPT-5.5 default; rides the no-OpenAI-models decision), #1745 (deepagents 0.7.0a7 alpha validation).
Net: **8 of 19 rows become clean/near-clean**, and *every future upstream commit* stops needing path remaps — the standing tax on the cherry-pick pipeline disappears (recent won't-merge rows #1759/#1769 cited the layout mismatch explicitly).
## 6. Execution plan sketch (one commit per cascade class; validate ruff → `pytest --co` → unit → e2e per layer)
Work on a throwaway branch off `dev`; promote as one PR with granular commits.
1. **C1 — docs/resources/assets** (~1h): `git mv` `INSTALLATION.md`/`CUSTOMIZATION.md` → `docs/`, `static/` → `assets/` (check README badge/image refs), `default_prompt.md` → `agent/resources/` + apply the `prompt.py` importlib.resources hunk + `agent/resources/__init__.py`; build wheel, confirm the .md ships. Gate: ruff + unit.
2. **C2 — `agent/review/` package** (~half day): mv 9 reviewer/style modules, rewrite the 38 importer files, add `agent/review/__init__.py` (fork's export surface). Gate: ruff + `pytest --co` + reviewer unit tests.
3. **C3 — graphs/runtime/providers shims + `langgraph.json`** (~2h): take upstream's 21 structural A-files nearly verbatim (they delegate to `agent.server` etc., which is fork code), add fork-only `agent/graphs/ci_monitor.py`, retarget `langgraph.json`. Gate: `pytest --co` + `make dev` boots all graphs. **Adam sign-off: decision 2.**
4. **C4 — FastAPI split** (~1–1.5 days, the critical commit): split fork's `webapp.py` → `webhooks/common.py` + `api/{app,health}.py` + `{github,linear,slack,jira,confluence}_routes.py` (+ Connect routes per decision 6); sed handlers `webapp.` → `common.`; retarget 240 test monkeypatch sites + `tests/conftest.py` + `tests/e2e/harness.py`; keep `webapp.py` shim. Gate: full unit + **E2E (Playwright + real LangGraph dev server)** — the only layer that catches wiring drift. **Adam sign-offs: decisions 1, 4, 6; /sh-security-review on this commit.**
5. **C5 — tests/<domain>/ moves** (~half day): mechanical map moves (§2a) + R<100 monkeypatch retargets not already done in C4 + the 13 fork-only placements (§2c). Gate: `pytest --co` + full unit (paths only, no logic).
6. **C6 — UI features/ moves** (~half day–1 day): `git mv` per map incl. `ported/` → `experiments|chat`, rewrite `@/` imports in the 54 importer files + moved files, tsconfig/eslint exclude glob, AgentPromptBar shim retarget, delete `ui/pnpm-workspace.yaml` (lockfile hygiene). Gate: `bunx tsc --noEmit` + bun build + Playwright e2e. **Adam sign-off: decision 5.**
7. **C7 — docs + ledger + memory** (~2h): update fork `CLAUDE.md`/`README`/`AGENTS.md` path references, flip `8356eb34` to Landed in `triage.jsonl` (+ re-triage notes on the 8 unblocked rows), update the open-swe project memory, Confluence architecture page if module map is documented there.
**Effort estimate: 3–5 focused days** end-to-end, dominated by C4. Rollback story is clean: each commit is independently revertable until C4; after C4, C5/C6 are path-only.
## Spot-checks validating the classification method
1. **Mechanical** — `tests/test_dispatch.py → tests/agent/test_dispatch.py` listed `R100` in the diff-tree output (byte-identical move); fork's diverged copy moves via `git mv` untouched.
2. **Hand-carry (R<100)** — `agent/reviewer_publish.py → agent/review/publish.py` (R099): upstream's entire edit is the import block (`from .reviewer_findings import` → `from .findings import`, `from .utils.github_http import` → `from ..utils.github_http import`); no logic lines. Worst-scoring test `tests/test_pr_ready_auto_review.py` (R062): every hunk is `monkeypatch.setattr(webapp, ...)` → `monkeypatch.setattr(webhook_common, ...)` / `webapp.process_github_pr_ready` → `github_webhooks.process_github_pr_ready`. Worst-scoring fork-diverged UI file `AgentThreadView.tsx` (R092): grep of the rename diff shows 0 non-import changed lines.
3. **Consolidation** — post-reorg `agent/webapp.py` is exactly `from .api.app import app` (4 lines, verified via `git show 8356eb34:agent/webapp.py`); `agent/webhooks/github.py`'s reorg diff swaps `from agent import webapp` for `from . import common` and rewrites `webapp.X` → `common.X` with the docstring explicitly noting module-attribute access is preserved for monkeypatching.
4. **Test pairing** — `tests/conftest.py` reorg diff retargets the autouse auto-review fixture from `agent.webapp` to `agent.webhooks.common`, confirming test fixtures follow the split, not the moves.
Working artifacts: `docs/upstream-sync/domain-reorg/{movemap-m50.txt, fork-divergence-status.txt, cross.json, forkonly.json, merge-tree-now.txt, head-tree.txt, upstream-pre-tree.txt, upstream-main-tree.txt}`.

View file

@ -0,0 +1,568 @@
.codespellignore
.dockerignore
.github/dependabot.yml
.github/workflows/ci.yml
.github/workflows/pr_lint.yml
.github/workflows/promote_main_to_prod.yml
.github/workflows/reviewer_eval.yml
.gitignore
AGENTS.md
CLAUDE.md
Dockerfile
LICENSE
Makefile
README.md
SECURITY.md
agent/analyzer.py
agent/api/__init__.py
agent/api/app.py
agent/api/health.py
agent/chat.py
agent/completion.py
agent/dashboard/__init__.py
agent/dashboard/admin.py
agent/dashboard/agent_instructions.py
agent/dashboard/agent_overrides.py
agent/dashboard/agent_usage.py
agent/dashboard/analyzer_cron.py
agent/dashboard/autofix_state.py
agent/dashboard/enabled_repos.py
agent/dashboard/eval_jobs.py
agent/dashboard/notion_oauth.py
agent/dashboard/oauth.py
agent/dashboard/options.py
agent/dashboard/plan_api.py
agent/dashboard/plan_store.py
agent/dashboard/pr_diff.py
agent/dashboard/profiles.py
agent/dashboard/repo_access.py
agent/dashboard/repo_snapshots.py
agent/dashboard/review_api.py
agent/dashboard/review_chat_api.py
agent/dashboard/review_style_jobs.py
agent/dashboard/review_styles.py
agent/dashboard/routes.py
agent/dashboard/schedules.py
agent/dashboard/slack_oauth.py
agent/dashboard/team_credentials.py
agent/dashboard/team_settings.py
agent/dashboard/thread_api.py
agent/dashboard/user_credentials.py
agent/dashboard/user_mappings.py
agent/dashboard/workflow_approval.py
agent/dashboard/workflow_approval_api.py
agent/dispatch.py
agent/encryption.py
agent/graphs/__init__.py
agent/graphs/agent.py
agent/graphs/analyzer.py
agent/graphs/chat.py
agent/graphs/reviewer.py
agent/graphs/scheduler.py
agent/integrations/__init__.py
agent/integrations/corridor_mcp.py
agent/integrations/currents_tools.py
agent/integrations/datadog_mcp.py
agent/integrations/daytona.py
agent/integrations/e2b.py
agent/integrations/langsmith.py
agent/integrations/langsmith_tools.py
agent/integrations/local.py
agent/integrations/modal.py
agent/integrations/notion_mcp.py
agent/integrations/runloop.py
agent/integrations/stagehand_browser.py
agent/middleware/__init__.py
agent/middleware/check_message_queue.py
agent/middleware/ensure_no_empty_msg.py
agent/middleware/exclude_tools.py
agent/middleware/model_fallback.py
agent/middleware/notify_step_limit.py
agent/middleware/plan_mode.py
agent/middleware/pr_creation_guard.py
agent/middleware/prepare_run.py
agent/middleware/refresh_github_proxy.py
agent/middleware/refresh_slack_status.py
agent/middleware/repair_orphaned_tool_calls.py
agent/middleware/sandbox_circuit_breaker.py
agent/middleware/sanitize_fireworks_messages.py
agent/middleware/sanitize_thinking_blocks.py
agent/middleware/sanitize_tool_inputs.py
agent/middleware/settle_review_check.py
agent/middleware/subdir_agents.py
agent/middleware/task_retry.py
agent/middleware/timeout_wrapup.py
agent/middleware/tool_artifact.py
agent/middleware/tool_error_handler.py
agent/middleware/workflow_push_guard.py
agent/prompt.py
agent/providers/__init__.py
agent/reconcile.py
agent/resources/__init__.py
agent/resources/default_prompt.md
agent/review/__init__.py
agent/review/diff.py
agent/review/eval_store.py
agent/review/findings.py
agent/review/groups.py
agent/review/publish.py
agent/review/reconcile.py
agent/review/style_collector.py
agent/review/style_guidance.py
agent/review/trace_context.py
agent/reviewer.py
agent/runtime/__init__.py
agent/runtime/constants.py
agent/runtime/execution.py
agent/runtime/sandbox.py
agent/scheduler.py
agent/server.py
agent/skills/bootstrap-repo-analysis/SKILL.md
agent/skills/continual-learning/SKILL.md
agent/tools/__init__.py
agent/tools/_sandbox_output.py
agent/tools/add_finding.py
agent/tools/enter_plan_mode.py
agent/tools/fetch_review_diff.py
agent/tools/fetch_url.py
agent/tools/http_request.py
agent/tools/linear_comment.py
agent/tools/linear_create_issue.py
agent/tools/linear_delete_issue.py
agent/tools/linear_get_issue.py
agent/tools/linear_get_issue_comments.py
agent/tools/linear_list_teams.py
agent/tools/linear_search_issues.py
agent/tools/linear_update_issue.py
agent/tools/list_findings.py
agent/tools/list_review_findings.py
agent/tools/open_pull_request.py
agent/tools/publish_review.py
agent/tools/read_finding_outcomes.py
agent/tools/read_repo_file.py
agent/tools/reply_to_finding_thread.py
agent/tools/report_platform_issue.py
agent/tools/request_pr_review.py
agent/tools/resolve_finding_thread.py
agent/tools/save_plan.py
agent/tools/save_review_style.py
agent/tools/schedule_thread_wakeup.py
agent/tools/search_repo_code.py
agent/tools/slack_add_reaction.py
agent/tools/slack_read_thread_messages.py
agent/tools/slack_start_new_thread.py
agent/tools/slack_thread_reply.py
agent/tools/update_finding.py
agent/tools/web_search.py
agent/utils/agents_md.py
agent/utils/analyzer_skills.py
agent/utils/api_standards_skill.py
agent/utils/auth.py
agent/utils/authorship.py
agent/utils/comments.py
agent/utils/dashboard_handoff.py
agent/utils/dashboard_links.py
agent/utils/deferred_model.py
agent/utils/gateway.py
agent/utils/github_app.py
agent/utils/github_checks.py
agent/utils/github_ci.py
agent/utils/github_comments.py
agent/utils/github_feedback.py
agent/utils/github_http.py
agent/utils/github_org_membership.py
agent/utils/github_proxy.py
agent/utils/github_token.py
agent/utils/http.py
agent/utils/json_types.py
agent/utils/langsmith.py
agent/utils/linear.py
agent/utils/linear_team_repo_map.py
agent/utils/model.py
agent/utils/multimodal.py
agent/utils/repo.py
agent/utils/repo_prep.py
agent/utils/reviewer_outcomes.py
agent/utils/sandbox.py
agent/utils/sandbox_paths.py
agent/utils/sandbox_state.py
agent/utils/slack.py
agent/utils/slack_feedback.py
agent/utils/thread_ids.py
agent/utils/thread_ops.py
agent/utils/tracing.py
agent/utils/ttl_cache.py
agent/utils/url_safety.py
agent/webapp.py
agent/webhooks/__init__.py
agent/webhooks/common.py
agent/webhooks/github.py
agent/webhooks/github_routes.py
agent/webhooks/linear.py
agent/webhooks/linear_routes.py
agent/webhooks/slack.py
agent/webhooks/slack_routes.py
assets/dark.svg
assets/light.svg
docs/CUSTOMIZATION.md
docs/INSTALLATION.md
evals/reviewer/README.md
evals/reviewer/build_dataset.py
evals/reviewer/config.toml
evals/reviewer/golden_comments/cal_dot_com.json
evals/reviewer/golden_comments/discourse.json
evals/reviewer/golden_comments/grafana.json
evals/reviewer/golden_comments/keycloak.json
evals/reviewer/golden_comments/sentry.json
evals/reviewer/judge.py
evals/reviewer/run_eval.py
evals/reviewer/store_reporter.py
evals/reviewer/target.py
langgraph.json
package.json
pyproject.toml
scripts/__init__.py
scripts/check_pr_merge_status.py
scripts/create_sandbox_snapshot.py
scripts/list_snapshots.py
scripts/purge_wakeup_crons.py
tests/agent/test_agent_assembly_context.py
tests/agent/test_agent_instructions.py
tests/agent/test_agent_schedules.py
tests/agent/test_agent_thread_pr_state.py
tests/agent/test_agent_usage.py
tests/agent/test_agents_md.py
tests/agent/test_api_standards_skill.py
tests/agent/test_dispatch.py
tests/agent/test_import_hygiene.py
tests/agent/test_langsmith_trace_url.py
tests/agent/test_multimodal.py
tests/agent/test_plan_mode.py
tests/agent/test_plan_review.py
tests/agent/test_prompt_default_repo.py
tests/agent/test_repo_prep.py
tests/agent/test_task_retry.py
tests/agent/test_timeout_wrapup.py
tests/agent/test_workflow_push_guard.py
tests/analyzer/test_analyzer_cron.py
tests/analyzer/test_analyzer_skills.py
tests/analyzer/test_eval_jobs.py
tests/analyzer/test_eval_store_reporter.py
tests/auth/test_auth_sources.py
tests/auth/test_authorship.py
tests/auth/test_encryption.py
tests/auth/test_github_oauth_refresh.py
tests/auth/test_github_token_ttl.py
tests/auth/test_notion_oauth.py
tests/auth/test_slack_oauth.py
tests/auth/test_user_credentials.py
tests/conftest.py
tests/dashboard/test_account_link.py
tests/dashboard/test_autofix_state.py
tests/dashboard/test_dashboard_admin.py
tests/dashboard/test_dashboard_csrf.py
tests/dashboard/test_dashboard_links.py
tests/dashboard/test_dashboard_oauth_redirect.py
tests/dashboard/test_dashboard_org_login_gate.py
tests/dashboard/test_dashboard_repo_optional.py
tests/dashboard/test_dashboard_repos.py
tests/dashboard/test_dashboard_reviews.py
tests/dashboard/test_dashboard_run_email.py
tests/dashboard/test_dashboard_thread_api.py
tests/dashboard/test_dashboard_thread_api_activity.py
tests/dashboard/test_dashboard_web_handoff.py
tests/dashboard/test_public_repo_org_gate.py
tests/dashboard/test_team_credentials.py
tests/dashboard/test_team_settings_fable.py
tests/dashboard/test_team_settings_grouping.py
tests/dashboard/test_team_settings_org_guidelines.py
tests/dashboard/test_user_mappings.py
tests/dashboard/test_workflow_approval_api.py
tests/e2e/.gitignore
tests/e2e/README.md
tests/e2e/agent_entrypoint.py
tests/e2e/dev-mock.sh
tests/e2e/e2e_env.py
tests/e2e/fake_llm.py
tests/e2e/fakes.py
tests/e2e/global-setup.ts
tests/e2e/harness.py
tests/e2e/langgraph.e2e.json
tests/e2e/package-lock.json
tests/e2e/package.json
tests/e2e/patches.py
tests/e2e/playwright.config.ts
tests/e2e/screenshots/queued-messages-dashboard.png
tests/e2e/static/github.html
tests/e2e/static/slack.html
tests/e2e/tests/dashboard.spec.ts
tests/e2e/tests/full_flow.spec.ts
tests/e2e/tests/plan_review.spec.ts
tests/e2e/tests/sandbox_id.spec.ts
tests/e2e/tests/slack_debounce.spec.ts
tests/e2e/tests/slack_untagged_reply.spec.ts
tests/github/test_github_app.py
tests/github/test_github_checks.py
tests/github/test_github_ci.py
tests/github/test_github_comment_prompts.py
tests/github/test_github_feedback.py
tests/github/test_github_http.py
tests/github/test_github_issue_webhook.py
tests/github/test_github_proxy_refresh.py
tests/github/test_normalize_repo.py
tests/github/test_open_pull_request.py
tests/github/test_pr_creation_guard.py
tests/github/test_repo_extraction.py
tests/middleware/test_check_message_queue.py
tests/middleware/test_ensure_no_empty_msg.py
tests/middleware/test_model_fallback_middleware.py
tests/middleware/test_notify_step_limit_middleware.py
tests/middleware/test_prepare_run_middleware.py
tests/middleware/test_repair_orphaned_tool_calls.py
tests/middleware/test_sanitize_fireworks_messages.py
tests/middleware/test_sanitize_thinking_blocks.py
tests/middleware/test_sanitize_tool_inputs.py
tests/middleware/test_subdir_agents_middleware.py
tests/middleware/test_tool_artifact_middleware.py
tests/models/test_agent_subagent_models.py
tests/models/test_anthropic_effort.py
tests/models/test_anthropic_model.py
tests/models/test_deferred_model.py
tests/models/test_fireworks_model.py
tests/models/test_google_model.py
tests/models/test_model_fallback_resolution.py
tests/reviewer/test_factory_config_isolation.py
tests/reviewer/test_fetch_review_diff_tool.py
tests/reviewer/test_pr_ready_auto_review.py
tests/reviewer/test_recent_comments.py
tests/reviewer/test_reconcile_sweep.py
tests/reviewer/test_review_api.py
tests/reviewer/test_review_chat.py
tests/reviewer/test_review_style_collector.py
tests/reviewer/test_review_style_sync.py
tests/reviewer/test_review_styles_store.py
tests/reviewer/test_reviewer.py
tests/reviewer/test_reviewer_diff.py
tests/reviewer/test_reviewer_eval_judge.py
tests/reviewer/test_reviewer_eval_run.py
tests/reviewer/test_reviewer_eval_target.py
tests/reviewer/test_reviewer_findings.py
tests/reviewer/test_reviewer_groups.py
tests/reviewer/test_reviewer_outcomes.py
tests/reviewer/test_reviewer_publish.py
tests/reviewer/test_reviewer_reconcile.py
tests/reviewer/test_reviewer_tools.py
tests/reviewer/test_reviewer_trace_context.py
tests/reviewer/test_reviewer_watch.py
tests/sandbox/test_daytona_integration.py
tests/sandbox/test_e2b_integration.py
tests/sandbox/test_gateway.py
tests/sandbox/test_langsmith_sandbox_config.py
tests/sandbox/test_langsmith_sandbox_timeout.py
tests/sandbox/test_local_integration.py
tests/sandbox/test_proxy_auth.py
tests/sandbox/test_repo_snapshots.py
tests/sandbox/test_sandbox_paths.py
tests/sandbox/test_sandbox_recovery.py
tests/sandbox/test_sandbox_state.py
tests/sandbox/test_stale_sandbox_creating.py
tests/slack/test_refresh_slack_status_middleware.py
tests/slack/test_slack_add_reaction_tool.py
tests/slack/test_slack_assistants_status.py
tests/slack/test_slack_context.py
tests/slack/test_slack_feedback.py
tests/slack/test_slack_start_new_thread_tool.py
tests/slack/test_slack_thread_reply_tool.py
tests/slack/test_slack_webhook_errors.py
tests/test_openai_responses_replay.py
tests/test_tool_output_offloading.py
tests/tools/test_corridor_mcp.py
tests/tools/test_currents_tools.py
tests/tools/test_http_security.py
tests/tools/test_linear_search_issues.py
tests/tools/test_observability_tools.py
tests/tools/test_report_platform_issue_tool.py
tests/tools/test_schedule_thread_wakeup.py
tests/tools/test_stagehand_browser.py
tests/webhooks/test_completion_webhook.py
tests/webhooks/test_linear_webhook_author.py
ui/.cta.json
ui/.gitignore
ui/.prettierignore
ui/.prettierrc
ui/AGENTS.md
ui/README.md
ui/assets/LangChain_Symbol_LightBlue.png
ui/assets/ic_launcher_round.png
ui/assets/playstore-icon.png
ui/components.json
ui/eslint.config.js
ui/package.json
ui/pnpm-lock.yaml
ui/pnpm-workspace.yaml
ui/public/apple-touch-icon.png
ui/public/favicon.png
ui/public/gh-open-in-open-swe-dark.svg
ui/public/gh-open-in-open-swe-light.svg
ui/public/logo-mark.png
ui/public/logo512.png
ui/public/robots.txt
ui/src/client.tsx
ui/src/components/AgentInstructionsPanel.tsx
ui/src/components/AppShell.tsx
ui/src/components/AppSidebar.tsx
ui/src/components/InstructionsEditor.tsx
ui/src/components/RepoSnapshotsPanel.tsx
ui/src/components/ReviewStylesPanel.tsx
ui/src/components/SidebarUserMenu.tsx
ui/src/components/sidebar-layout.tsx
ui/src/components/ui/avatar.tsx
ui/src/components/ui/badge.tsx
ui/src/components/ui/button.tsx
ui/src/components/ui/card.tsx
ui/src/components/ui/combobox.tsx
ui/src/components/ui/input-group.tsx
ui/src/components/ui/input.tsx
ui/src/components/ui/label.tsx
ui/src/components/ui/select.tsx
ui/src/components/ui/separator.tsx
ui/src/components/ui/skeleton.tsx
ui/src/components/ui/switch.tsx
ui/src/components/ui/textarea.tsx
ui/src/features/agents/components/AgentGitPanel.tsx
ui/src/features/agents/components/AgentPromptBar.tsx
ui/src/features/agents/components/AgentRunCard.tsx
ui/src/features/agents/components/AgentThreadView.tsx
ui/src/features/agents/components/AgentsHome.tsx
ui/src/features/agents/components/AgentsSidebar.tsx
ui/src/features/agents/components/AgentsThreadsPage.tsx
ui/src/features/agents/components/OnboardingDialog.tsx
ui/src/features/agents/components/PlanReview.tsx
ui/src/features/agents/components/SidebarFilterMenu.tsx
ui/src/features/agents/components/WorkflowApprovalCard.tsx
ui/src/features/agents/components/chat/CloudPromptBar.tsx
ui/src/features/agents/components/chat/CodeBlock.tsx
ui/src/features/agents/components/chat/DiffView.tsx
ui/src/features/agents/components/chat/Logo.tsx
ui/src/features/agents/components/chat/Markdown.tsx
ui/src/features/agents/components/chat/ReplyCard.tsx
ui/src/features/agents/components/chat/ShellCommand.tsx
ui/src/features/agents/components/chat/ToolExecution.test.ts
ui/src/features/agents/components/chat/ToolExecution.tsx
ui/src/features/agents/components/chat/toolExecutionDisplay.ts
ui/src/features/agents/components/messages/AgentMessage.tsx
ui/src/features/agents/components/messages/ChunkRenderer.tsx
ui/src/features/agents/components/messages/MessageTimestamp.tsx
ui/src/features/agents/components/messages/Messages.tsx
ui/src/features/agents/components/messages/ReasoningBlock.tsx
ui/src/features/agents/components/messages/ThinkingSpinner.tsx
ui/src/features/agents/components/messages/TurnChangedFilesCard.tsx
ui/src/features/agents/components/messages/UserMessage.tsx
ui/src/features/agents/components/messages/WorkSummary.tsx
ui/src/features/agents/components/messages/index.ts
ui/src/features/agents/components/messages/renderItems.ts
ui/src/features/agents/components/messages/summarizeChangedFiles.ts
ui/src/features/agents/components/messages/types.ts
ui/src/features/agents/components/subagents/SubagentActivity.tsx
ui/src/features/agents/components/subagents/SubagentCard.tsx
ui/src/features/agents/components/subagents/SubagentGroup.tsx
ui/src/features/agents/components/subagents/index.ts
ui/src/features/agents/components/z-index.ts
ui/src/features/agents/experiments/BranchSelector.tsx
ui/src/features/agents/experiments/BubblePrefix.tsx
ui/src/features/agents/experiments/ChatView.tsx
ui/src/features/agents/experiments/CompactingIndicator.tsx
ui/src/features/agents/experiments/ContextIndicator.tsx
ui/src/features/agents/experiments/Footer.tsx
ui/src/features/agents/experiments/HeaderBar.tsx
ui/src/features/agents/experiments/MarkdownTable.tsx
ui/src/features/agents/experiments/PanelResizeHandle.tsx
ui/src/features/agents/experiments/PromptBar.tsx
ui/src/features/agents/experiments/SourceControlPanel.tsx
ui/src/features/agents/experiments/SourceControlTile.tsx
ui/src/features/agents/experiments/ThreadPicker.tsx
ui/src/features/agents/experiments/TodoList.tsx
ui/src/features/agents/experiments/index.ts
ui/src/features/agents/lib/AgentThreadStreamProvider.tsx
ui/src/features/agents/lib/api.ts
ui/src/features/agents/lib/gitPanelPreferences.test.ts
ui/src/features/agents/lib/gitPanelPreferences.ts
ui/src/features/agents/lib/messageTimestamps.ts
ui/src/features/agents/lib/provider/useIsInAgentThreadStream.tsx
ui/src/features/agents/lib/provider/useLiveMarkdownMessageId.ts
ui/src/features/agents/lib/provider/useModelOptions.ts
ui/src/features/agents/lib/provider/useSubmitAgentMessage.ts
ui/src/features/agents/lib/queries.ts
ui/src/features/agents/lib/sidebarFilter.test.ts
ui/src/features/agents/lib/sidebarFilter.ts
ui/src/features/agents/lib/sidebarPrefs.ts
ui/src/features/agents/lib/streamMessagesToUi.ts
ui/src/features/agents/lib/toolNames.ts
ui/src/features/agents/lib/types.ts
ui/src/features/agents/lib/useRunCompletionNotifier.ts
ui/src/features/agents/utils/diffStats.ts
ui/src/features/agents/utils/diffUtils.test.ts
ui/src/features/agents/utils/diffUtils.ts
ui/src/features/automations/components/AutomationEditor.tsx
ui/src/features/automations/components/AutomationTemplates.tsx
ui/src/features/automations/components/AutomationsList.tsx
ui/src/features/automations/components/ScheduleTriggerPicker.tsx
ui/src/features/automations/lib/automation-templates.ts
ui/src/features/automations/lib/cron.ts
ui/src/features/reviews/components/PrHeader.tsx
ui/src/features/reviews/components/ReviewChat.tsx
ui/src/features/reviews/components/ReviewCommentsMenu.tsx
ui/src/features/reviews/components/ReviewMainBody.tsx
ui/src/features/reviews/components/ReviewSidebar.tsx
ui/src/features/reviews/components/ReviewTab.tsx
ui/src/features/settings/components/RepoSelector.tsx
ui/src/lib/api.ts
ui/src/lib/auth-redirect-core.ts
ui/src/lib/auth-redirect.test.ts
ui/src/lib/auth-redirect.tsx
ui/src/lib/hotkeys.ts
ui/src/lib/notifications.ts
ui/src/lib/plan.ts
ui/src/lib/profile.ts
ui/src/lib/query.ts
ui/src/lib/repo.test.ts
ui/src/lib/repo.ts
ui/src/lib/reviewImage.test.ts
ui/src/lib/session.ts
ui/src/lib/theme.ts
ui/src/lib/useIsMobile.ts
ui/src/lib/utils.ts
ui/src/logo.svg
ui/src/routeTree.gen.ts
ui/src/router.tsx
ui/src/routes/$owner.$repo.pull.$number.tsx
ui/src/routes/__root.tsx
ui/src/routes/admin.tsx
ui/src/routes/admin_.evals.tsx
ui/src/routes/agents.tsx
ui/src/routes/agents/$threadId.tsx
ui/src/routes/agents/$threadId_.plan.tsx
ui/src/routes/agents/automations/$scheduleId.tsx
ui/src/routes/agents/automations/index.tsx
ui/src/routes/agents/automations/new.tsx
ui/src/routes/agents/index.tsx
ui/src/routes/agents/reviews/$owner.$repo.$number.tsx
ui/src/routes/agents/reviews/index.tsx
ui/src/routes/agents/threads.tsx
ui/src/routes/agents_.instructions.tsx
ui/src/routes/agents_.snapshots.tsx
ui/src/routes/cloud-agents.tsx
ui/src/routes/index.tsx
ui/src/routes/integrations.tsx
ui/src/routes/login.tsx
ui/src/routes/my-settings.tsx
ui/src/routes/review.tsx
ui/src/routes/review_.repositories.$owner.tsx
ui/src/routes/review_.styles.tsx
ui/src/routes/usage.tsx
ui/src/styles.css
ui/src/styles/agents.css
ui/tsconfig.json
ui/vercel.json
ui/vite.config.ts
uv.lock

View file

@ -0,0 +1,535 @@
.codespellignore
.dockerignore
.github/dependabot.yml
.github/workflows/ci.yml
.github/workflows/pr_lint.yml
.github/workflows/promote_main_to_prod.yml
.github/workflows/reviewer_eval.yml
.gitignore
.vscode/settings.json
AGENTS.md
CLAUDE.md
CUSTOMIZATION.md
Dockerfile
INSTALLATION.md
LICENSE
Makefile
README.md
SECURITY.md
agent/analyzer.py
agent/chat.py
agent/completion.py
agent/dashboard/__init__.py
agent/dashboard/admin.py
agent/dashboard/agent_instructions.py
agent/dashboard/agent_overrides.py
agent/dashboard/agent_usage.py
agent/dashboard/analyzer_cron.py
agent/dashboard/autofix_state.py
agent/dashboard/enabled_repos.py
agent/dashboard/eval_jobs.py
agent/dashboard/notion_oauth.py
agent/dashboard/oauth.py
agent/dashboard/options.py
agent/dashboard/plan_api.py
agent/dashboard/plan_store.py
agent/dashboard/pr_diff.py
agent/dashboard/profiles.py
agent/dashboard/repo_access.py
agent/dashboard/repo_snapshots.py
agent/dashboard/review_api.py
agent/dashboard/review_chat_api.py
agent/dashboard/review_style_jobs.py
agent/dashboard/review_styles.py
agent/dashboard/routes.py
agent/dashboard/schedules.py
agent/dashboard/slack_oauth.py
agent/dashboard/team_credentials.py
agent/dashboard/team_settings.py
agent/dashboard/thread_api.py
agent/dashboard/user_credentials.py
agent/dashboard/user_mappings.py
agent/dashboard/workflow_approval.py
agent/dashboard/workflow_approval_api.py
agent/dispatch.py
agent/encryption.py
agent/integrations/__init__.py
agent/integrations/corridor_mcp.py
agent/integrations/currents_tools.py
agent/integrations/datadog_mcp.py
agent/integrations/daytona.py
agent/integrations/e2b.py
agent/integrations/langsmith.py
agent/integrations/langsmith_tools.py
agent/integrations/local.py
agent/integrations/modal.py
agent/integrations/notion_mcp.py
agent/integrations/runloop.py
agent/integrations/stagehand_browser.py
agent/middleware/__init__.py
agent/middleware/check_message_queue.py
agent/middleware/ensure_no_empty_msg.py
agent/middleware/exclude_tools.py
agent/middleware/model_fallback.py
agent/middleware/notify_step_limit.py
agent/middleware/plan_mode.py
agent/middleware/pr_creation_guard.py
agent/middleware/prepare_run.py
agent/middleware/refresh_github_proxy.py
agent/middleware/refresh_slack_status.py
agent/middleware/repair_orphaned_tool_calls.py
agent/middleware/sandbox_circuit_breaker.py
agent/middleware/sanitize_fireworks_messages.py
agent/middleware/sanitize_openai_responses.py
agent/middleware/sanitize_thinking_blocks.py
agent/middleware/sanitize_tool_inputs.py
agent/middleware/settle_review_check.py
agent/middleware/subdir_agents.py
agent/middleware/task_retry.py
agent/middleware/timeout_wrapup.py
agent/middleware/tool_artifact.py
agent/middleware/tool_error_handler.py
agent/middleware/workflow_push_guard.py
agent/prompt.py
agent/reconcile.py
agent/review_style_collector.py
agent/review_style_guidance.py
agent/reviewer.py
agent/reviewer_diff.py
agent/reviewer_eval_store.py
agent/reviewer_findings.py
agent/reviewer_groups.py
agent/reviewer_publish.py
agent/reviewer_reconcile.py
agent/reviewer_trace_context.py
agent/scheduler.py
agent/server.py
agent/skills/bootstrap-repo-analysis/SKILL.md
agent/skills/continual-learning/SKILL.md
agent/tools/__init__.py
agent/tools/add_finding.py
agent/tools/enter_plan_mode.py
agent/tools/fetch_url.py
agent/tools/http_request.py
agent/tools/linear_comment.py
agent/tools/linear_create_issue.py
agent/tools/linear_delete_issue.py
agent/tools/linear_get_issue.py
agent/tools/linear_get_issue_comments.py
agent/tools/linear_list_teams.py
agent/tools/linear_update_issue.py
agent/tools/list_findings.py
agent/tools/list_review_findings.py
agent/tools/open_pull_request.py
agent/tools/publish_review.py
agent/tools/read_finding_outcomes.py
agent/tools/read_repo_file.py
agent/tools/reply_to_finding_thread.py
agent/tools/report_platform_issue.py
agent/tools/request_pr_review.py
agent/tools/resolve_finding_thread.py
agent/tools/save_plan.py
agent/tools/save_review_style.py
agent/tools/schedule_thread_wakeup.py
agent/tools/search_repo_code.py
agent/tools/slack_add_reaction.py
agent/tools/slack_read_thread_messages.py
agent/tools/slack_start_new_thread.py
agent/tools/slack_thread_reply.py
agent/tools/update_finding.py
agent/tools/web_search.py
agent/utils/agents_md.py
agent/utils/analyzer_skills.py
agent/utils/api_standards_skill.py
agent/utils/auth.py
agent/utils/authorship.py
agent/utils/comments.py
agent/utils/dashboard_handoff.py
agent/utils/dashboard_links.py
agent/utils/deferred_model.py
agent/utils/gateway.py
agent/utils/github_app.py
agent/utils/github_checks.py
agent/utils/github_ci.py
agent/utils/github_comments.py
agent/utils/github_feedback.py
agent/utils/github_http.py
agent/utils/github_org_membership.py
agent/utils/github_proxy.py
agent/utils/github_token.py
agent/utils/http.py
agent/utils/langsmith.py
agent/utils/linear.py
agent/utils/linear_team_repo_map.py
agent/utils/model.py
agent/utils/multimodal.py
agent/utils/repo.py
agent/utils/repo_prep.py
agent/utils/reviewer_outcomes.py
agent/utils/sandbox.py
agent/utils/sandbox_paths.py
agent/utils/sandbox_state.py
agent/utils/slack.py
agent/utils/slack_feedback.py
agent/utils/thread_ids.py
agent/utils/thread_ops.py
agent/utils/tracing.py
agent/utils/ttl_cache.py
agent/utils/url_safety.py
agent/webapp.py
agent/webhooks/__init__.py
agent/webhooks/github.py
agent/webhooks/linear.py
agent/webhooks/slack.py
default_prompt.md
evals/reviewer/README.md
evals/reviewer/build_dataset.py
evals/reviewer/config.toml
evals/reviewer/golden_comments/cal_dot_com.json
evals/reviewer/golden_comments/discourse.json
evals/reviewer/golden_comments/grafana.json
evals/reviewer/golden_comments/keycloak.json
evals/reviewer/golden_comments/sentry.json
evals/reviewer/judge.py
evals/reviewer/run_eval.py
evals/reviewer/store_reporter.py
evals/reviewer/target.py
langgraph.json
package.json
pyproject.toml
scripts/__init__.py
scripts/check_pr_merge_status.py
scripts/create_sandbox_snapshot.py
scripts/list_snapshots.py
scripts/purge_wakeup_crons.py
static/dark.svg
static/light.svg
tests/conftest.py
tests/e2e/.gitignore
tests/e2e/README.md
tests/e2e/agent_entrypoint.py
tests/e2e/dev-mock.sh
tests/e2e/e2e_env.py
tests/e2e/fake_llm.py
tests/e2e/fakes.py
tests/e2e/global-setup.ts
tests/e2e/harness.py
tests/e2e/langgraph.e2e.json
tests/e2e/package-lock.json
tests/e2e/package.json
tests/e2e/patches.py
tests/e2e/playwright.config.ts
tests/e2e/screenshots/queued-messages-dashboard.png
tests/e2e/static/github.html
tests/e2e/static/slack.html
tests/e2e/tests/dashboard.spec.ts
tests/e2e/tests/full_flow.spec.ts
tests/e2e/tests/plan_review.spec.ts
tests/e2e/tests/sandbox_id.spec.ts
tests/middleware/test_sandbox_recovery.py
tests/test_account_link.py
tests/test_agent_assembly_context.py
tests/test_agent_instructions.py
tests/test_agent_schedules.py
tests/test_agent_subagent_models.py
tests/test_agent_thread_pr_state.py
tests/test_agent_usage.py
tests/test_agents_md.py
tests/test_analyzer_cron.py
tests/test_analyzer_skills.py
tests/test_anthropic_effort.py
tests/test_anthropic_model.py
tests/test_api_standards_skill.py
tests/test_auth_sources.py
tests/test_authorship.py
tests/test_autofix_state.py
tests/test_check_message_queue.py
tests/test_completion_webhook.py
tests/test_corridor_mcp.py
tests/test_currents_tools.py
tests/test_dashboard_admin.py
tests/test_dashboard_csrf.py
tests/test_dashboard_links.py
tests/test_dashboard_oauth_redirect.py
tests/test_dashboard_org_login_gate.py
tests/test_dashboard_repo_optional.py
tests/test_dashboard_repos.py
tests/test_dashboard_reviews.py
tests/test_dashboard_run_email.py
tests/test_dashboard_thread_api.py
tests/test_dashboard_thread_api_activity.py
tests/test_dashboard_web_handoff.py
tests/test_daytona_integration.py
tests/test_deferred_model.py
tests/test_dispatch.py
tests/test_e2b_integration.py
tests/test_encryption.py
tests/test_ensure_no_empty_msg.py
tests/test_eval_jobs.py
tests/test_eval_store_reporter.py
tests/test_fireworks_model.py
tests/test_gateway.py
tests/test_github_app.py
tests/test_github_checks.py
tests/test_github_ci.py
tests/test_github_comment_prompts.py
tests/test_github_feedback.py
tests/test_github_http.py
tests/test_github_issue_webhook.py
tests/test_github_oauth_refresh.py
tests/test_github_proxy_refresh.py
tests/test_github_token_ttl.py
tests/test_google_model.py
tests/test_http_security.py
tests/test_import_hygiene.py
tests/test_langsmith_sandbox_config.py
tests/test_langsmith_sandbox_timeout.py
tests/test_langsmith_trace_url.py
tests/test_linear_webhook_author.py
tests/test_local_integration.py
tests/test_model_fallback_middleware.py
tests/test_model_fallback_resolution.py
tests/test_multimodal.py
tests/test_normalize_repo.py
tests/test_notify_step_limit_middleware.py
tests/test_notion_oauth.py
tests/test_observability_tools.py
tests/test_open_pull_request.py
tests/test_plan_mode.py
tests/test_plan_review.py
tests/test_pr_creation_guard.py
tests/test_pr_ready_auto_review.py
tests/test_prepare_run_middleware.py
tests/test_prompt_default_repo.py
tests/test_proxy_auth.py
tests/test_public_repo_org_gate.py
tests/test_recent_comments.py
tests/test_reconcile_sweep.py
tests/test_refresh_slack_status_middleware.py
tests/test_repair_orphaned_tool_calls.py
tests/test_repo_extraction.py
tests/test_repo_prep.py
tests/test_repo_snapshots.py
tests/test_report_platform_issue_tool.py
tests/test_review_api.py
tests/test_review_chat.py
tests/test_review_style_collector.py
tests/test_review_style_sync.py
tests/test_review_styles_store.py
tests/test_reviewer.py
tests/test_reviewer_diff.py
tests/test_reviewer_eval_judge.py
tests/test_reviewer_eval_run.py
tests/test_reviewer_eval_target.py
tests/test_reviewer_findings.py
tests/test_reviewer_groups.py
tests/test_reviewer_outcomes.py
tests/test_reviewer_publish.py
tests/test_reviewer_reconcile.py
tests/test_reviewer_tools.py
tests/test_reviewer_trace_context.py
tests/test_reviewer_watch.py
tests/test_sandbox_paths.py
tests/test_sandbox_state.py
tests/test_sanitize_fireworks_messages.py
tests/test_sanitize_openai_responses.py
tests/test_sanitize_thinking_blocks.py
tests/test_sanitize_tool_inputs.py
tests/test_schedule_thread_wakeup.py
tests/test_slack_add_reaction_tool.py
tests/test_slack_assistants_status.py
tests/test_slack_context.py
tests/test_slack_feedback.py
tests/test_slack_oauth.py
tests/test_slack_start_new_thread_tool.py
tests/test_slack_thread_reply_tool.py
tests/test_slack_webhook_errors.py
tests/test_stagehand_browser.py
tests/test_stale_sandbox_creating.py
tests/test_subdir_agents_middleware.py
tests/test_task_retry.py
tests/test_team_credentials.py
tests/test_team_settings_fable.py
tests/test_team_settings_grouping.py
tests/test_team_settings_org_guidelines.py
tests/test_timeout_wrapup.py
tests/test_tool_artifact_middleware.py
tests/test_user_credentials.py
tests/test_user_mappings.py
tests/test_workflow_approval_api.py
tests/test_workflow_push_guard.py
ui/.cta.json
ui/.gitignore
ui/.prettierignore
ui/.prettierrc
ui/AGENTS.md
ui/README.md
ui/assets/LangChain_Symbol_LightBlue.png
ui/assets/ic_launcher_round.png
ui/assets/playstore-icon.png
ui/components.json
ui/eslint.config.js
ui/package.json
ui/pnpm-lock.yaml
ui/pnpm-workspace.yaml
ui/public/apple-touch-icon.png
ui/public/favicon.png
ui/public/gh-open-in-open-swe-dark.svg
ui/public/gh-open-in-open-swe-light.svg
ui/public/logo-mark.png
ui/public/logo512.png
ui/public/robots.txt
ui/src/client.tsx
ui/src/components/AgentInstructionsPanel.tsx
ui/src/components/AppShell.tsx
ui/src/components/AppSidebar.tsx
ui/src/components/InstructionsEditor.tsx
ui/src/components/RepoSnapshotsPanel.tsx
ui/src/components/ReviewStylesPanel.tsx
ui/src/components/SidebarUserMenu.tsx
ui/src/components/agents/AgentGitPanel.tsx
ui/src/components/agents/AgentPromptBar.tsx
ui/src/components/agents/AgentRunCard.tsx
ui/src/components/agents/AgentThreadView.tsx
ui/src/components/agents/AgentsHome.tsx
ui/src/components/agents/AgentsSidebar.tsx
ui/src/components/agents/AgentsThreadsPage.tsx
ui/src/components/agents/AutomationEditor.tsx
ui/src/components/agents/AutomationTemplates.tsx
ui/src/components/agents/AutomationsList.tsx
ui/src/components/agents/OnboardingDialog.tsx
ui/src/components/agents/PlanReview.tsx
ui/src/components/agents/PrHeader.tsx
ui/src/components/agents/RepoSelector.tsx
ui/src/components/agents/ReviewChat.tsx
ui/src/components/agents/ReviewCommentsMenu.tsx
ui/src/components/agents/ReviewMainBody.tsx
ui/src/components/agents/ReviewSidebar.tsx
ui/src/components/agents/ReviewTab.tsx
ui/src/components/agents/ScheduleTriggerPicker.tsx
ui/src/components/agents/SidebarFilterMenu.tsx
ui/src/components/agents/WorkflowApprovalCard.tsx
ui/src/components/agents/messages/AgentMessage.tsx
ui/src/components/agents/messages/ChunkRenderer.tsx
ui/src/components/agents/messages/MessageTimestamp.tsx
ui/src/components/agents/messages/Messages.tsx
ui/src/components/agents/messages/ReasoningBlock.tsx
ui/src/components/agents/messages/ThinkingSpinner.tsx
ui/src/components/agents/messages/TurnChangedFilesCard.tsx
ui/src/components/agents/messages/UserMessage.tsx
ui/src/components/agents/messages/WorkSummary.tsx
ui/src/components/agents/messages/index.ts
ui/src/components/agents/messages/renderItems.ts
ui/src/components/agents/messages/summarizeChangedFiles.ts
ui/src/components/agents/messages/types.ts
ui/src/components/agents/ported/BranchSelector.tsx
ui/src/components/agents/ported/BubblePrefix.tsx
ui/src/components/agents/ported/ChatView.tsx
ui/src/components/agents/ported/CloudPromptBar.tsx
ui/src/components/agents/ported/CodeBlock.tsx
ui/src/components/agents/ported/CompactingIndicator.tsx
ui/src/components/agents/ported/ContextIndicator.tsx
ui/src/components/agents/ported/DiffView.tsx
ui/src/components/agents/ported/Footer.tsx
ui/src/components/agents/ported/HeaderBar.tsx
ui/src/components/agents/ported/Logo.tsx
ui/src/components/agents/ported/Markdown.tsx
ui/src/components/agents/ported/MarkdownTable.tsx
ui/src/components/agents/ported/PanelResizeHandle.tsx
ui/src/components/agents/ported/PromptBar.tsx
ui/src/components/agents/ported/ReplyCard.tsx
ui/src/components/agents/ported/ShellCommand.tsx
ui/src/components/agents/ported/SourceControlPanel.tsx
ui/src/components/agents/ported/SourceControlTile.tsx
ui/src/components/agents/ported/ThreadPicker.tsx
ui/src/components/agents/ported/TodoList.tsx
ui/src/components/agents/ported/ToolExecution.tsx
ui/src/components/agents/ported/index.ts
ui/src/components/agents/subagents/SubagentActivity.tsx
ui/src/components/agents/subagents/SubagentCard.tsx
ui/src/components/agents/subagents/SubagentGroup.tsx
ui/src/components/agents/subagents/index.ts
ui/src/components/agents/utils/diffStats.ts
ui/src/components/agents/utils/diffUtils.test.ts
ui/src/components/agents/utils/diffUtils.ts
ui/src/components/agents/z-index.ts
ui/src/components/sidebar-layout.tsx
ui/src/components/ui/avatar.tsx
ui/src/components/ui/badge.tsx
ui/src/components/ui/button.tsx
ui/src/components/ui/card.tsx
ui/src/components/ui/combobox.tsx
ui/src/components/ui/input-group.tsx
ui/src/components/ui/input.tsx
ui/src/components/ui/label.tsx
ui/src/components/ui/select.tsx
ui/src/components/ui/separator.tsx
ui/src/components/ui/skeleton.tsx
ui/src/components/ui/switch.tsx
ui/src/components/ui/textarea.tsx
ui/src/lib/agents/AgentThreadStreamProvider.tsx
ui/src/lib/agents/api.ts
ui/src/lib/agents/automation-templates.ts
ui/src/lib/agents/cron.ts
ui/src/lib/agents/messageTimestamps.ts
ui/src/lib/agents/provider/useIsInAgentThreadStream.tsx
ui/src/lib/agents/provider/useLiveMarkdownMessageId.ts
ui/src/lib/agents/provider/useModelOptions.ts
ui/src/lib/agents/provider/useSubmitAgentMessage.ts
ui/src/lib/agents/queries.ts
ui/src/lib/agents/sidebarFilter.test.ts
ui/src/lib/agents/sidebarFilter.ts
ui/src/lib/agents/sidebarPrefs.ts
ui/src/lib/agents/streamMessagesToUi.ts
ui/src/lib/agents/types.ts
ui/src/lib/agents/useRunCompletionNotifier.ts
ui/src/lib/api.ts
ui/src/lib/auth-redirect-core.ts
ui/src/lib/auth-redirect.test.ts
ui/src/lib/auth-redirect.tsx
ui/src/lib/hotkeys.ts
ui/src/lib/notifications.ts
ui/src/lib/plan.ts
ui/src/lib/profile.ts
ui/src/lib/query.ts
ui/src/lib/repo.test.ts
ui/src/lib/repo.ts
ui/src/lib/reviewImage.test.ts
ui/src/lib/session.ts
ui/src/lib/theme.ts
ui/src/lib/useIsMobile.ts
ui/src/lib/utils.ts
ui/src/logo.svg
ui/src/routeTree.gen.ts
ui/src/router.tsx
ui/src/routes/$owner.$repo.pull.$number.tsx
ui/src/routes/__root.tsx
ui/src/routes/admin.tsx
ui/src/routes/admin_.evals.tsx
ui/src/routes/agents.tsx
ui/src/routes/agents/$threadId.tsx
ui/src/routes/agents/$threadId_.plan.tsx
ui/src/routes/agents/automations/$scheduleId.tsx
ui/src/routes/agents/automations/index.tsx
ui/src/routes/agents/automations/new.tsx
ui/src/routes/agents/index.tsx
ui/src/routes/agents/reviews/$owner.$repo.$number.tsx
ui/src/routes/agents/reviews/index.tsx
ui/src/routes/agents/threads.tsx
ui/src/routes/agents_.instructions.tsx
ui/src/routes/agents_.snapshots.tsx
ui/src/routes/cloud-agents.tsx
ui/src/routes/index.tsx
ui/src/routes/integrations.tsx
ui/src/routes/login.tsx
ui/src/routes/my-settings.tsx
ui/src/routes/review.tsx
ui/src/routes/review_.repositories.$owner.tsx
ui/src/routes/review_.styles.tsx
ui/src/routes/usage.tsx
ui/src/styles.css
ui/src/styles/agents.css
ui/tsconfig.json
ui/vercel.json
ui/vite.config.ts
uv.lock

View file

@ -90,7 +90,7 @@
{"sha": "35659177", "pr": 1718, "subject": "fix: sanitize orphaned OpenAI tool results (#1718)", "disposition": "wont-merge", "reason": "N/A — fork has no OpenAI Responses traffic path; middleware present is #155's leaner rewrite lacking the orphan machinery #1718 patches; langchain-openai>=1.3.4 already satisfied; superseded upstream by #1731 (re-evaluate #155 rewrite when triaging #1731). Note: fd2541ce row's '#62 deleted sanitize_openai_responses.py' is inaccurate — the path was added fresh by #155", "branch": "", "local_sha": null, "updated": "2026-07-16T22:30:00Z"}
{"sha": "092abafa", "pr": 1717, "subject": "fix: enforce terse Slack tool messages (#1717)", "disposition": "landed", "reason": "cherry-picked (-x) in #197; one test conflict resolved by adopting upstream's test (fork prompt now contains upstream text); follow-up to landed #1681, not superseded by it", "branch": "fix/port-1717-terse-slack-tool-msgs", "local_sha": null, "updated": "2026-07-16T22:30:00Z"}
{"sha": "92d63170", "pr": 1720, "subject": "fix: separate review access from automatic reviews (#1720)", "disposition": "landed", "reason": "cherry-picked (-x) in #198 + fork-only auto-fix gates renamed to _is_repo_auto_review_enabled (kept opt-in-gated); opt-in list now gates only AUTOMATIC reviews — manual/re-review/watch/finding-replies open to any App-installed repo. Adam signed off 2026-07-16; /sh-security-review explicitly waived by Adam 2026-07-16", "branch": "fix/port-1720-review-access-split", "local_sha": null, "updated": "2026-07-16T22:30:00Z"}
{"sha": "8356eb34", "pr": 1726, "subject": "refactor: organize repository by domain (#1726)", "disposition": "deferred", "reason": "FLAG-HUMAN: 298-file structural reorg (tests/<domain>/, ui/src/features/); chain head — every later upstream commit is written against this layout. Fork policy defers structural refactors (CLAUDE.md); adopting is a dedicated merge exercise. Until then, later picks need path remapping.", "branch": "domain-reorg", "local_sha": null, "updated": "2026-07-16T18:46:21Z"}
{"sha": "8356eb34", "pr": 1726, "subject": "refactor: organize repository by domain (#1726)", "disposition": "landed", "reason": "Adopted as a file-move exercise (fork content, upstream layout) on branch refactor/domain-reorg-adoption — gate-approved plan in docs/upstream-sync/domain-reorg/. New layout: agent/{graphs,runtime,api,review,resources}, agent/webhooks/*_routes.py (webapp.py split into api/ + per-source route modules; webapp.py now a shim), tests/<domain>/, ui/src/features/. Kills the per-pick path-remap tax and unblocks 8 deferred rows (#1732/#1761/#1744/#1748/#1742 clean; #1736/#1758/#1760 near-clean).", "branch": "refactor/domain-reorg-adoption", "local_sha": null, "updated": "2026-07-17T00:00:00Z"}
{"sha": "83abea26", "pr": 1724, "subject": "fix: accept natural-language Slack plan approvals (#1724)", "disposition": "deferred", "reason": "natural-language Slack plan approvals; touches fork-diverged plan-mode + Slack webhook stack (#130); post-reorg test paths need remap", "branch": "plan-approval", "local_sha": null, "updated": "2026-07-16T18:46:52Z"}
{"sha": "129ddcf9", "pr": 1728, "subject": "chore: include ripgrep in sandbox image (#1728)", "disposition": "landed", "reason": "1-line Dockerfile add (ripgrep); dev image lacks it; trivial pick", "branch": "chore/upstream-easy-picks", "local_sha": null, "updated": "2026-07-16T19:15:42Z"}
{"sha": "ddbe457b", "pr": 1727, "subject": "fix: restore GPT-5.5 as default model (#1727)", "disposition": "deferred", "reason": "restores GPT-5.5 default in options/team_settings; fork picker is Bedrock/Fireworks-only — rides the #1708 OpenAI-models product decision (27b0ddeb)", "branch": "model-picker", "local_sha": null, "updated": "2026-07-16T18:46:38Z"}
@ -98,28 +98,28 @@
{"sha": "5136079d", "pr": 1725, "subject": "chore: disable todos for GPT-5.6 Sol (#1725)", "disposition": "wont-merge", "reason": "superseded — #1733 (136d28e6) rewrites the same todo-exclusion block to a global default-off with env opt-in; per-model GPT-5.6 Sol list moot (fork picker has no OpenAI models)", "branch": "", "local_sha": null, "updated": "2026-07-16T18:46:21Z"}
{"sha": "09eaf94c", "pr": 1730, "subject": "fix: let admins interrupt runaway agents (#1730)", "disposition": "landed", "reason": "admin interrupt for runaway agents (dashboard route + UI); useful ops control; UI half on post-reorg ui/src/features — remap to ui/src/components/agents", "branch": "feat/admin-thread-interrupt", "local_sha": null, "updated": "2026-07-16T19:36:07Z"}
{"sha": "30832d29", "pr": 1731, "subject": "fix: preserve OpenAI Responses tool history (#1731)", "disposition": "deferred", "reason": "deletes SanitizeOpenAIResponsesMiddleware in favor of replay-history preservation in utils/model.py; supersedes deferred #1718 (35659177) — triage the pair together against fork-diverged middleware + model.py", "branch": "openai-sanitize", "local_sha": null, "updated": "2026-07-16T18:46:38Z"}
{"sha": "1ea0e600", "pr": 1736, "subject": "fix: bind cached GitHub tokens to users (#1736)", "disposition": "deferred", "reason": "FLAG-HUMAN: security fix — binds per-thread cached GitHub tokens to a user principal (closes cross-user token-reuse leak). Fork has github_token.py but not webhooks/common.py — hand-map; auth surface: GPT-4.1 cross-review + /sh-security-review on landing. Priority pick.", "branch": "github-token-binding", "local_sha": null, "updated": "2026-07-16T18:46:37Z"}
{"sha": "1ea0e600", "pr": 1736, "subject": "fix: bind cached GitHub tokens to users (#1736)", "disposition": "deferred", "reason": "FLAG-HUMAN: security fix — binds per-thread cached GitHub tokens to a user principal (closes cross-user token-reuse leak). Fork has github_token.py but not webhooks/common.py — hand-map; auth surface: GPT-4.1 cross-review + /sh-security-review on landing. Priority pick. Re-triage (post-reorg 8356eb34 landed): biggest beneficiary — agent/webhooks/common.py now exists, so the pick is near-clean against it; auth gates (GPT-4.1 cross-review + /sh-security-review) still apply.", "branch": "github-token-binding", "local_sha": null, "updated": "2026-07-17T00:00:00Z"}
{"sha": "3fcb27ce", "pr": 1737, "subject": "fix: bound reviewer diff fetching (#1737)", "disposition": "deferred", "reason": "bounds reviewer diff fetching + new fetch_review_diff tool; reviewer.py fork-diverged — reconcile like #1713 (71e3b818)", "branch": "reviewer-misc", "local_sha": null, "updated": "2026-07-16T18:46:52Z"}
{"sha": "d714586c", "pr": 1732, "subject": "fix: normalize dashboard label rendering (#1732)", "disposition": "deferred", "reason": "dashboard tool-label normalization; post-reorg ui/src/features paths — remap", "branch": "dashboard-ui", "local_sha": null, "updated": "2026-07-16T18:47:07Z"}
{"sha": "d714586c", "pr": 1732, "subject": "fix: normalize dashboard label rendering (#1732)", "disposition": "deferred", "reason": "dashboard tool-label normalization; post-reorg ui/src/features paths — remap. Re-triage (post-reorg 8356eb34 landed): path-remap blocker removed — clean pick; ui/src/features/ now matches upstream.", "branch": "dashboard-ui", "local_sha": null, "updated": "2026-07-17T00:00:00Z"}
{"sha": "ef68c09b", "pr": 1734, "subject": "fix: handle Slack DMs as mentions (#1734)", "disposition": "deferred", "reason": "treat Slack DMs as mentions; touches fork-diverged Slack webhook + e2e harness; post-reorg test paths", "branch": "slack-tooling", "local_sha": null, "updated": "2026-07-16T18:46:52Z"}
{"sha": "bfa67a7a", "pr": 1711, "subject": "chore(deps): bump soupsieve from 2.8.3 to 2.8.4 (#1711)", "disposition": "wont-merge", "reason": "already in dev — uv.lock resolves soupsieve 2.8.4", "branch": "", "local_sha": null, "updated": "2026-07-16T18:46:21Z"}
{"sha": "26828ff9", "pr": 1745, "subject": "chore: upgrade deepagents to 0.7.0a7 (#1745)", "disposition": "deferred", "reason": "deepagents 0.6.12 -> 0.7.0a7 alpha + [tool.uv] override-dependencies to bypass sandbox integrations <0.7.0 bound; fork is built on create_deep_agent — dedicated validation (unit + e2e) before adopting an alpha", "branch": "deps", "local_sha": null, "updated": "2026-07-16T18:46:38Z"}
{"sha": "ef0ed5af", "pr": 1741, "subject": "fix: centralize SSRF-safe image fetches (#1741)", "disposition": "deferred", "reason": "security hardening — centralizes image fetches through url_safety.py (SSRF); fork has url_safety.py/multimodal.py (diverged) — hand-reconcile; untrusted-input surface: /sh-security-review on landing", "branch": "ssrf-hardening", "local_sha": null, "updated": "2026-07-16T18:46:37Z"}
{"sha": "136d28e6", "pr": 1733, "subject": "fix: disable todos by default (#1733)", "disposition": "deferred", "reason": "global write_todos/TodoListMiddleware default-off with OPEN_SWE_ENABLE_TODOS opt-in; edits fork-customized prompt.py — small hand-merge; supersedes #1725", "branch": "prompt-tweaks", "local_sha": null, "updated": "2026-07-16T18:47:07Z"}
{"sha": "3e8089c3", "pr": 1744, "subject": "fix: collapse git panel by default (#1744)", "disposition": "deferred", "reason": "collapse git panel by default (localStorage pref); post-reorg UI paths — remap to ui/src/components/agents", "branch": "dashboard-ui", "local_sha": null, "updated": "2026-07-16T18:47:08Z"}
{"sha": "3e8089c3", "pr": 1744, "subject": "fix: collapse git panel by default (#1744)", "disposition": "deferred", "reason": "collapse git panel by default (localStorage pref); post-reorg UI paths — remap to ui/src/components/agents. Re-triage (post-reorg 8356eb34 landed): path-remap blocker removed — clean pick; ui/src/features/ now matches upstream.", "branch": "dashboard-ui", "local_sha": null, "updated": "2026-07-17T00:00:00Z"}
{"sha": "5077e2c7", "pr": 1735, "subject": "feat(open-swe): untagged two-party Slack replies + debounced interrupts (#1735)", "disposition": "deferred", "reason": "untagged two-party Slack replies + debounced interrupts (~620 LOC incl. e2e); rides fork-diverged Slack webhook stack; own branch + e2e validation", "branch": "slack-untagged-replies", "local_sha": null, "updated": "2026-07-16T18:46:52Z"}
{"sha": "f7d94ad3", "pr": 1721, "subject": "docs: add e2b to sandbox provider lists in AGENTS.md and CLAUDE.md (#1721)", "disposition": "wont-merge", "reason": "N/A — edits upstream AGENTS.md/CLAUDE.md, both fully fork-rewritten; E2B provider itself deferred (48217b68) — add a doc line if/when E2B lands", "branch": "", "local_sha": null, "updated": "2026-07-16T18:46:21Z"}
{"sha": "8b26819f", "pr": 1719, "subject": "fix: offload web tool results to sandbox (#1719)", "disposition": "deferred", "reason": "offloads large web tool results to sandbox files; touches fork-relevant web_search/http tools; check interplay with fork sandbox lifecycle", "branch": "tool-offloading", "local_sha": null, "updated": "2026-07-16T18:46:52Z"}
{"sha": "c34e04f4", "pr": 1742, "subject": "fix: defensive copy in get_reviewer_agent and get_chat_agent [closes #1584] (#1742)", "disposition": "deferred", "reason": "defensive copy of config in get_reviewer_agent/get_chat_agent; small correctness fix; dev has chat.py + reviewer.py (diverged) — likely near-clean pick", "branch": "small-tools", "local_sha": null, "updated": "2026-07-16T18:46:52Z"}
{"sha": "c34e04f4", "pr": 1742, "subject": "fix: defensive copy in get_reviewer_agent and get_chat_agent [closes #1584] (#1742)", "disposition": "deferred", "reason": "defensive copy of config in get_reviewer_agent/get_chat_agent; small correctness fix; dev has chat.py + reviewer.py (diverged) — likely near-clean pick. Re-triage (post-reorg 8356eb34 landed): path-remap blocker removed — clean pick; reviewer.py/chat.py factories unmoved.", "branch": "small-tools", "local_sha": null, "updated": "2026-07-17T00:00:00Z"}
{"sha": "4773b336", "pr": 1746, "subject": "chore: point basedpyright at uv's .venv (#1746)", "disposition": "wont-merge", "reason": "tooling — fork does not use basedpyright (lint stack is ruff); nothing to point at .venv", "branch": "", "local_sha": null, "updated": "2026-07-16T18:46:21Z"}
{"sha": "79df6b2f", "pr": 1748, "subject": "feat: add Linear issue search tool (#1748)", "disposition": "deferred", "reason": "additive linear_search_issues tool + utils/linear.py search helper; fork ships Linear tools — straightforward port; post-reorg test path remap", "branch": "small-tools", "local_sha": null, "updated": "2026-07-16T18:46:52Z"}
{"sha": "79df6b2f", "pr": 1748, "subject": "feat: add Linear issue search tool (#1748)", "disposition": "deferred", "reason": "additive linear_search_issues tool + utils/linear.py search helper; fork ships Linear tools — straightforward port; post-reorg test path remap. Re-triage (post-reorg 8356eb34 landed): test-path blocker removed — clean pick; tests/<domain>/ now matches upstream.", "branch": "small-tools", "local_sha": null, "updated": "2026-07-17T00:00:00Z"}
{"sha": "b7c5dbd6", "pr": 1747, "subject": "fix: simplify Slack run links (#1747)", "disposition": "deferred", "reason": "simplifies Slack run links; heavy churn on fork-diverged Slack context/prompt tests", "branch": "slack-tooling", "local_sha": null, "updated": "2026-07-16T18:46:52Z"}
{"sha": "c69459ad", "pr": 1751, "subject": "fix: prefer LangSmith tools for trace links (#1751)", "disposition": "landed", "reason": "1-line prompt: prefer LangSmith tools for trace links; trivial but edits fork-customized prompt.py", "branch": "chore/upstream-easy-picks", "local_sha": null, "updated": "2026-07-16T19:15:42Z"}
{"sha": "5cb2e2bb", "pr": 1750, "subject": "fix: add trace link to error banner (#1750)", "disposition": "landed", "reason": "adds trace link to error banner (13 lines); remap AgentThreadView.tsx path (fork: ui/src/components/agents/)", "branch": "chore/upstream-easy-picks", "local_sha": null, "updated": "2026-07-16T19:15:42Z"}
{"sha": "697adaa7", "pr": 1752, "subject": "fix: update PyJWT to 2.13.0 (#1752)", "disposition": "wont-merge", "reason": "already in dev — uv.lock resolves PyJWT 2.13.0", "branch": "", "local_sha": null, "updated": "2026-07-16T18:46:21Z"}
{"sha": "22383033", "pr": 1758, "subject": "feat: inject extra JSON fields into sandbox create via env var (#1758)", "disposition": "deferred", "reason": "additive: extra JSON fields into sandbox create via env var (integrations/langsmith.py); fork langsmith.py diverged (proxy config) — small reconcile", "branch": "sandbox-config", "local_sha": null, "updated": "2026-07-16T18:47:08Z"}
{"sha": "22383033", "pr": 1758, "subject": "feat: inject extra JSON fields into sandbox create via env var (#1758)", "disposition": "deferred", "reason": "additive: extra JSON fields into sandbox create via env var (integrations/langsmith.py); fork langsmith.py diverged (proxy config) — small reconcile. Re-triage (post-reorg 8356eb34 landed): near-clean — path-remap tax gone; remaining work is the additive reconcile against fork's diverged langsmith.py (integrations/ unmoved by reorg).", "branch": "sandbox-config", "local_sha": null, "updated": "2026-07-17T00:00:00Z"}
{"sha": "714ea4a2", "pr": 1759, "subject": "fix: clear basedpyright standard-mode type errors (#1759)", "disposition": "wont-merge", "reason": "tooling — basedpyright type-error cleanup across 123 post-reorg files; fork uses ruff and the pre-reorg layout", "branch": "", "local_sha": null, "updated": "2026-07-16T18:46:21Z"}
{"sha": "e826864d", "pr": 1760, "subject": "feat: optional separate LangSmith key/endpoint for sandboxes (#1760)", "disposition": "deferred", "reason": "optional separate LangSmith key/endpoint for sandboxes; additive to langsmith.py + proxy auth; useful for fork LangSmith sandbox usage", "branch": "sandbox-config", "local_sha": null, "updated": "2026-07-16T18:47:08Z"}
{"sha": "dd5b7bec", "pr": 1761, "subject": "fix: capitalize dashboard tool labels (#1761)", "disposition": "deferred", "reason": "capitalize dashboard tool labels; post-reorg UI paths; stacks on #1732 (d714586c)", "branch": "dashboard-ui", "local_sha": null, "updated": "2026-07-16T18:47:08Z"}
{"sha": "e826864d", "pr": 1760, "subject": "feat: optional separate LangSmith key/endpoint for sandboxes (#1760)", "disposition": "deferred", "reason": "optional separate LangSmith key/endpoint for sandboxes; additive to langsmith.py + proxy auth; useful for fork LangSmith sandbox usage. Re-triage (post-reorg 8356eb34 landed): near-clean — path-remap tax gone; remaining work is the additive reconcile against fork's diverged langsmith.py + proxy auth (integrations/ unmoved).", "branch": "sandbox-config", "local_sha": null, "updated": "2026-07-17T00:00:00Z"}
{"sha": "dd5b7bec", "pr": 1761, "subject": "fix: capitalize dashboard tool labels (#1761)", "disposition": "deferred", "reason": "capitalize dashboard tool labels; post-reorg UI paths; stacks on #1732 (d714586c). Re-triage (post-reorg 8356eb34 landed): path-remap blocker removed — clean pick after #1732.", "branch": "dashboard-ui", "local_sha": null, "updated": "2026-07-17T00:00:00Z"}
{"sha": "dccf6437", "pr": 1769, "subject": "fix: tighten sandbox config test types (#1769)", "disposition": "wont-merge", "reason": "test-only change in post-reorg path tests/sandbox/ — fork doesn't have this file (domain reorg #1726 deferred)", "branch": "", "local_sha": null, "updated": "2026-07-17T12:42:37Z"}
{"sha": "c9a193e2", "pr": 1766, "subject": "chore(deps): bump mcp from 1.27.2 to 1.28.1 (#1766)", "disposition": "wont-merge", "reason": "indirect dependency bump (mcp); fork's own Dependabot handles these", "branch": "", "local_sha": null, "updated": "2026-07-17T12:42:37Z"}

View file

@ -71,6 +71,7 @@ Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred ro
| `71e3b818` | #1713 | fix: align reviewer eval with published findings (#1713) | Landed | cherry-picked (-x) in #201; reviewer.py + publish_review.py hand-reconciled into fork structure (kept sandbox/skills/middleware stack, kebab-case workflow, Bedrock eval default); publish_review cap param removed (tool-facing only) | fix/port-1713-reviewer-eval-alignment |
| `092abafa` | #1717 | fix: enforce terse Slack tool messages (#1717) | Landed | cherry-picked (-x) in #197; one test conflict resolved by adopting upstream's test (fork prompt now contains upstream text); follow-up to landed #1681, not superseded by it | fix/port-1717-terse-slack-tool-msgs |
| `92d63170` | #1720 | fix: separate review access from automatic reviews (#1720) | Landed | cherry-picked (-x) in #198 + fork-only auto-fix gates renamed to _is_repo_auto_review_enabled (kept opt-in-gated); opt-in list now gates only AUTOMATIC reviews — manual/re-review/watch/finding-replies open to any App-installed repo. Adam signed off 2026-07-16; /sh-security-review explicitly waived by Adam 2026-07-16 | fix/port-1720-review-access-split |
| `8356eb34` | #1726 | refactor: organize repository by domain (#1726) | Landed | Adopted as a file-move exercise (fork content, upstream layout) on branch refactor/domain-reorg-adoption — gate-approved plan in docs/upstream-sync/domain-reorg/. New layout: agent/{graphs,runtime,api,review,resources}, agent/webhooks/*_routes.py (webapp.py split into api/ + per-source route modules; webapp.py now a shim), tests/<domain>/, ui/src/features/. Kills the per-pick path-remap tax and unblocks 8 deferred rows (#1732/#1761/#1744/#1748/#1742 clean; #1736/#1758/#1760 near-clean). | refactor/domain-reorg-adoption |
| `129ddcf9` | #1728 | chore: include ripgrep in sandbox image (#1728) | Landed | 1-line Dockerfile add (ripgrep); dev image lacks it; trivial pick | chore/upstream-easy-picks |
| `1ea03a43` | #1729 | feat: include Cargo in sandbox image (#1729) | Landed | 2-line Dockerfile add (Cargo); adopt with #1728 | chore/upstream-easy-picks |
| `09eaf94c` | #1730 | fix: let admins interrupt runaway agents (#1730) | Landed | admin interrupt for runaway agents (dashboard route + UI); useful ops control; UI half on post-reorg ui/src/features — remap to ui/src/components/agents | feat/admin-thread-interrupt |
@ -114,25 +115,24 @@ Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred ro
| `714ea4a2` | #1759 | fix: clear basedpyright standard-mode type errors (#1759) | Won't merge | tooling — basedpyright type-error cleanup across 123 post-reorg files; fork uses ruff and the pre-reorg layout | |
| `dccf6437` | #1769 | fix: tighten sandbox config test types (#1769) | Won't merge | test-only change in post-reorg path tests/sandbox/ — fork doesn't have this file (domain reorg #1726 deferred) | |
| `c9a193e2` | #1766 | chore(deps): bump mcp from 1.27.2 to 1.28.1 (#1766) | Won't merge | indirect dependency bump (mcp); fork's own Dependabot handles these | |
| `8356eb34` | #1726 | refactor: organize repository by domain (#1726) | Deferred | FLAG-HUMAN: 298-file structural reorg (tests/<domain>/, ui/src/features/); chain head — every later upstream commit is written against this layout. Fork policy defers structural refactors (CLAUDE.md); adopting is a dedicated merge exercise. Until then, later picks need path remapping. | domain-reorg |
| `83abea26` | #1724 | fix: accept natural-language Slack plan approvals (#1724) | Deferred | natural-language Slack plan approvals; touches fork-diverged plan-mode + Slack webhook stack (#130); post-reorg test paths need remap | plan-approval |
| `ddbe457b` | #1727 | fix: restore GPT-5.5 as default model (#1727) | Deferred | restores GPT-5.5 default in options/team_settings; fork picker is Bedrock/Fireworks-only — rides the #1708 OpenAI-models product decision (27b0ddeb) | model-picker |
| `30832d29` | #1731 | fix: preserve OpenAI Responses tool history (#1731) | Deferred | deletes SanitizeOpenAIResponsesMiddleware in favor of replay-history preservation in utils/model.py; supersedes deferred #1718 (35659177) — triage the pair together against fork-diverged middleware + model.py | openai-sanitize |
| `1ea0e600` | #1736 | fix: bind cached GitHub tokens to users (#1736) | Deferred | FLAG-HUMAN: security fix — binds per-thread cached GitHub tokens to a user principal (closes cross-user token-reuse leak). Fork has github_token.py but not webhooks/common.py — hand-map; auth surface: GPT-4.1 cross-review + /sh-security-review on landing. Priority pick. | github-token-binding |
| `1ea0e600` | #1736 | fix: bind cached GitHub tokens to users (#1736) | Deferred | FLAG-HUMAN: security fix — binds per-thread cached GitHub tokens to a user principal (closes cross-user token-reuse leak). Fork has github_token.py but not webhooks/common.py — hand-map; auth surface: GPT-4.1 cross-review + /sh-security-review on landing. Priority pick. Re-triage (post-reorg 8356eb34 landed): biggest beneficiary — agent/webhooks/common.py now exists, so the pick is near-clean against it; auth gates (GPT-4.1 cross-review + /sh-security-review) still apply. | github-token-binding |
| `3fcb27ce` | #1737 | fix: bound reviewer diff fetching (#1737) | Deferred | bounds reviewer diff fetching + new fetch_review_diff tool; reviewer.py fork-diverged — reconcile like #1713 (71e3b818) | reviewer-misc |
| `d714586c` | #1732 | fix: normalize dashboard label rendering (#1732) | Deferred | dashboard tool-label normalization; post-reorg ui/src/features paths — remap | dashboard-ui |
| `d714586c` | #1732 | fix: normalize dashboard label rendering (#1732) | Deferred | dashboard tool-label normalization; post-reorg ui/src/features paths — remap. Re-triage (post-reorg 8356eb34 landed): path-remap blocker removed — clean pick; ui/src/features/ now matches upstream. | dashboard-ui |
| `ef68c09b` | #1734 | fix: handle Slack DMs as mentions (#1734) | Deferred | treat Slack DMs as mentions; touches fork-diverged Slack webhook + e2e harness; post-reorg test paths | slack-tooling |
| `26828ff9` | #1745 | chore: upgrade deepagents to 0.7.0a7 (#1745) | Deferred | deepagents 0.6.12 -> 0.7.0a7 alpha + [tool.uv] override-dependencies to bypass sandbox integrations <0.7.0 bound; fork is built on create_deep_agent — dedicated validation (unit + e2e) before adopting an alpha | deps |
| `ef0ed5af` | #1741 | fix: centralize SSRF-safe image fetches (#1741) | Deferred | security hardening — centralizes image fetches through url_safety.py (SSRF); fork has url_safety.py/multimodal.py (diverged) — hand-reconcile; untrusted-input surface: /sh-security-review on landing | ssrf-hardening |
| `136d28e6` | #1733 | fix: disable todos by default (#1733) | Deferred | global write_todos/TodoListMiddleware default-off with OPEN_SWE_ENABLE_TODOS opt-in; edits fork-customized prompt.py — small hand-merge; supersedes #1725 | prompt-tweaks |
| `3e8089c3` | #1744 | fix: collapse git panel by default (#1744) | Deferred | collapse git panel by default (localStorage pref); post-reorg UI paths — remap to ui/src/components/agents | dashboard-ui |
| `3e8089c3` | #1744 | fix: collapse git panel by default (#1744) | Deferred | collapse git panel by default (localStorage pref); post-reorg UI paths — remap to ui/src/components/agents. Re-triage (post-reorg 8356eb34 landed): path-remap blocker removed — clean pick; ui/src/features/ now matches upstream. | dashboard-ui |
| `5077e2c7` | #1735 | feat(open-swe): untagged two-party Slack replies + debounced interrupts (#1735) | Deferred | untagged two-party Slack replies + debounced interrupts (~620 LOC incl. e2e); rides fork-diverged Slack webhook stack; own branch + e2e validation | slack-untagged-replies |
| `8b26819f` | #1719 | fix: offload web tool results to sandbox (#1719) | Deferred | offloads large web tool results to sandbox files; touches fork-relevant web_search/http tools; check interplay with fork sandbox lifecycle | tool-offloading |
| `c34e04f4` | #1742 | fix: defensive copy in get_reviewer_agent and get_chat_agent [closes #1584] (#1742) | Deferred | defensive copy of config in get_reviewer_agent/get_chat_agent; small correctness fix; dev has chat.py + reviewer.py (diverged) — likely near-clean pick | small-tools |
| `79df6b2f` | #1748 | feat: add Linear issue search tool (#1748) | Deferred | additive linear_search_issues tool + utils/linear.py search helper; fork ships Linear tools — straightforward port; post-reorg test path remap | small-tools |
| `c34e04f4` | #1742 | fix: defensive copy in get_reviewer_agent and get_chat_agent [closes #1584] (#1742) | Deferred | defensive copy of config in get_reviewer_agent/get_chat_agent; small correctness fix; dev has chat.py + reviewer.py (diverged) — likely near-clean pick. Re-triage (post-reorg 8356eb34 landed): path-remap blocker removed — clean pick; reviewer.py/chat.py factories unmoved. | small-tools |
| `79df6b2f` | #1748 | feat: add Linear issue search tool (#1748) | Deferred | additive linear_search_issues tool + utils/linear.py search helper; fork ships Linear tools — straightforward port; post-reorg test path remap. Re-triage (post-reorg 8356eb34 landed): test-path blocker removed — clean pick; tests/<domain>/ now matches upstream. | small-tools |
| `b7c5dbd6` | #1747 | fix: simplify Slack run links (#1747) | Deferred | simplifies Slack run links; heavy churn on fork-diverged Slack context/prompt tests | slack-tooling |
| `22383033` | #1758 | feat: inject extra JSON fields into sandbox create via env var (#1758) | Deferred | additive: extra JSON fields into sandbox create via env var (integrations/langsmith.py); fork langsmith.py diverged (proxy config) — small reconcile | sandbox-config |
| `e826864d` | #1760 | feat: optional separate LangSmith key/endpoint for sandboxes (#1760) | Deferred | optional separate LangSmith key/endpoint for sandboxes; additive to langsmith.py + proxy auth; useful for fork LangSmith sandbox usage | sandbox-config |
| `dd5b7bec` | #1761 | fix: capitalize dashboard tool labels (#1761) | Deferred | capitalize dashboard tool labels; post-reorg UI paths; stacks on #1732 (d714586c) | dashboard-ui |
| `22383033` | #1758 | feat: inject extra JSON fields into sandbox create via env var (#1758) | Deferred | additive: extra JSON fields into sandbox create via env var (integrations/langsmith.py); fork langsmith.py diverged (proxy config) — small reconcile. Re-triage (post-reorg 8356eb34 landed): near-clean — path-remap tax gone; remaining work is the additive reconcile against fork's diverged langsmith.py (integrations/ unmoved by reorg). | sandbox-config |
| `e826864d` | #1760 | feat: optional separate LangSmith key/endpoint for sandboxes (#1760) | Deferred | optional separate LangSmith key/endpoint for sandboxes; additive to langsmith.py + proxy auth; useful for fork LangSmith sandbox usage. Re-triage (post-reorg 8356eb34 landed): near-clean — path-remap tax gone; remaining work is the additive reconcile against fork's diverged langsmith.py + proxy auth (integrations/ unmoved). | sandbox-config |
| `dd5b7bec` | #1761 | fix: capitalize dashboard tool labels (#1761) | Deferred | capitalize dashboard tool labels; post-reorg UI paths; stacks on #1732 (d714586c). Re-triage (post-reorg 8356eb34 landed): path-remap blocker removed — clean pick after #1732. | dashboard-ui |
_Maintenance: after a `git sync`, add new `dev..upstream/main` SHAs as **Untriaged** (edit `triage.jsonl`) and bump "Last synced". A successful `git cherry-pick -x` auto-moves the row to **Landed** via the `post-commit` journal + `make triage-reconcile`._

View file

@ -22,7 +22,7 @@ from uuid import UUID
from langchain_anthropic import ChatAnthropic
from langsmith.schemas import Example, Run
from agent.reviewer_findings import REVIEW_FINDING_CAP
from agent.review.findings import REVIEW_FINDING_CAP
JUDGE_MODEL = "claude-opus-4-5"

View file

@ -23,8 +23,8 @@ from langgraph_sdk import get_client
from langsmith import Client, aevaluate
from langsmith.schemas import Example
from agent.reviewer_eval_store import _EXPERIMENT_URL_RE, _LOG_TAIL_CHARS
from agent.reviewer_findings import REVIEW_FINDING_CAP
from agent.review.eval_store import _EXPERIMENT_URL_RE, _LOG_TAIL_CHARS
from agent.review.findings import REVIEW_FINDING_CAP
from evals.reviewer.judge import aggregate_pr, judge_match
from evals.reviewer.store_reporter import StoreReporter, is_enabled
from evals.reviewer.target import (

View file

@ -18,7 +18,7 @@ from typing import Any
from langgraph_sdk import get_client
from agent.reviewer_eval_store import (
from agent.review.eval_store import (
_HEARTBEAT_INTERVAL_SECONDS,
EVALS_NAMESPACE,
REVIEWER_EVAL_KEY,

View file

@ -16,7 +16,7 @@ from typing import Any, Literal, cast
from langgraph_sdk import get_client
from agent.reviewer_findings import (
from agent.review.findings import (
REVIEW_FINDING_CAP,
REVIEWER_EVAL_PUBLICATION_KEY,
Finding,

View file

@ -3,12 +3,12 @@
"python_version": "3.12",
"api_version": "0.10.0rc3",
"graphs": {
"agent": "agent.server:traced_agent",
"reviewer": "agent.reviewer:traced_reviewer_agent",
"analyzer": "agent.analyzer:traced_analyzer",
"chat": "agent.chat:traced_chat_agent",
"scheduler": "agent.scheduler:get_scheduler",
"ci_monitor": "agent.ci_monitor:get_ci_monitor"
"agent": "agent.graphs.agent:traced_agent",
"reviewer": "agent.graphs.reviewer:traced_reviewer_agent",
"analyzer": "agent.graphs.analyzer:traced_analyzer",
"chat": "agent.graphs.chat:traced_chat_agent",
"scheduler": "agent.graphs.scheduler:get_scheduler",
"ci_monitor": "agent.graphs.ci_monitor:get_ci_monitor"
},
"dependencies": [
"."

View file

@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent import webapp
from agent.webhooks import common as webhook_common
def _pr_payload(*, state: str, merged: bool = False, draft: bool = False) -> dict[str, Any]:
@ -22,23 +22,25 @@ def _pr_payload(*, state: str, merged: bool = False, draft: bool = False) -> dic
def test_pr_state_from_payload_merged() -> None:
assert webapp._pr_state_from_payload(_pr_payload(state="closed", merged=True)) == "merged"
assert (
webhook_common._pr_state_from_payload(_pr_payload(state="closed", merged=True)) == "merged"
)
def test_pr_state_from_payload_closed() -> None:
assert webapp._pr_state_from_payload(_pr_payload(state="closed")) == "closed"
assert webhook_common._pr_state_from_payload(_pr_payload(state="closed")) == "closed"
def test_pr_state_from_payload_draft() -> None:
assert webapp._pr_state_from_payload(_pr_payload(state="open", draft=True)) == "draft"
assert webhook_common._pr_state_from_payload(_pr_payload(state="open", draft=True)) == "draft"
def test_pr_state_from_payload_open() -> None:
assert webapp._pr_state_from_payload(_pr_payload(state="open")) == "open"
assert webhook_common._pr_state_from_payload(_pr_payload(state="open")) == "open"
def test_pr_state_from_payload_missing_pull_request() -> None:
assert webapp._pr_state_from_payload({}) is None
assert webhook_common._pr_state_from_payload({}) is None
@pytest.mark.asyncio
@ -54,8 +56,8 @@ async def test_update_agent_thread_pr_state_updates_matching_thread() -> None:
)
fake_client.threads.update = AsyncMock()
with patch("agent.webapp.get_client", return_value=fake_client):
await webapp.update_agent_thread_pr_state(_pr_payload(state="closed"))
with patch("agent.webhooks.common.get_client", return_value=fake_client):
await webhook_common.update_agent_thread_pr_state(_pr_payload(state="closed"))
fake_client.threads.search.assert_awaited_once()
fake_client.threads.update.assert_awaited_once()
@ -71,8 +73,8 @@ async def test_update_agent_thread_pr_state_skips_reviewer_threads() -> None:
)
fake_client.threads.update = AsyncMock()
with patch("agent.webapp.get_client", return_value=fake_client):
await webapp.update_agent_thread_pr_state(_pr_payload(state="closed"))
with patch("agent.webhooks.common.get_client", return_value=fake_client):
await webhook_common.update_agent_thread_pr_state(_pr_payload(state="closed"))
fake_client.threads.update.assert_not_called()
@ -85,7 +87,7 @@ async def test_update_agent_thread_pr_state_noop_when_state_unchanged() -> None:
)
fake_client.threads.update = AsyncMock()
with patch("agent.webapp.get_client", return_value=fake_client):
await webapp.update_agent_thread_pr_state(_pr_payload(state="closed", merged=True))
with patch("agent.webhooks.common.get_client", return_value=fake_client):
await webhook_common.update_agent_thread_pr_state(_pr_payload(state="closed", merged=True))
fake_client.threads.update.assert_not_called()

View file

@ -191,7 +191,7 @@ async def test_save_plan_reads_markdown_file_from_sandbox(
def test_plan_routes_registered() -> None:
from agent.webapp import app
from agent.api.app import app
paths = set()
for route in app.routes:
@ -746,7 +746,7 @@ async def test_approve_plan_hands_edited_plan_to_agent(
def test_plan_update_route_registered() -> None:
from agent.webapp import app
from agent.api.app import app
paths = set()
for route in app.routes:

Some files were not shown because too many files have changed in this diff Show more