An AI-powered pipeline that turns a Jira bug ticket into a reviewed, ready-to-merge pull request.
A Slack bot watches for Jira ticket links. When one appears, it fetches the ticket and its Sentry error context, asks Claude to diagnose the root cause, and posts the diagnosis back to the thread for a human to approve. On approval it applies the fix in an isolated git worktree, verifies it (lint / type-check), runs a multi-model code review, and opens a pull request — posting the PR link back to the same thread.
Input: a Jira ticket URL. Output: an open pull request, plus a full Markdown audit trail of every stage.
Single Python process. The Slack bot runs in the main thread; each pipeline run executes in its own worker thread. The bot never calls AI APIs directly — every AI stage (diagnosis, fix, review) shells out to the Claude Code CLI as a subprocess, so each stage gets full read/write codebase access without the orchestrator managing context windows or token accounting itself.
graph TD
Slack[Slack Bot] --> Orchestrator[Pipeline Orchestrator]
Orchestrator --> Jira[jira CLI]
Orchestrator --> Sentry[sentry CLI]
Orchestrator --> Agent[AgentClient]
Agent --> Claude[Claude Code CLI]
Orchestrator --> Git[git / gh CLI]
Agent -. optional base_url .-> LiteLLM[LiteLLM proxy]
LiteLLM -.-> AnyModel[Any OpenAI-compatible model]
AgentClient() talks to Anthropic directly. AgentClient(base_url="http://127.0.0.1:4444") routes the same Claude Code CLI subprocess through a local LiteLLM proxy instead, so a non-Anthropic model can answer while the CLI keeps its full agentic capability (file reads, grep, following imports). This is what powers the optional secondary reviewer (see Multi-model review).
The orchestrator runs 10 ordered stages per ticket:
flowchart TD
A[Resolve repo] --> B[Resolve branch]
B --> C[Fetch Sentry context]
C --> D[Diagnose root cause]
D --> E{Approve in Slack thread}
E -->|fix| F[Apply fix in git worktree]
E -->|skip| Z[Stop]
F --> G["Verify (lint / tsc)"]
G --> H["Review (multi-model)"]
H --> I[Open PR]
I --> J[Cleanup worktree]
- Resolve repo — map the Sentry project to a GitHub repo (config, keyword match, or ask)
- Resolve branch — map the Jira fix version to a base branch (config, or ask)
- Sentry fetch — pull the issue and latest event (stacktrace, request, breadcrumbs, tags)
- Diagnose — Claude, read-only, proposes a root cause and a fix
- Approve — post the diagnosis to the Slack thread and wait for
fixorskip - Fix — Claude, edit mode, applies the fix inside a dedicated git worktree
- Verify — run lint / type-check, auto-retrying type errors up to 2 rounds
- Review — one or two models review the diff against the diagnosis
- PR — commit, push, and open a pull request
- Cleanup — offer to remove the worktree
git clone https://github.com/your-org/mendbot.git
cd mendbot
uv sync
cp .env.example .env
# edit .env, config/repos.yaml, and config/branches.yaml for your org
uv run python main.py # Slack bot (default channel)
uv run python main.py cli PROJ-123 # or run a ticket straight from the terminal| Tool | Purpose | Link |
|---|---|---|
| uv | Python package manager | docs.astral.sh/uv |
| Python 3.14+ | Runtime | — |
| Claude Code CLI | Runs diagnosis, fix, and review as a subprocess | docs.anthropic.com/en/docs/claude-code |
| jira-cli | Fetches ticket metadata and attachment listings | github.com/ankitpokhrel/jira-cli |
sentry CLI (npm package sentry) |
Fetches issue + event context — not the older Rust sentry-cli |
github.com/getsentry/cli |
GitHub CLI (gh) |
Opens pull requests | cli.github.com |
| LiteLLM (optional) | Proxy for routing the secondary reviewer to a non-Anthropic model | github.com/BerriAI/litellm |
Copy .env.example to .env and fill in these variables:
| Variable | Required | Description | Example |
|---|---|---|---|
SLACK_BOT_TOKEN |
Yes | Slack bot token (Socket Mode app) | xoxb-... |
SLACK_APP_TOKEN |
Yes | Slack app-level token (Socket Mode) | xapp-... |
REPOS_BASE_PATH |
Yes | Local path where target repos are cloned | /path/to/local/repos |
GITHUB_ORG |
Yes | GitHub organization that owns the target repos | your-org |
JIRA_BASE_URL |
Yes | Jira instance base URL | https://your-org.atlassian.net |
JIRA_EMAIL |
Yes | Jira account email used for API auth | you@example.com |
JIRA_API_TOKEN |
Yes | Jira API token | ... |
SENTRY_BASE_URL |
Yes | Sentry instance base URL | https://sentry.example.com |
SENTRY_AUTH_TOKEN |
Yes | Sentry auth token | sntryu_... |
SENTRY_ORG |
Yes | Sentry organization slug | your-org |
DOCS_PATH |
No | Path to a local docs repo used for extra diagnosis context | /path/to/business-docs |
SECONDARY_REVIEW_ENABLED |
No | Turns on the secondary reviewer model | true |
SECONDARY_REVIEW_MODEL |
No | Model name routed through the LiteLLM proxy | gpt-5-mini |
SECONDARY_REVIEW_BASE_URL |
No | LiteLLM proxy URL | http://127.0.0.1:4444 |
config/repos.yaml maps a Sentry project to a GitHub repo and its language:
# Maps Sentry project names -> GitHub repo + language.
# Edit for your org.
sentry_project_map:
BILLING-SERVICE:
repo: billing-service
lang: typescript
PAYMENT-API:
repo: payment-api
lang: pythonconfig/branches.yaml maps a Jira fix version to a base branch, with per-repo overrides:
# Maps Jira fix-version -> base branch, with per-repo overrides.
# Edit for your org.
default:
"1.0": main
"1.1": develop
overrides:
payment-api:
"1.0": release/1.0
pr_target: mainconfig/pricing.json holds per-model token pricing (input / output / cache read / cache write) used to estimate the USD cost reported in 05-metrics.md. Edit it to match your model rates.
Create a Slack app with:
- Socket Mode enabled
- Bot scopes:
chat:write,reactions:write,channels:history,groups:history - Event subscriptions:
message.channels,message.groups,app_mention
The entry point dispatches by channel: slack (default) or cli. Each channel is a package implementing the same PipelineUI protocol — a new channel (telegram, discord, ...) is a <channel>/frontend.py plus one entry in main.py's FRONTENDS registry.
- Post a Jira ticket URL in the configured Slack channel
- The bot reacts with 👀 and starts processing
- It posts the diagnosis (root cause, affected files, proposed fix) to the thread
- Reply
fixto apply it, orskipto stop - On
fix, the bot verifies, reviews, and opens a PR, then posts the PR link back to the thread
uv run python main.py cli PROJ-12345 # interactive: prompts fix/skip
uv run python main.py cli https://…/browse/PROJ-12345 # URLs work too
uv run python main.py cli PROJ-1 PROJ-2 # several tickets, sequential
uv run python main.py cli PROJ-12345 --yes # auto-approve the fix
uv run python main.py cli PROJ-12345 --diagnose-only # report only, never fix
uv run python main.py cli PROJ-12345 --verbose # show INFO logsSame pipeline, same report artifacts — stage progress, the diagnosis report, and PR links render in the terminal. Exit code is non-zero if any ticket errored (deliberate skips exit 0). --yes auto-answers the fix/skip gate and takes the safe default on the worktree-cleanup question; unknown repo or missing fix version still prompt. Slack credentials are not required for this channel.
Different models catch different bugs — one might flag a logic error the other misses. When SECONDARY_REVIEW_ENABLED=true, a second reviewer runs in parallel with the primary Claude review, and their findings are merged into a single report tagged by source model. The secondary reviewer is never blocking: if it fails, the primary review result still stands.
The secondary reviewer needs a LiteLLM proxy running locally. Example config.yaml:
model_list:
- model_name: gpt-5-mini
litellm_params:
model: openai/gpt-5-mini
api_key: os.environ/OPENAI_API_KEY
litellm_settings:
drop_params: trueRun it with (the proxy runs on Python 3.12 in its own venv; uvloop is not yet 3.14-compatible):
OPENAI_API_KEY=sk-... uvx --python 3.12 --from 'litellm[proxy]' \
litellm --config config.yaml --port 4444Then set:
SECONDARY_REVIEW_ENABLED=true
SECONDARY_REVIEW_MODEL=gpt-5-mini
SECONDARY_REVIEW_BASE_URL=http://127.0.0.1:4444Attachments on the ticket (screenshots, log files) are automatically downloaded and fed into the diagnosis prompt as extra context. Only images and text/log files up to 5 MB are downloaded — everything else is listed as skipped, with the reason, in the ticket report.
- Allowed:
png,jpg,jpeg,gif,webp,svg,txt,log,csv,json,xml,yaml,yml,md,har - Size limit: 5 MB per file
jira-cli lists attachment metadata (filename, size, content URL); the actual bytes are fetched over authenticated HTTP, since jira-cli itself has no download command.
Every run writes a full audit trail to reports/<TICKET_ID>/ (e.g. reports/PROJ-123/):
| File | Contents |
|---|---|
01-ticket.md |
Jira metadata — key, summary, repo, base branch, fix version, Sentry URL |
01b-sentry.md |
Issue title, stacktrace, request data, breadcrumbs, tag distributions |
02-diagnosis.md |
Root cause, data flow, affected files, proposed fix, risk level |
03-fix-summary.md |
Lint / type-check output after the fix |
03b-review.md |
Review findings by dimension and severity, with model attribution |
04-result.md |
Outcome (fixed / no_changes / error / skipped) and PR URL |
05-metrics.md |
Per-stage token usage, wall-clock duration, and estimated cost |
uv run pytest tests/228 tests cover Slack parsing, the Claude CLI wrapper, orchestration, review, Sentry integration, git operations, config loading, and report generation. External CLIs (jira, sentry, git, gh) are mocked.
- Per-repo review guidelines injected into the review prompt
- Fix retry loop when the first patch attempt doesn't resolve the diagnosed cause
- Streaming progress updates to Slack during diagnosis and fix stages
- Complexity-based model routing (cheaper/faster models for simple bugs)
- Learning from dismissed review findings to reduce false positives over time
- Auto-generated regression tests alongside each fix
MIT — see LICENSE.