A client-facing POC proving an LLM can autonomously run an end-to-end ops workflow: read monitoring logs → triage errors → file a ticket → write a code fix → open a PR, with a human only firing the initial trigger.
Constraints: ~1.5 business days, lightweight MVP we can iterate on.
Locked decisions:
- Orchestrator: Python
- Ticketing: Trello
- Fix/PR (Phase 1): single-shot patch generated by Claude, committed to a branch, PR opened
via the
ghCLI. Behind aFixerinterface so an Agent SDK implementation drops in later with no orchestrator changes. - Target codebase: a self-contained planted-bug
sample_service/in this repo; the PR opens against this same repo.
Environment verified: gh authed as paytown; origin = github.com/stride-nyc/llm-monitoring-demo;
.env holds CLAUDE_API_KEY.
A linear pipeline orchestrated by one entrypoint. Every external system sits behind a small module so the mock version now and the real version later share an interface.
run.py # CLI entrypoint: `python run.py --since 1d`
agent/
config.py # load .env (reads CLAUDE_API_KEY), settings, FIXER flag
llm.py # Anthropic SDK wrapper; prompt caching; structured tool-use helper
monitoring.py # read_alerts(since) -> list[LogEntry] (mock JSON now; Sentry/DataDog later)
triage.py # triage(entries) -> list[TriageResult] via Claude
ticketing.py # Trello: create_card -> Ticket; move_card(card, list); attach PR link
# card state machine: To Do -> Doing -> PR (live on the board)
fixer.py # Fixer protocol + SingleShotFixer (now), AgentSDKFixer (stub)
github_pr.py # branch + commit patched file + `gh pr create` -> pr_url
models.py # dataclasses: LogEntry, TriageResult, Ticket, FixResult
mock_data/logs.json # timestamped alerts; error entries point at sample_service bug(s)
sample_service/
app.py # tiny buggy "service" the logs reference
test_app.py # failing test demonstrating the bug
.env.example # documents all required keys
requirements.txt # anthropic, python-dotenv, requests
- Trigger (human-in-the-loop, once):
python run.py --since 1d. Narrates each step to stdout. - Read logs:
monitoring.read_alerts(since)loadsmock_data/logs.json, filters by time window (timestamps relative to today so--sincevisibly works). ReturnsLogEntrylist. - Triage:
triage.triage(entries)sends ERROR-level entries to Claude to dedupe/group, assign severity, identify the implicated file + likely root cause, and produce a ticket title + body. Structured JSON via tool-use (no brittle parsing). - Ticket: for each actionable
TriageResult,ticketing.create_card()POSTs a Trello card to the To Do list and returns its URL/id. - Fix → PR: for each ticket:
- Move card To Do → Doing (
ticketing.move_card) to show work has started. fixer.fix(ticket, repo_context):SingleShotFixerreads the implicated file, sends file + ticket + error to Claude, gets the full corrected file, writes it to branchfix/<slug>-<cardid>, commits, andgithub_pr.open_pr()opens the PR via the GitHub REST API.- PR body references the Trello card; attach the PR URL to the card and move it Doing → PR (loop closed both directions; card marches across the board live).
- Move card To Do → Doing (
- Summary: print
alert → ticket URL → PR URLfor each handled error.
Both implementations take identical inputs and produce identical outputs — the only difference is how files on disk get changed. Everything upstream (triage, ticket) and downstream (commit, PR, comment) is untouched.
triage → ticket → [ Fixer.fix(ticket, ctx) → FixResult ] → git commit → gh pr create → Trello comment
^ only this box changes ^ consumes FixResult; agnostic to how made
class Fixer(Protocol):
def fix(self, ticket: Ticket, ctx: RepoContext) -> FixResult: ...
# FixResult = (branch_name, files_changed, summary)
def make_fixer(config) -> Fixer: # factory keyed on config.FIXER
return AgentSDKFixer() if config.FIXER == "agent_sdk" else SingleShotFixer()run.pycallsmake_fixer(config)once, thenfixer.fix(...). Never branches on the impl.- Phase 1 —
SingleShotFixer: one Claude call returns corrected file content; writes one file. Deterministic, demo-safe, fast. - Phase 2 —
AgentSDKFixer(if time): Claude Agent SDK loop with read/edit/bash tools — can explore, fix multiple files, run tests. Edits the working tree directly. - Both leave the same artifact: a changed working tree + a
FixResult. Upgrading = add one file- flip
FIXER=agent_sdk. Zero changes torun.py.
- flip
- LLM (
llm.py): Anthropic Python SDK, client built withapi_keyfromCLAUDE_API_KEY.claude-opus-4-8for both triage and fix (best quality for the demo; cost-tune later via theTRIAGE_MODEL/FIX_MODELenv vars). Adaptive thinking + structured outputs (messages.parse) + prompt caching on the system prompt / file context. - Planted bug: one clear bug in
sample_service/app.py(e.g.KeyError/ off-by-one / unguarded division). The matchinglogs.jsonerror carries a stack trace naming file + line. - Time filtering: seed
logs.jsonwith several entries inside the last day and a couple older so--since 1ddemonstrably filters. - Mock-to-real seams:
monitoringandfixerare the only two swap points; both isolated. - Safety: every PR opens on a fresh branch; nothing auto-merges; human reviews the PR. New
.envkeys:TRELLO_KEY,TRELLO_TOKEN,TRELLO_LIST_ID(documented in.env.example).
read_alerts(since) sits behind a MonitoringSource interface; the LogEntry model is the
contract. Mock reads JSON; a real adapter maps a provider's API response into LogEntry.
class MonitoringSource(Protocol):
def read_alerts(self, since: timedelta) -> list[LogEntry]: ...
# MockSource (now) reads mock_data/logs.json
# SentrySource / DatadogSource (later) call the provider API and map -> LogEntryRecommended provider: Sentry (not Google Analytics):
- Sentry (★ recommended): purpose-built error tracking. Events carry exception type, message,
stack trace + file/line, release — exactly what triage needs. Free tier, token auth.
GET /api/0/projects/{org}/{proj}/issues/?statsPeriod=24hthen latest event for stack detail. - DataDog Logs (alternative):
POST /api/v2/logs/events/searchwithstatus:error+ time window. Strong if the client already runs DataDog; logs often lack a code location. API+APP keys. - Google Analytics (not recommended): web/product analytics — no errors/stack traces/code locations, nothing for triage→fix to act on.
Shaping the mock like a Sentry issue (stack frame naming file+line) makes SentrySource a near
drop-in.
- Scaffold:
requirements.txt,agent/package,config.py,models.py,.env.example. -
sample_service/app.py+ planted bug +mock_data/logs.jsonreferencing it. -
monitoring.read_alerts+ time-window filtering. -
llm.py+triage.py(structured outputs viamessages.parse). -
ticketing.pyTrello create-card + move across board (To Do/Doing/PR). -
fixer.pySingleShotFixer+github_pr.py(branch + single commit + PR via REST API). -
run.pywiring + narrated output;--dry-run/--max-tickets; end-to-end verified.
- Dedicated bot identity for PRs. PRs are currently authored by the token owner (the
GITHUB_TOKENin.env). Create a separate GitHub machine account, mint its PAT, and use that so PRs + commits show as e.g.monitoring-agent-bot— the clean "an agent did this, not a human" look. - Real monitoring source (
SentrySource/DatadogSource) behind the existingMonitoringSourceinterface — Sentry recommended (stack traces with file/line map cleanly to triage). -
AgentSDKFixer— swap the single-shot fix for an agentic Claude Agent SDK loop (multi-file fixes, can run the tests) viaFIXER=agent_sdk. Interface already in place. - More varied mock logs / multiple simultaneous bugs; a polished demo script.
- Unit-ish: run
sample_service/test_app.pybefore fix (fails) / after fix (passes). - Per-stage:
python run.py --since 1d --dry-runprints parsed logs, triage JSON, and the proposed patch without touching Trello/GitHub. - End-to-end:
python run.py --since 1d→ Trello card created,fix/*branch pushed, PR ongithub.com/stride-nyc/llm-monitoring-demo, PR URL commented back on the card.--since 1hfilters out older seeded errors.
Create a Trello API key + token and a target list, then add TRELLO_KEY / TRELLO_TOKEN /
TRELLO_LIST_ID to .env. (If Trello signup is a timeline risk, ticketing can fall back to
GitHub Issues with a one-line config change.)