jira-watcher-agent
An AI-powered JIRA planner, organizer, and triage CLI for engineers.
Fetch every JIRA ticket assigned to you, get an LLM-generated triage digest by category, and investigate potential resolutions — all from your terminal, powered by the coding CLI you already use (Claude Code, OpenAI Codex, or Cursor Agent).
**Quickstart · Configuration · Integrations · Resolve stage · How it works · Troubleshooting**
Also known as a JIRA planner, JIRA organizer, JIRA triage tool, JIRA AI agent, JIRA CLI, or a Claude / Codex / Cursor JIRA integration.
Public alpha. Core workflows are usable day-to-day, but flag names, JQL defaults, and memory schema may still change.
Before you begin
Make sure you have:
- Python 3.9+ with
pipandvenv. - A JIRA account — either JIRA Cloud (Atlassian-hosted, including custom
vanity domains like
jira.mycompany.com) or JIRA Server / Data Center. - One coding CLI on your
PATH— any ofclaude,codex, orcursor-agent. Watcher shells out to it; you keep your existing subscription and auth. - A JIRA credential appropriate to your flavor (see Configuration).
You do not need a separate Anthropic, OpenAI, or Cursor API key.
Quickstart
# once published to PyPI
pip install jira-watcher-agent
# or from source
git clone https://github.com/<you>/jira-watcher-agent && cd jira-watcher-agent
python -m venv .venv && source .venv/bin/activate
pip install -e .
# configure
cp .env.example .env
$EDITOR .env # fill in JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN
# verify auth, then run
watcher --whoami # confirms which JIRA account you're hitting
watcher # one-shot triage digest
watcher --watch # keep polling
Three console commands ship with the package — pick whichever reads best in your muscle memory:
| Command | What it is |
|---|---|
watcher |
Short, original name — used everywhere in this doc. |
jira-watcher |
Longer alias, disambiguates from other "watcher" tools. |
jira-watcher-agent |
Full name — useful in scripts and docs. |
First run produces an LLM-authored triage digest of every ticket assigned
to you, grouped by category, with a small stats table underneath. Answer
y to the follow-up prompt to investigate specific tickets in detail
(see the Resolve stage).
Table of contents
- Before you begin
- Quickstart
- Why a JIRA planner CLI?
- Configuration
- Usage
- Integrations
- Resolve stage
- How watcher works
- Capabilities
- Supported providers
- Memory & categorization
- Token optimization
- Troubleshooting
- Project layout
- When does watcher connect to what?
- License
Why a JIRA planner CLI?
Every engineer with a real JIRA queue lives with the same three problems that a JIRA planner, organizer, or triage tool is supposed to solve:
- The queue is noisy. Twenty tickets, six statuses, three priorities, two "urgent" flags — and no single view tells you what actually matters this morning.
- Reading tickets is slow. Descriptions, comments, and status hops are scattered. You end up context-switching into the browser just to remember what a ticket is about.
- You lose the shape of the work. Six months in, you have no honest picture of whether you're mostly fixing bugs, shipping features, upgrading dependencies, or answering support questions.
jira-watcher-agent is a small opinionated JIRA AI agent for exactly this
workflow:
- It fetches what's assigned to you (or any JQL you like).
- It hands the payload to an LLM CLI you already have — no extra API keys — and gets back a terse, category-grouped digest optimized for reading in a terminal.
- It remembers the category the LLM assigns to each ticket, so re-runs skip work that hasn't changed and you can watch the composition of your backlog evolve over time.
The whole thing is ~1000 lines of Python, one dependency layer, and no service to host.
Configuration
All settings live in a .env file at the project root. Only three variables
are strictly required for JIRA; everything else has sensible defaults.
Common settings
# Pick your LLM CLI — one of: claude | codex | cursor
LLM_PROVIDER=claude
# Watch-mode polling interval (seconds)
WATCHER_INTERVAL=300
# Where to persist ticket→category memory.
# Default:
# * from-source install: <project-root>/memory/memory.json
# * PyPI install: ~/.watcher/memory.json
# WATCHER_MEMORY_PATH=/absolute/path/to/memory.json
JIRA Cloud (Atlassian-hosted)
Applies whether your site is <name>.atlassian.net or a custom vanity
domain (e.g. prod.jira.yourcompany.com) that's still an Atlassian Cloud
tenant.
JIRA_FLAVOR=cloud
JIRA_BASE_URL=https://your-tenant.atlassian.net
JIRA_EMAIL=you@example.com
JIRA_API_TOKEN=ATATT3xFf...
- Create the API token at https://id.atlassian.com/manage-profile/security/api-tokens.
JIRA_EMAILmust be the login email of the Atlassian account that created the token.- Watcher auto-discovers your tenant's
cloudIdfrom<base>/_edge/tenant_infoand routes API calls throughhttps://api.atlassian.com/ex/jira/<cloudId>— this is required for scopedATATT…tokens, which do not authenticate against the site URL. If discovery is blocked, setJIRA_CLOUD_ID=<uuid>in.env.
JIRA Server / Data Center (self-hosted)
JIRA_FLAVOR=server
JIRA_BASE_URL=https://jira.internal.corp
JIRA_API_TOKEN=<your Personal Access Token>
# JIRA_EMAIL is not needed for server flavor
- Create the PAT in JIRA: your avatar → Profile → Personal Access Tokens → Create token.
- Watcher uses Bearer auth and the v2 REST API.
Optional overrides
JIRA_JQL=assignee = currentUser() AND sprint in openSprints()
JIRA_USER_AGENT=Mozilla/5.0 (compatible; watcher/0.1)
LLM_CLI_PATH=/opt/homebrew/bin/claude
Usage
One-shot digest
watcher # default provider, default JQL
watcher --provider codex # pick a provider ad hoc
watcher --jql 'assignee = currentUser() AND priority = Highest'
Watch mode
watcher --watch # poll every WATCHER_INTERVAL seconds
watcher --watch --interval 120 # override interval
The LLM is only invoked when tickets actually change (fingerprinted by
(key, updated)). Quiet periods print a one-line "no changes" heartbeat.
All flags
| Flag | Purpose |
|---|---|
--provider {claude,codex,cursor} |
Pick the LLM CLI for this run. |
--cli-path PATH |
Override the binary invoked for the provider. |
--jql "..." |
Override the JQL query. |
--watch |
Poll on a loop; re-summarize only on change. |
--interval N |
Polling interval in seconds. |
--no-llm |
Skip the LLM; use a rule-based digest (still shows cached categories). |
--forget |
Wipe the category memory before running. |
--whoami |
Print the JIRA account the token authenticates as, then exit. |
--debug |
Print JIRA request/response diagnostics (URL, page counts, cloudId, etc.). |
--check [target] |
Verify integration connectivity — see Integrations. |
--eks-login |
Write a kubeconfig entry via aws eks update-kubeconfig, then exit. |
--resolve |
Deep-investigate specific tickets — see Resolve stage. |
--keys A,B,C |
With --resolve, comma-separated issue keys to investigate. |
--category NAME |
With --resolve, pick currently-assigned tickets tagged with this category. |
--limit N |
Cap tickets per run (default: 3). Ignored when --keys is used — explicit keys always run in full. |
--no-interactive |
Skip the post-digest "investigate any tickets?" prompt. |
--kube-context NAME |
kubectl context used for read-only EKS probes during investigation. Overrides KUBE_CONTEXT env and skips the interactive picker. |
Integrations
Watcher connects to systems adjacent to your JIRA queue so it can (later) suggest concrete resolution steps — the linked PR, the failing pipeline, the pod that's crash-looping. As of this release, connectivity is verify-only; each integration is optional and activates when its env vars are present.
| System | Purpose | Auth |
|---|---|---|
| JIRA | Fetch assigned tickets | Cloud API token (via gateway) or Server PAT |
| Bitbucket | Repos, PRs, commits | Atlassian API token (Cloud) or PAT (DC) |
| Jenkins | Build & pipeline lookups | Username + API token |
| GitHub | Repos, PRs, issues | PAT or gh CLI |
| EKS | Cluster & workload inspection | AWS CLI + kubectl |
Configure
Add whichever sections you want to .env; unset ones are simply skipped.
# Bitbucket (Cloud shown — Data Center uses BITBUCKET_AUTH_MODE=bearer)
BITBUCKET_HOST=bitbucket.org
BITBUCKET_WORKSPACE=your-workspace
BITBUCKET_EMAIL=you@example.com
BITBUCKET_API_TOKEN=ATATT3xFf...
# Jenkins
JENKINS_BASE_URL=https://jenkins.example.com
JENKINS_USERNAME=your-jenkins-username
JENKINS_API_TOKEN=<api-token-from-jenkins>
# GitHub — either a PAT or an authenticated `gh` CLI on PATH is enough
GITHUB_TOKEN=ghp_xxx
# GITHUB_USE_GH=1 # prefer `gh api` over REST even when a token is set
# EKS — needs AWS CLI v2 and kubectl on PATH; run `aws sso login` first.
# When resolve-stage investigation runs, watcher lists your kubectl
# contexts and asks which to use (unless KUBE_CONTEXT or --kube-context
# is set). Probes are strictly read-only: kubectl get + describe only.
EKS_CLUSTER_NAME=prod-cluster
EKS_REGION=us-east-1
# EKS_NAMESPACE=default
# AWS_PROFILE=my-profile
# KUBE_CONTEXT=arn:aws:eks:us-east-1:123456789012:cluster/prod
Verify
watcher --check # every configured integration
watcher --check bitbucket # just one
watcher --check eks # aws sts + describe-cluster; no writes
The check prints a status table (identity, details, error, hint) and exits
0 when every checked integration is OK.
EKS login
Verification is read-only. To write a kubeconfig entry so kubectl can
talk to the cluster:
watcher --eks-login # runs `aws eks update-kubeconfig` under the hood
Where the patterns come from
Bitbucket and Jenkins clients are ported from
engineering-knowledge-base/skills/{bitbucket-agent,jenkins}/scripts/*_common.py
so behavior matches the other tooling in the org (User-Agent, auth modes,
keychain fallback via security find-generic-password). GitHub and EKS
have no prior KB skill, so watcher defines fresh but conventional patterns.
Resolve stage
The digest tells you what's in your queue. The resolve stage tells you whether a specific ticket is a real, currently-active defect — grounded in live evidence pulled at investigation time — and what to do about it.
Read-only guarantee. Investigation only reads — JIRA, Bitbucket, GitHub, and
kubectl get/describecalls. No writes, noapply, nopatch, nouse-contextmutation of your kubeconfig. Enforced at the code level by a subcommand guard on everykubectlcall.
Invocation
There are two ways in:
1. Interactive follow-up (default). After a one-shot watcher run
prints the digest, it asks:
Investigate any tickets from this list? [y/N]:
Answer y and watcher shows the categories present in the current
queue, then a single smart prompt that auto-detects category name vs.
JIRA keys:
Available categories: bug (5), infra (3), refactor (2)
Pick a category name or JIRA keys (e.g. bug or PROJ-1, PROJ-2): bug
Limit (3): 5
Or pick keys directly — no limit prompt is shown because the caller has already committed to N tickets:
Pick a category name or JIRA keys (e.g. bug or PROJ-1, PROJ-2): PROJ-123, PROJ-456
Context is preserved across the digest → prompt → investigation flow —
the already-fetched ticket list and in-memory categories are reused, so
selecting by category makes zero extra JIRA calls; only the actual
per-ticket enrichment hits GET /issue/{key} per chosen ticket.
After each investigation you're asked whether to investigate more from the same queue, so you can chain a few passes in one session.
The prompt is skipped automatically when stdin/stdout aren't a TTY
(pipes, cron, CI), or when you pass --no-interactive, or when
--no-llm is set (no LLM = no investigation).
2. One-shot flags (scriptable).
# investigate two tickets by key (--limit is ignored: keys are explicit)
watcher --resolve --keys PROJ-123,PROJ-456
# investigate the top N cached-'bug' tickets from your queue
watcher --resolve --category bug --limit 5
# swap providers ad hoc
watcher --resolve --keys PROJ-123 --provider codex
# pin the kubectl context used for EKS probes (skips the picker)
watcher --resolve --keys PROJ-123 --kube-context prod-us-east-1
--category reads from watcher's memory, so run watcher at least once
before this mode so tickets have categories assigned.
Kube context selection
Whenever the investigation is about to hit EKS (i.e. EKS_CLUSTER_NAME
EKS_REGIONare configured), watcher enumerates your kubectl contexts and asks which one to use. It never mutates your kubeconfig (nokubectl config use-context) — the chosen context is passed on every call as--context <name>.
Available kubectl contexts (read-only probes will use the one you pick)
# ┃ Context ┃ Note
━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━
1 │ arn:aws:eks:us-east-1:…:cluster/prod │ current
2 │ arn:aws:eks:us-east-1:…:cluster/staging │
3 │ arn:aws:eks:us-east-1:…:cluster/dev │
Type a number, a full context name, or `skip` to run without EKS probes.
Use kube context (1): 2
Using kube context arn:aws:eks:us-east-1:…:cluster/staging (read-only:
kubectl get only — nothing will be modified).
How the pick is resolved:
| Signal | Effect |
|---|---|
--kube-context NAME CLI flag |
Uses it, skips the picker. |
KUBE_CONTEXT env var |
Uses it, skips the picker. |
| Interactive TTY + contexts available | Shows the table, defaults to current. |
| Non-interactive + a current context exists | Uses current context silently. |
| Non-interactive + no current context | Skips EKS probes; digest still runs. |
| Empty kubeconfig | Skips EKS probes with a hint to run watcher --eks-login. |
User types skip |
Skips EKS probes for the session. |
What it fetches (live evidence)
For each ticket, watcher probes every downstream system named in the ticket at investigation time and hands the LLM the answers — the LLM is instructed to use those answers rather than telling you to "go check yourself." Evidence streams (weighted top-down):
- Live workload evidence (kubectl) — for each candidate deployment
name extracted from JIRA
components+labels, watcher runs read-only calls against the selected context:
kubectl get deployment <name> -n <ns> -o json→Availablecondition, ready/desired replicas, transition time.kubectl get pods -l app=<name> -n <ns>→ up to 5 pods with phase, ready flag, restart count, andwaiting.reason(e.g.CrashLoopBackOff,ImagePullBackOff).kubectl get events --field-selector involvedObject.name=<name>→ up to 5 most-recent events. Ready pods + no Warning events → nudges towardLIKELY_INVALID. CrashLoop / recent Warning events → nudges towardREAL/LIKELY_REAL. Missing deployment → no signal.
- Live repo evidence (Bitbucket / GitHub API) — for each repo URL watcher spots in the description, comments, or JIRA remote links, it calls the corresponding API to check existence:
- Bitbucket Cloud:
GET /2.0/repositories/<ws>/<repo> - Bitbucket DC:
GET /rest/api/1.0/projects/<key>/repos/<slug> - GitHub:
GET /repos/<owner>/<repo>(viagh apiwhen available) The result lands in the prompt as either EXISTS (with default branch,last_activity, private/public flag) or NOT FOUND (404) — repo already deleted or renamed. So if the ticket says "deletems-ccx-verification" and Bitbucket returns 404, watcher tells the LLM "the repo is already gone" and the verdict becomesINVALIDwith a "close the ticket" resolution — no more "please confirm in Bitbucket" hedging.
- Live PR evidence — same URL scan, but for
/pull-requests/<n>//pull/<n>links. Fetches title + state + author + updated-at. MERGED PRs are flagged as a "possible stale-ticket signal" so the LLM considers whether the fix already shipped but the ticket wasn't closed. - Live Jenkins evidence (Jenkins API) — for each Jenkins job URL
in the ticket that matches your configured
JENKINS_BASE_URLhost:
GET <base>/<job-path>/api/json→ existence,buildableflag,lastCompletedBuild.- Follow-up
GET .../<n>/api/json→ last buildresultand timestamp. The prompt gets EXISTS · last build #42 → SUCCESS, EXISTS · DISABLED, or NOT FOUND (404) — job is not present at that URL. AFAILURE/ABORTED/UNSTABLElast build nudges towardREAL; a long streak ofSUCCESSon the referenced job nudges towardLIKELY_INVALID.
- Ticket content — description + latest ~8 comments. The description is treated as background rather than authoritative (often stale).
Anything watcher couldn't fetch — because the integration isn't configured, the host doesn't match, or the API returned an unexpected error — is passed through as NO SIGNAL and the LLM is told not to use it as evidence either way.
What the LLM returns
A verdict + reasoning + resolutions block per ticket. The verdict itself is a clickable OSC 8 hyperlink in modern terminals — click it (or ⌘-click on macOS, Ctrl-click on Linux) to open the JIRA ticket in your browser.
──────────── PROJ-123 · Checkout 500s in prod ────────────
Verdict: LIKELY_REAL https://jira.example.com/browse/PROJ-123
╭─────────────────────────────────────────────────────────╮
│ **Verdict:** LIKELY_REAL │
│ │
│ **Reasoning** │
│ Live probe shows `orders-api` has 2/3 ready with 4 │
│ restarts and a recent `BackOff` Warning event (×17, │
│ last 12min ago). PR #42 is OPEN — fix not yet shipped. │
│ │
│ **Possible resolutions** │
│ - Merge orders-api#42 and roll a redeploy. │
│ - If rollback is safer, revert commit abc1234. │
│ - Ping @bob (last reviewer) — flagged this in PR #42. │
╰─────────────────────────────────────────────────────────╯
Verdicts are colored in the terminal:
| Verdict | Meaning |
|---|---|
| REAL (red) | Live evidence + comments strongly support an active defect. |
| LIKELY_REAL (yellow) | Probably an active defect; some ambiguity remains. |
| NEEDS_INFO (cyan) | Not enough info in the ticket or live signals to decide. |
| LIKELY_INVALID (magenta) | Likely stale ticket (fix already shipped, workload healthy, etc.). |
| INVALID (green) | Clearly not a defect based on the evidence. |
The full LLM response is rendered as a bordered markdown panel below the verdict line.
Cost & limits
Each ticket is one LLM call — the prompt is per-ticket rather than
batched so a failure doesn't wipe out the rest of the run. The default
--limit 3 keeps ad-hoc invocations cheap; raise it deliberately when
doing a bulk triage sweep. When you pass --keys, --limit is ignored
— explicit key lists always run in full.
How watcher works
JIRA site Watcher Your local CLI
───────────── ───────────── ─────────────────
┌───────────┐
/_edge/tenant_info ◀───│ discover │ claude / codex /
│ cloudId │ cursor-agent
└─────┬─────┘ ▲
▼ │
/rest/api/…/search ◀───┤ fetch │ │
└─────┬─────┘ │
▼ │
┌───────────┐ compact prompt │
│ diff vs │ ────────────────▶ │
│ memory │ │
└─────┬─────┘ │
▼ markdown │
┌───────────┐ + category tags │
│ parse & │ ◀──────────────────┘
│ render │
└───────────┘
│
▼
memory/memory.json (from source)
~/.watcher/memory.json (from PyPI)
Step by step:
- Discover. For Cloud tenants, watcher hits
<base>/_edge/tenant_infoto pick up thecloudIdand rewrites the API base to Atlassian's platform gateway. For Server/DC, it uses your site URL directly. - Fetch.
jira_client.pycalls the search endpoint (v3/search/jqlfor Cloud, v2/searchfor Server), pages through all results, and flattens ADF descriptions to plain text. - Diff against memory.
memory.pyloads the memory file — a per-ticket record of the last-seenupdatedtimestamp and cached category. When installed from source it lives at<project-root>/memory/memory.json; on a plain PyPI install it falls back to~/.watcher/memory.json. Override withWATCHER_MEMORY_PATH. Tickets whose signature is unchanged are shipped compactly (no description, cached category attached); new or changed tickets are shipped with description + latest comment for re-tagging. - Prompt the LLM.
providers.pypipes the prompt to the selected CLI over stdin. The prompt asks for a markdown digest followed by a===CATEGORIES===block withKEY: categorylines. - Parse. The digest is extracted and rendered with
rich; the tag block updates memory for the next run. - Render. You see the digest and a category count table for the current run.
Resolve stage flow
When you accept the "Investigate any tickets?" prompt (or invoke
--resolve), the flow above is followed by a per-ticket enrichment
pass:
Ticket keys Watcher Evidence sources
───────────── ────────────── ────────────────────────
┌──────────────┐
GET /issue ◀── │ enrich per │
GET remotelinks│ ticket │
└──────┬───────┘
▼
┌──────────────┐ Bitbucket / GitHub API ─── Repo & PR
│ extract URLs │ ─▶ /repositories/<w>/<r> evidence
│ + service │ /pulls/<n> (EXISTS
│ candidates │ / 404)
└──────┬───────┘
▼
┌──────────────┐ Jenkins API ─── Jenkins
│ probe │ ─▶ /<job>/api/json evidence
│ integrations │ /<job>/<n>/api/json (last build
│ │ status)
└──────┬───────┘
▼
┌──────────────┐ kubectl (read-only) ─── Workload
│ pick kube- │ ─▶ get deployment / pods / evidence
│ context │ events
│ (once/sess.) │
└──────┬───────┘
▼
┌──────────────┐ Coding CLI ─── Verdict +
│ prompt LLM, │ ─▶ (Claude / Codex / reasoning +
│ per ticket │ Cursor) resolutions
└──────────────┘
Capabilities
| 🔍 JQL-driven queue view | Default to your assigned open tickets; override with any JQL. |
| 🧠 LLM-authored triage digest | Category counts, urgent-item callouts, grouped bullets, focus recommendation. |
| 🏷️ Persistent categorization | 14 generic buckets remembered per ticket; watch your backlog shape evolve. |
| 👁️ Watch mode | Polls JIRA; only invokes the LLM when a ticket signature changes. |
| ⚡ Token-optimized prompts | Short field names, days-ago dates, cached categories skipped from the payload. |
| 🔌 Bring-your-own coding CLI | claude, codex, or cursor-agent — no separate API keys. |
| 🛡️ Diagnostics-first | --whoami, --debug, and auth-failure hints that explain what actually broke. |
| 🧩 Cloud + Server support | Handles Atlassian gateway auth, vanity domains, and Server/DC Bearer PATs. |
| 🔌 Adjacent-system connectivity | One --check command verifies Bitbucket, Jenkins, GitHub, and EKS in one pass. |
| 🕵️ Resolve stage | Deep-investigate a ticket with full context + linked PRs; get a verdict and fixes. |
| 📡 Live workload evidence | Reads deployment / pod / event state via kubectl get so the verdict reflects reality. |
| 🗂️ Live repo evidence | Probes Bitbucket / GitHub for repos named in the ticket — EXISTS vs 404 goes straight to the verdict. |
| 🏗️ Live Jenkins evidence | Fetches job existence + last build result for Jenkins URLs in the ticket. |
| 🔒 Read-only investigation | Every kubectl call is guarded — no writes, no apply, no kubeconfig mutation. |
| 🎯 Interactive kube-context picker | Lists your contexts, asks which one to use for probes, and never overwrites your use-context. |
| 🔗 Clickable verdict | Verdict labels are OSC 8 hyperlinks — click to open the JIRA ticket. |
Supported providers
| Provider | CLI binary | Non-interactive invocation | Auth |
|---|---|---|---|
| Claude Code | claude |
claude -p --output-format text |
Your Claude Code login |
| OpenAI Codex | codex |
codex exec - |
Your Codex CLI login |
| Cursor Agent | cursor-agent |
cursor-agent -p |
Your Cursor login |
Whichever you pick must already be installed and authenticated on your machine. Pipe the prompt over stdin; watcher captures stdout. Missing binary → a clear error with an install hint.
Memory & categorization
Every ticket the LLM sees is tagged with one of these generic buckets:
bug · feature · enhancement · refactor · infra · docs · testing
performance · security · dependency · data · ux · support · other
Assignments are persisted next to your install:
- From source (
pip install -e .) →<project-root>/memory/memory.json. Already listed in.gitignore; nothing to worry about. - From PyPI →
~/.watcher/memory.json. Watcher won't write intosite-packages— it would get clobbered on the nextpip install --upgrade.
Override either default with WATCHER_MEMORY_PATH in .env. On
subsequent runs:
- Tickets whose
(key, updated)signature is unchanged ride along without description or comments, tagged with the cached category — no re-categorization needed. - Only new or changed tickets are shipped with description + latest comment and asked to be tagged.
Over time this gives you an honest picture of what your work actually consists of. Handy for retrospectives, capacity planning, or noticing you've become the person who quietly handles every dependency upgrade.
Wipe the memory anytime with:
watcher --forget
Token optimization
The prompt sent to the LLM is aggressively slimmed:
- Short JSON keys (
k,t,s,p,u,c,d,lc) instead of verbose field names. - Timestamps collapsed to
days-agointegers. - Summaries clipped to 200 chars, descriptions to 300, single latest comment to 200.
- Cached-category tickets ship without body; only new/changed tickets carry the description + latest comment.
- Watch mode skips the LLM entirely when nothing has changed since the previous poll.
Net effect: quiet days send only a compact key/status/priority list.
Troubleshooting
Run any command with --debug to see actual URLs, HTTP status codes, and
pagination state. Start with watcher --whoami — it isolates the auth
question from the search question.
401 "Client must be authenticated"
- Cloud, custom vanity domain (e.g.
prod.jira.yourcompany.com): the scopedATATT…token only authenticates against the platform gateway. Watcher auto-discovers thecloudIdfrom<base>/_edge/tenant_info; if that discovery is blocked, setJIRA_CLOUD_ID=<uuid>in.env. - Cloud, plain email/token: double-check that
JIRA_EMAILis the login email of the Atlassian account that created the token, and that the token itself was pasted with no whitespace or quotes. - Server / DC: make sure the token is a Personal Access Token from your JIRA profile — not your login password.
403 "Failed to parse Connect Session Auth Token"
Cloud-only error meaning the token was sent to the site URL instead of the
platform gateway. Confirm JIRA_FLAVOR=cloud and that watcher's --debug
output shows cloud gateway resolved: https://api.atlassian.com/ex/jira/….
If not, discovery failed — set JIRA_CLOUD_ID manually.
"No tickets currently assigned to you" — but you have tickets
- Some JIRA setups don't populate the
resolutionfield on open tickets. Watcher's default JQL usesstatusCategory != Donewhich is more portable, but you can override with--jql "assignee = currentUser()"to see everything, resolved or not. - Confirm
watcher --whoamishows the account you expect. If a different account,assignee = currentUser()is resolving to someone else.
LLM CLI errors
If the selected CLI isn't on PATH, watcher prints an install hint. Point
at a specific binary with --cli-path or LLM_CLI_PATH. If the CLI runs
but returns garbled output, add --no-llm to fall back to the rule-based
digest while you investigate.
Corporate WAF / User-Agent blocks
Some corporate proxies and Cloudflare fronts block the default
python-requests User-Agent. Watcher already sends a browser-like UA; if
your setup needs a specific string, set JIRA_USER_AGENT in .env.
EKS probes return "not found" for every candidate
Watcher extracts service names from JIRA components and labels, then
runs kubectl get deployment <candidate> -n <ns> against the context you
picked. Common causes when everything comes back not-found:
- Deployment lives in a different namespace — set
EKS_NAMESPACEin.envor pass a JIRA component name that matches the actual deployment. - Wrong kubectl context selected — re-run with a different context via
--kube-context NAME, or setKUBE_CONTEXTin.env. - JIRA Components field is empty and labels don't match k8s naming conventions — the extractor requires lowercase-alphanumeric-hyphen names.
Watcher never treats "not found" as evidence of a working service — it's recorded as no signal, and the LLM is instructed to ignore it.
"Skipping EKS probes" appears even though EKS is configured
Investigation runs read-only kubectl calls and needs a context. It skips probes when:
- No kubectl contexts exist (
kubectl config get-contextsis empty). - Running non-interactively without a current context.
- You typed
skipat the picker.
To force a specific context in scripts, use
watcher --resolve --keys PROJ-1 --kube-context <name> or set
KUBE_CONTEXT in .env.
Project layout
watcher/
├── cli.py # argparse entry point + one-shot / watch loops
├── config.py # loads and validates .env
├── jira_client.py # Cloud + Server API client with cloudId discovery
├── memory.py # persistent ticket→category memory
├── providers.py # wraps claude / codex / cursor-agent CLIs
├── summarizer.py # compact prompt builder + response parser
├── investigator.py # resolve: enrich, extract PRs, gather live evidence, prompt
└── integrations/
├── _base.py # IntegrationStatus + macOS keychain fallback
├── bitbucket.py # Cloud (Basic email:token) + DC (Bearer PAT)
├── jenkins.py # Basic username:api_token + CSRF crumb helper
├── github.py # gh CLI when available, else PAT via REST
└── eks.py # aws sts + describe-cluster; read-only kubectl probes
# (get deployment/pods/events, kube-context picker)
When does watcher connect to what?
The digest only talks to JIRA. Every other integration is contacted
lazily, on-demand, during --resolve (investigation):
| Integration | When it's called | What triggers a call |
|---|---|---|
| JIRA | Every run | Search + per-ticket get_issue / remote-links during resolve. |
| Bitbucket | Resolve only | A bitbucket.org/<ws>/<repo>[/…] or DC repo URL in description, comments, or JIRA remote links. |
| GitHub | Resolve only | A github.com/<owner>/<repo>[/…] URL in the ticket. |
| Jenkins | Resolve only | A /job/… URL whose host matches JENKINS_BASE_URL. |
EKS (kubectl) |
Resolve only | A JIRA components or labels entry that looks like a valid Kubernetes name. |
If an integration isn't configured in .env, watcher simply skips its
probes — no warnings, no failures. If a probe fails (host mismatch,
network error, 404), the result is passed through to the LLM as no
signal rather than as evidence.
License
Released under the MIT License — see LICENSE for the full text.
In plain English:
- ✅ Free for personal and commercial use.
- ✅ Fork, modify, and redistribute.
- ✅ Bundle it inside larger closed-source products.
- ✅ Sell products built on top of it.
- ⚠️ Ship a copy of the license and the copyright notice along with any redistribution.
- ❌ No warranty, no liability. Use at your own risk.
MIT is the most permissive of the widely-used open-source licenses and what tools like jq, curl, Rich, and FastAPI ship under. The copyright stays with the author; anyone else is free to use it.
Built for engineers who'd rather stare at a terminal than a JIRA board.
Metadata
Release files for jira-watcher-agent 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| jira_watcher_agent-0.1.0.tar.gz | 78.5 kB | With provenance | Details for jira_watcher_agent-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| jira_watcher_agent-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for jira_watcher_agent-0.1.0-py3-none-any.whl |
Total release size: 135.2 kB
Release files / jira_watcher_agent-0.1.0.tar.gz
| Download URL | jira_watcher_agent-0.1.0.tar.gz |
|---|---|
| Size | 78.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d8a94a9487403ac6a336e04a4496589ae208076a9ba8b328597bdcee3621952c
|
|
BLAKE2b-256 checksum How to use checksums |
c1f382b8dc24f1d1fdc93b6ac52637a4fe98bb45df5e1e775d0449098647b419
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logRelease files / jira_watcher_agent-0.1.0-py3-none-any.whl
| Download URL | jira_watcher_agent-0.1.0-py3-none-any.whl |
|---|---|
| Size | 56.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
97685a6b5e188dd2895b7cff3645f2ede81e819d41582fca9c3262ec5580e129
|
|
BLAKE2b-256 checksum How to use checksums |
b4f77c2c68d70218ad85fc954c3d793fd0df23a10acaaeb9855023995a2725bd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logSecurity for jira-watcher-agent 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for jira_watcher_agent-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.