Your codebase has secrets. Jack Ryan finds them before they find you.
Read the full write-up on Medium: Building a Background Agent That Monitors Codebases for Risk Indicators
Jack Ryan is a silent background agent that monitors your engineering team's GitHub and Jira activity around the clock. It spots the risk patterns that humans miss or notice too late — and delivers targeted alerts directly to Slack, at the moment they matter.
No dashboards. No queries. No waiting to be asked.
It just watches. And when something is wrong, it tells you.
Incidents don't come out of nowhere. The signals are always there:
- A PR quietly touching a file that's caused three outages
- A 600-line change with no ticket, no review trail, no rollback plan
- Two engineers unknowingly editing the same critical module
- A ticket marked Done that was never actually code-reviewed
- Half the sprint converging on the same service with nobody coordinating
By the time anyone notices, it's too late. Jack Ryan notices first.
Some files break repeatedly. Jack Ryan tracks which files have appeared in past incidents and flags any new PR that touches them — before it merges.
[Risk] PR #42 "Update settlement logic" touches 2 file(s) linked to past incidents:
settlement_engine.py (3 incidents), position_calculator.py (1 incident).
Review carefully before merge.
A PR with 300+ lines changed and no Jira ticket has no review trail, no sprint visibility, and no rollback context. High blast radius, zero traceability.
[Risk] PR #67 "Refactor auth flow" has 847 lines changed but no linked Jira ticket.
Large untracked changes carry rollback risk.
Merge conflicts are annoying. Conflicting logic that merges cleanly is dangerous. Jack Ryan catches simultaneous edits before they hit main — one alert per file, all PRs listed.
[Risk] 4 open PRs are all modifying src/order_router.py:
PR #2 "Add retry logic", PR #3 "Fix timeout handling",
PR #7 "Fix settlement routing", PR #8 "Add metrics".
Coordinate before merging.
Work that's marked done without a PR either wasn't reviewed, was committed directly, or was never implemented. All three are worth knowing about.
[Risk] JRT-204 "Migrate rate limiter config" was marked Done but has no linked pull request.
Was this change code-reviewed?
Parallel work on the same service from different contributors is a coordination risk — especially across team boundaries. Small changes can interact in ways nobody anticipated.
[Risk] 4 tickets in the current sprint all touch the payments service:
JRT-188, JRT-192, JRT-199, JRT-203. Confirm the team is coordinating.
When a risk clears — a PR merges, a ticket gets a PR link, the overlap resolves — Jack Ryan posts a [Resolved] message and moves on. It never re-alerts on the same condition while it's still active.
Every 15 minutes:
GitHub ──────────────┐
├──▶ 5 signal detectors ──▶ compare vs DB ──▶ Slack
Jira ────────────────┘
- Fetch all open PRs and their file diffs from GitHub
- Fetch active sprint tickets, recently Done tickets, and incident tickets from Jira
- Run all 5 signal detectors against the data
- Compare against the active signals database
- New condition → post alert, record in DB
- Condition cleared → post
[Resolved], remove from DB - Condition unchanged → stay silent
- Repeat
State is stored locally in SQLite. No external services. No dependencies beyond the APIs it already monitors.
- Python 3.10+
- GitHub personal access token (read-only)
- Jira Cloud account with a Scrum board (Kanban boards have no sprints — Signal 5 won't work)
- Slack workspace with a bot app
git clone https://github.com/parambhatia2004/jack-ryan
cd jack-ryan
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtRun the interactive setup script — it walks through each credential, explains where to find it, and tests all three connections before writing .env:
python setup.pyThe script will:
- Prompt for GitHub token, org, and repo
- Prompt for Jira credentials and board ID
- Prompt for Slack bot token and channel
- Test all three connections live
- Write
.envif everything passes
If you prefer to configure manually, copy .env.example to .env and fill in each value:
cp .env.example .env- Go to github.com → Settings → Developer settings → Fine-grained tokens
- Generate new token, select your target repo
- Set permissions:
Pull requests: Read-only,Contents: Read-only - Paste into
GITHUB_TOKEN
- Go to id.atlassian.com/manage-profile/security/api-tokens
- Create API token, paste into
JIRA_API_TOKEN - Set
JIRA_EMAIL,JIRA_BASE_URL,JIRA_PROJECT_KEY - Set
JIRA_BOARD_ID— find it in your board URL:.../boards/34→34
Note: Jack Ryan uses the Jira Agile API, which requires a board ID. If you get 410 errors, your project is likely team-managed — setting
JIRA_BOARD_IDfixes this.
- Go to api.slack.com/apps → Create New App → From scratch
- Add the
chat:writescope under OAuth & Permissions - Install to workspace, paste the
xoxb-...token intoSLACK_BOT_TOKEN - Create a channel, invite the bot:
/invite @Jack Ryan - Set
SLACK_CHANNEL
# GitHub
GITHUB_TOKEN=
GITHUB_ORG=
GITHUB_REPO=
# Jira
JIRA_BASE_URL=https://yourorg.atlassian.net
JIRA_EMAIL=
JIRA_API_TOKEN=
JIRA_PROJECT_KEY=
JIRA_BOARD_ID=
# Slack
SLACK_BOT_TOKEN=
SLACK_CHANNEL=#jack-ryan
# Thresholds (defaults shown)
POLL_INTERVAL_MINUTES=15
LARGE_DIFF_THRESHOLD=300
FILE_RISK_SCORE_THRESHOLD=2
SPRINT_SERVICE_OVERLAP_THRESHOLD=3
SIGNAL_4_GRACE_PERIOD_HOURS=2
# Signal 3: files to ignore in simultaneous edit detection
EXCLUDED_FILE_PATTERNS=*.lock,*.md,*.json,*.yaml,*.yml,*.env,*.ambr,tests/*
# Signal 5: service names to match against sprint ticket titles
KNOWN_SERVICES=payments,auth,orders,data-pipeline
# Signal 1: Jira issue types treated as incidents
JIRA_INCIDENT_ISSUE_TYPES=Bug,Incidentsource .venv/bin/activate
python agent.pyJack Ryan validates all three API connections on startup and logs the result before the first cycle runs. One cycle runs immediately, then every POLL_INTERVAL_MINUTES. Logs go to stdout. Alerts go to Slack.
To stop:
pkill -f "agent.py"Always kill the existing process before restarting. Multiple instances will post duplicate alerts. Verify with
pgrep -f "agent.py".
No Python or venv required — just Docker.
First time:
git clone https://github.com/parambhatia2004/jack-ryan
cd jack-ryan
cp .env.example .env
# fill in .env with your credentials
docker compose up -dView logs:
docker compose logs -fUpdate to latest version:
git pull && docker compose up --build -dStop:
docker compose downState persists in a data/ directory on the host via a volume mount — survives container restarts and rebuilds.
Jack Ryan builds a file risk table automatically: when it sees a Jira Bug or Incident ticket linked to a GitHub PR, it records every file that PR touched. Over time this becomes a map of your most dangerous files.
On day one the table is empty. Seed it with files you already know are risky:
source .venv/bin/activate
python3 -c "
import db; db.setup()
db.increment_file_risk_score('src/payments.py')
db.increment_file_risk_score('src/payments.py') # twice = score of 2, hits default threshold
"Checks PR title, description, and branch name for a ticket pattern (PROJ-123). A PR named feature/JRT-10-refactor or with JRT-10 anywhere in its description is correctly excluded.
One alert per file, listing all PRs touching it. Files matching EXCLUDED_FILE_PATTERNS are skipped — add your own patterns for generated files, fixtures, or version files specific to your codebase. Defaults exclude lock files, config files, test files, and snapshot files.
PR links are detected via Jira's remote links API. If your team uses the GitHub for Jira integration, these are created automatically when a PR references the ticket key. Without the integration, a developer can paste the PR URL into the ticket.
The 2-hour grace period accounts for the natural delay between merging a PR and marking the ticket Done. Resolution only fires when a PR link is genuinely found — not when the ticket simply ages out of the query window.
Matches KNOWN_SERVICES keywords against ticket titles (case-insensitive substring match). This signal is disabled if KNOWN_SERVICES is not set, and requires an active Scrum sprint.
State is stored in jack_ryan.db. A signal fires once when a condition appears and resolves when it clears. To force everything to re-fire:
python3 -c "
import sqlite3
with sqlite3.connect('jack_ryan.db') as conn:
conn.execute('DELETE FROM active_signals')
"Docker (recommended):
docker compose up -dsystemd:
[Unit]
Description=Jack Ryan
After=network.target
[Service]
WorkingDirectory=/path/to/jack-ryan
ExecStart=/path/to/jack-ryan/.venv/bin/python3 agent.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target| Problem | Fix |
|---|---|
| No Slack messages | Invite the bot to the channel: /invite @Jack Ryan |
| Jira 410 errors | Set JIRA_BOARD_ID — find in board URL .../boards/34 → 34 |
| Signal 1 never fires | Seed the risk table manually (see above) |
| Signal 5 never fires | Set KNOWN_SERVICES and ensure a sprint is active |
| Duplicate alerts | Kill all instances with pkill -f "agent.py" before restarting |
| Signals re-fire after restart | Expected if jack_ryan.db was deleted — all conditions fire once on next cycle |
| Too many Signal 3 alerts | Add directory patterns to EXCLUDED_FILE_PATTERNS, e.g. tests/*,*/migrations/* |