github-triage-agent
# GitHub Triage Agent
[](https://github.com/umeshmynampati3-cmd/github-triage-agent/actions/workflows/ci.yml)
An autonomous GitHub issue & pull-request triage agent: a **LangGraph** state
machine that classifies, deduplicates, prioritizes and acts on repository
traffic, exposed to any MCP client (Claude Desktop, IDEs) through a
**Model Context Protocol** server — with a hard human-in-the-loop safety
layer, per-repository customer policies, a full audit trail, and a
36-scenario evaluation harness gating it all in CI.
```
Analyzing acme/backend#42… (DRY RUN)
Type: Issue
Classification: bug Confidence: 93%
Severity: high Priority: high
Possible duplicate: #7 — "OAuth callback crashes when token expires" (confidence 96%, similarity 0.82)
Actions:
◦ Add labels: bug, P1 — would add labels ['bug', 'P1']
Held for human approval:
⚠ Post comment (214 chars) — Explains the duplicate verdict before closing as duplicate of #7.
⚠ Close as not_planned — Likely duplicate of #7 with 96% confidence.
Escalated: close_issue is destructive and requires confirmation
```
## 1. Problem
Maintainers spend hours a week on repetitive triage: classifying reports,
hunting duplicates, chasing missing reproduction info, mapping components to
owners, nudging PRs without tests. An LLM can do the judgment; the hard part
is doing it *safely* — no hallucinated assignees, no silently closed issues,
no actions a repository's maintainers didn't opt into. This project treats
that as an engineering problem, not a prompting problem.
## 2. Demo
```bash
# dry-run: full reasoning, zero GitHub mutations
triage-agent run --repo you/triage-demo --issue 2 --dry-run
# live, with interactive approval for anything sensitive
triage-agent run --repo you/triage-demo --issue 2 --live
```
[`docs/DEMO_REPOSITORY.md`](docs/DEMO_REPOSITORY.md) seeds a demo repository
with six issues and three PRs (valid bug, duplicate, feature request,
missing-repro, docs request, security report, PR w/o tests, failing CI) so
the whole system can be shown end-to-end without touching a real project.
## 3. Architecture
```mermaid
flowchart TD
Client["MCP Client (Claude Desktop)"] --> Server["MCP Server — 10 tools"]
CLI["CLI / evals"] --> Runner["TriageRunner"]
Server --> Svc["TriageToolService<br/>validation • dry-run • confirmation"]
Runner --> Graph["LangGraph triage graph<br/>typed TriageState"]
Graph --> LLM["Anthropic / OpenAI<br/>structured outputs"]
Graph --> Policy["Policy engine<br/>per-repo config"]
Graph --> Safety["Guardrails + interrupt approvals"]
Svc --> GH["GitHub client<br/>retries • rate limits • allowlist"]
Graph --> GH
Runner --> Audit["SQLite audit trail"]
```
Full detail, including the graph diagram and the approval sequence diagram:
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
## 4. Why MCP
MCP turns the agent's GitHub capabilities into a typed, discoverable tool
surface any client can drive — Claude Desktop today, an IDE tomorrow — with
the safety semantics owned by the *server*, not the model: inputs are
schema-validated, mutations respect dry-run, and destructive tools implement
a two-step confirmation protocol that works on every MCP client. The LLM
only ever sees predefined tools; there is no path from model output to an
arbitrary GitHub API call or a shell.
## 5. Agent workflow
The workflow is an explicit LangGraph state graph (no giant prompt):
```
START → fetch_repository_context → fetch_issue_or_pr → classify_request
→ gather_additional_context → search_duplicates
→ assess_priority_and_severity → determine_actions → safety_check
→ human_approval (interrupt, only when needed) → execute_actions
→ verify_actions → generate_summary → END
```
- **LLM (structured outputs only)**: classification, severity/priority,
missing-information analysis, duplicate comparison, comment wording.
Every response is validated against a Pydantic schema via
`messages.parse` — critical decisions are never parsed from prose.
- **Deterministic code**: PR facts (failing CI, diff size, tests/docs
touched) computed from the diff and forced over the model's echo; labels
filtered to the repo's real label set; duplicate verdicts only accepted if
they reference retrieved candidates; CODEOWNERS-grounded assignment with
an assignability check — unverifiable users are dropped, never guessed.
- **Hybrid duplicate detection**: keyword search → lexical ranking →
LLM comparison of the strongest candidates → threshold-gated proposal.
## 6. Tool definitions (MCP)
| Tool | Kind | Notes |
|---|---|---|
| `get_issue` | read | full metadata + comments |
| `search_issues` | read | repo-scoped GitHub search |
| `get_repository_context` | read | README, CONTRIBUTING, CODEOWNERS, labels, topics |
| `get_pull_request` | read | files, commits, reviews, CI checks, linked issues |
| `get_recent_repository_activity` | read | recently updated issues/PRs |
| `add_labels` | write | dry-run aware |
| `post_comment` | write | dry-run aware |
| `assign_issue` | write | assignability-verified; never guesses users |
| `close_issue` | **destructive** | two-step human confirmation required |
| `reopen_issue` | **destructive** | two-step human confirmation required |
## 7. Safety model
Layered, each independently tested:
1. **Input validation** — strict Pydantic schemas on every tool; repository
allowlist enforced on every GitHub call.
2. **Dry-run** (`DRY_RUN=true`, default) — full reasoning, `WOULD EXECUTE`
output, and a read-only GitHub client underneath as defence in depth.
3. **Policy engine** — per-repository automation opt-ins (labels, comments,
assignment, duplicate closing).
4. **Confidence gating** — configurable thresholds: ≥0.90 safe actions may
auto-run, 0.70–0.90 only safe-risk actions, <0.70 recommendations only.
5. **Guardrails** — destructive actions never auto-run; security-sensitive
contexts hold comments/assignments; bulk batches held; guardrails can
only approve or hold, never reject (that's the human's call).
6. **Interrupt approvals** — LangGraph checkpoint + `interrupt()`; the run
pauses with action/reason/evidence and resumes with per-action decisions;
undecided actions default to rejected. Silence never approves.
7. **Idempotency** — existing labels skipped, marker-tagged comments never
double-posted, effects verified by re-fetch after execution.
The critical invariant — *a destructive GitHub action can never execute
without explicit confirmation* — has a dedicated test
(`tests/test_safety.py::test_critical_invariant_close_never_executes_without_confirmation`)
and is re-checked across the whole eval suite on every run.
## 8. Evaluation results
`triage-agent eval` runs 36 JSON-defined scenarios through the **real**
graph, planner, guardrails and executor, with the two network edges (GitHub,
LLM) scripted for determinism. Current results (this repo, reproducible):
```
Evaluation Results
==================
Scenarios: 36
Task success: 100.0% (36/36)
Classification accuracy: 100.0%
Correct-label rate: 100.0%
Duplicate precision: 100.0%
Duplicate recall: 100.0%
Unsafe-action rate: 0.0%
Human-escalation accuracy:100.0%
Tool-call success rate: 100.0%
Average retries/call: 0.00
Mean triage latency: 3 ms (pipeline overhead, LLM/network excluded)
```
Honest framing: these numbers measure the *decision pipeline* — planning,
policy, safety, execution, idempotency, error recovery — under scripted
model outputs, including adversarial ones (hallucinated duplicate
references, LLM outages, unverifiable assignees, GitHub failures). They are
not live-model classification accuracy. For that, the same scenarios run
against the real configured model:
```bash
triage-agent eval --live # requires ANTHROPIC_API_KEY; skips scripted-outage scenarios
```
The scripted suite is a pytest gate, so `unsafe-action rate = 0%` is
enforced on every commit, and 105 unit/integration tests cover the layers
individually.
**Live-API verification** (against the seeded demo repository, real GitHub
REST API): repository context + CODEOWNERS fetched, duplicate search
returned the seeded pair, `add_labels` applied `bug`/`P1` live, the
repository allowlist blocked a non-allowlisted repo, and `close_issue`
without confirmation returned `confirmation_required` while the issue
stayed open — the destructive gate holds against the real API, not just in
tests.
## 9. Reliability engineering
- Exponential backoff with jitter on transport errors, 5xx and 429s;
GitHub `Retry-After`/`X-RateLimit-Reset` honoured; bounded attempts;
per-client retry metrics recorded into the audit trail.
- LLM calls: schema validation via `messages.parse`, retry on malformed
output, typed failure (`LLMError`) that degrades the run to an explicit
human escalation instead of crashing.
- Action isolation: each action executes independently; one failure never
aborts the batch; destructive actions run last.
- Idempotency: label diffs, comment markers, assignability verification.
- `verify_actions` re-fetches the issue and confirms effects actually landed.
## 10. Setup
```bash
git clone https://github.com/umeshmynampati3-cmd/github-triage-agent && cd github-triage-agent
python3.12 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]" # add ,openai for the OpenAI backend
cp .env.example .env # fill in GITHUB_TOKEN + ANTHROPIC_API_KEY
cp config.example.yaml config.yaml # thresholds + per-repo policies
pytest # 105 tests
triage-agent run --repo you/repo --issue 1 --dry-run
```
`DRY_RUN=true` is the default everywhere — the agent reasons fully but
prints `WOULD EXECUTE` instead of mutating GitHub.
## 11. Docker
```bash
docker build -t github-triage-agent .
docker run --env-file .env github-triage-agent # MCP server
docker run --env-file .env github-triage-agent \
run --repo you/repo --issue 1 --dry-run # CLI
```
Runs as a non-root user (`uid=1000 triage`); the SQLite audit DB lives on
the `/data` volume (`docker-compose.yml` provided). The image build and an
in-container run of the full eval suite are verified in CI and locally
(colima).
## 12. Claude Desktop / MCP setup
`claude_desktop_config.json` (Settings → Developer → Edit Config):
```json
{
"mcpServers": {
"github-triage": {
"command": "/path/to/github-triage-agent/.venv/bin/triage-agent",
"args": ["serve-mcp"],
"env": {
"GITHUB_TOKEN": "ghp_…",
"DRY_RUN": "true",
"TRIAGE_CONFIG": "/path/to/config.yaml"
}
}
}
}
```
Then ask Claude: *“Analyze issue #142 in my repository and triage it.”*
Claude will call `get_issue` → `get_repository_context` → `search_issues`,
propose labels/assignment, and — if it recommends closing — `close_issue`
returns a proposed action that Claude must show you; only your approval
(and a `confirm=true` re-call) executes it.
## 13. Running evaluations
```bash
triage-agent eval # printed report
triage-agent eval --json eval_results/latest.json
pytest tests/test_evals.py # the same suite as a CI gate
```
Add scenarios by dropping a JSON file into `evals/scenarios/` — inputs,
scripted model outputs, config overrides, and expectations (required /
forbidden proposals and executions, escalation, labels).
## 14. Example executions
Issue with missing info (dry run):
```
Classification: bug Confidence: 85%
Missing info: package version, complete traceback, minimal reproduction steps
◦ Add labels: bug, P1 — would add labels ['bug', 'P1']
Held for human approval:
⚠ Post comment (189 chars) — Bug report is missing information needed to debug.
```
PR without tests:
```
Type: Pull request
Classification: bug Confidence: 92%
Flags: missing tests
◦ Add labels: bug
Held for human approval:
⚠ Post comment — PR is missing tests/docs or has failing CI.
⚠ Request review from: alice — CODEOWNERS maps the changed files to these maintainers.
```
## 15. Design trade-offs
- **Deterministic planner over LLM tool-loop.** The model produces validated
*assessments*; code turns them into actions. Auditable, cheap to test, and
the safety layer gates a closed action vocabulary (there is no
`merge_pr` action to hallucinate).
- **Confirmation as protocol, not UI.** The MCP `close_issue` two-step works
on any client without relying on elicitation support.
- **Scripted-edge evals.** Deterministic and CI-fast; live-model quality is
measured separately rather than making every CI run cost tokens.
- **Own GitHub client over PyGithub.** Uniform async, injectable transport
for tests, and the allowlist/read-only gates sit below every caller.
- **SQLite behind a protocol.** Local-first; PostgreSQL is one new class.
## 16. Future improvements
- Embedding-based duplicate retrieval (the ranking hook exists) in front of
the LLM comparison.
- Webhook mode: triage on `issues.opened` events instead of on demand.
- PostgreSQL audit store + a small FastAPI dashboard over `agent_runs`.
- Live-model eval mode with labeled ground truth to track classification
accuracy per model/prompt version.
- Team-aware review requests (org team slugs) and multi-repo batch triage.
TDQS
Scored across 10 tools
Each tool targets a distinct resource and action: issue retrieval, PR retrieval, repo context, recent activity, search, labeling, commenting, assigning, closing, and reopening. The retrieval tools have clearly different scopes (specific issue, search results, recent updates), so agents can select correctly without ambiguity.
All tool names use consistent snake_case and follow a predictable verb_noun pattern. Retrieval tools use get_ or search_, while mutation tools use clear action verbs like add_, post_, assign_, close_, and reopen_.
Ten tools is well-scoped for a GitHub triage agent. Each tool covers a focused triage operation, and there is no obvious bloat or redundancy in the set.
The surface covers the core triage lifecycle: reading issues/PRs, searching, labeling, commenting, assigning, closing, reopening, and fetching repository context. Minor gaps remain, such as removing labels, unassigning users, or editing issue/comment content, but agents can work around these for most triage workflows.