Metadata-Version: 2.4
Name: jira-claude-toolkit
Version: 1.1.0
Summary: Reusable Jira → Claude Code → Spec-Kit specification toolkit
Author: jira-claude-toolkit contributors
License: MIT
Project-URL: Documentation, https://github.com/your-org/jira-claude-toolkit#readme
Project-URL: Source, https://github.com/your-org/jira-claude-toolkit
Keywords: jira,claude,spec-kit,specification,developer-tools
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Documentation
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: PyYAML>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"

# jira-claude-toolkit

Turn a Jira issue into a Spec-Kit technical specification, from inside Claude
Code or from a plain shell.

Install it once, use it in every repository. The toolkit discovers each
repository's own Spec-Kit conventions instead of imposing its own.

```
Jira  →  Jira Client  →  Normalized Issue Context  →  Claude Code
      →  Grill / Clarification  →  Decision  →  Spec-Kit  →  Technical Specification
                                      │
                    ┌─────────────────┼──────────────┬──────────────────┐
                    ▼                 ▼              ▼                  ▼
              CREATE_SPEC       UPDATE_SPEC       NO_OP        NEEDS_CLARIFICATION
                    └─────────────────┘                              / BLOCKED
                        └──► spec.md                                 (no spec)
```

## Quick start

```bash
pipx install jira-claude-toolkit          # or: pip install jira-claude-toolkit

cd ~/code/your-project
jira-claude install --config-files        # writes skills + starter config
$EDITOR .jira-claude.yaml                 # base_url, project_key
cp .env.example .env && $EDITOR .env      # JIRA_EMAIL, JIRA_API_TOKEN
jira-claude doctor                        # verify everything
```

Then in Claude Code — find something to work on:

```
/jira-ticket --mine --status "In Progress"
```

and turn one of them into a specification:

```
/jira-speckit PROJ-25
```

That second command fetches the ticket, investigates your codebase, grills the
requirements, decides whether a specification is actually needed, and — if it is
— drives **your repository's own** Spec-Kit specify command. It produces a
specification and nothing else; it never implements the feature.

The two are deliberately separate. Finding a ticket is not consent to specify
it, so `/jira-ticket` ends at a key and never runs `/jira-speckit` for you.

## Commands

```bash
jira-claude doctor            # every check: config, Jira, Spec-Kit, skills, secrets
jira-claude validate-env      # configuration only, no network
jira-claude connect           # authenticate and confirm project access
jira-claude ticket            # list issues in the configured project (bounded)
jira-claude ticket PROJ-25    # normalized issue context (Markdown or --json)
jira-claude grill PROJ-25     # clarification questions derived from the ticket
jira-claude decide PROJ-25    # CREATE_SPEC / UPDATE_SPEC / NO_OP / NEEDS_CLARIFICATION / BLOCKED
jira-claude smoke-test PROJ-25  # read-only checks against a REAL Jira site
jira-claude install           # write the Claude Code skills into a repository
jira-claude scan-secrets      # fail if a credential reached a committable file
```

`stdout` is data, `stderr` is diagnostics, exit codes are stable:

```bash
jira-claude ticket PROJ-25 --json | jq '.acceptance_criteria'
```

| Code | Meaning | Code | Meaning |
|------|---------|------|---------|
| 0 | ok | 7 | not found |
| 2 | usage | 8 | wrong project |
| 3 | missing dependency | 9 | malformed response |
| 4 | config invalid | 11 | unexpected HTTP status |
| 5 | Jira unreachable | 12 | Spec-Kit missing |
| 6 | auth failed | 14 | secret found |

## Claude Code commands

| Command | Purpose |
|---------|---------|
| `/jira-ticket` | Find a ticket: list, filter, search, or inspect one |
| `/jira-speckit <TICKET_ID>` | Jira ticket → codebase investigation → grilling → decision → specification |
| `/jira-connect` | Diagnostics, when something failed |

> **Status: `1.1.0`.** Verified against a mock Jira (892 tests), a CI matrix
> spanning Python 3.9–3.13 on Ubuntu and Windows, and a live Atlassian site over
> REST v2. See [docs/testing.md](docs/testing.md) and
> [CHANGELOG.md](CHANGELOG.md) for what live data did and did not exercise —
> including the checks a three-issue project could not.

### Finding a ticket

```
/jira-ticket                              # bounded list, most recently updated first
/jira-ticket PROJ-25                      # one issue in detail
/jira-ticket --status "In Progress"
/jira-ticket --mine
/jira-ticket --priority High
/jira-ticket --type Bug
/jira-ticket --search "payment"
/jira-ticket --limit 25
/jira-ticket --jql 'status = "In Progress"'
/jira-ticket --mine --json                # machine-readable
```

Filters combine, so `--mine --status "In Progress" --priority High` narrows on
all three.

**Results are bounded, and the toolkit never invents a total.** Jira's search
endpoint returns issues, not a count of everything that matched — so the output
distinguishes the two cases it can actually tell apart:

```
Total: 3 issues              # paging finished; this is every match
```

```
Showing: 25 issues           # the limit was reached first
Result set: bounded by --limit 25
More issues may exist.
```

Obtaining a true project-wide total would cost a second request nobody asked
for, so `Total:` appears **only** when paging exhausted the result set. The
status counts below the table describe the returned issues for the same reason,
and say so.

Listing is cheap by design: one request per page, never one per issue, and it
never asks Jira for descriptions or comments — so a 50-issue list does not drag
50 descriptions into the conversation.

`/jira-speckit` **does not require** `/jira-connect` to have been run. It
validates the connection itself and points at `/jira-connect` only if something
breaks. There is no connection-state file: every Jira call authenticates, so a
cached "connected" flag could only ever produce a misleading error message.

## A Jira ticket does not imply a specification

The workflow ends in an explicit, reportable decision — not in a document by
default:

| Outcome | Meaning |
|---------|---------|
| `CREATE_SPEC` | Nothing covers this ticket; write a specification |
| `UPDATE_SPEC` | One exists but is unfinished; extend it, do not fork it |
| `NO_OP` | An existing specification already covers the request |
| `NEEDS_CLARIFICATION` | Blocking questions must be answered first |
| `BLOCKED` | Something outside the ticket prevents progress |

```
$ jira-claude decide PROJ-25
Jira Issue: PROJ-25
Repository Investigation: COMPLETE
Existing Specification: FOUND
Grill: RESOLVED

Decision: NO_OP

Reason:
Existing specification completely covers this Jira request.
```

`jira-claude decide` computes a **mechanical proposal** from files on disk and
ticket fields — it searches the specs directory for a specification citing this
Jira key. `/jira-speckit` then confirms or overrides it after reading the
codebase, which is the one part of the pipeline that needs judgement.

Concluding `NO_OP` is a success. A pipeline that always emits a document emits
duplicates.

**Exit code 0 means a decision was computed, not that the answer was "yes".**
Branch on the outcome, never on the exit status:

```bash
outcome=$(jira-claude decide PROJ-25 --json | jq -r .outcome)
case "$outcome" in
  CREATE_SPEC|UPDATE_SPEC) run-spec-kit ;;
  NO_OP)                   echo "already specified — nothing to do" ;;
  NEEDS_CLARIFICATION)     echo "needs answers first" ;;
  BLOCKED)                 exit 1 ;;
esac
```

Failures that *prevent* a decision — invalid key (2), config (4), auth (6), not
found (7), wrong project (8) — still use their own exit codes.

## Configuration: two files, one rule each

```
.env                  credentials ONLY — JIRA_EMAIL, JIRA_API_TOKEN. Git-ignored.
.jira-claude.yaml     everything else — base URL, project key, limits. Committed.
```

Putting a credential in the YAML file is a hard error, not a warning: that file
is meant to be committed, so accepting a token there would create the exact leak
the split exists to prevent.

Precedence: process environment → `.env` → `.jira-claude.yaml` → defaults. The
environment wins so CI needs no files on disk.

See [docs/configuration.md](docs/configuration.md).

## How it stays repo-agnostic

The skills contain no Jira logic, no credentials, and no paths. Before doing
anything, `/jira-speckit` runs:

```bash
jira-claude doctor --json --offline
```

and reads this repository's actual conventions out of the result — the specs
directory, feature numbering (`003-name` vs `20260319-143022-name`), and the
real specify command (`/speckit-specify` or `/speckit.specify`). Nothing is
assumed, so the same two files work in Engineering OS, Product Intelligence,
TokenHelm and repositories that do not exist yet.

Override discovery per project in `.jira-claude.yaml` when a repository does
something unusual.

## Architecture

Layering is enforced by import direction, not convention:

| Module | Knows about | Knows nothing about |
|--------|-------------|---------------------|
| `client` | Jira HTTP | Claude Code, Spec-Kit, YAML, the CLI |
| `issue` | Jira's data shape | HTTP |
| `grill` | normalized issues | Jira |
| `speckit` | the host repository | Jira |
| `config` | files | HTTP |
| `installer` | Claude Code | Jira |
| `decision` | specs on disk | Jira, HTTP |
| `smoke` | real-site verification | Spec-Kit, the CLI |
| `cli` | all of the above | — |

So the Jira client is usable on its own:

```python
from jira_claude.client import JiraClient
from jira_claude.issue import normalize
from jira_claude.secrets import Secret

client = JiraClient("https://acme.atlassian.net", "dev@acme.io", Secret(token))
issue = normalize(client.issue("PROJ-25"))
print(issue.acceptance_criteria)
```

…and the Claude Code integration is usable against any repository.

See [docs/architecture.md](docs/architecture.md).

## Requirements

Python 3.9+. One runtime dependency: PyYAML. HTTP is stdlib `urllib`.

## Tests

```bash
pip install -e ".[dev]"
pytest                       # 389 tests — mock only, no credentials, no network
pytest -m "not integration"  # unit tests only
pytest --cov=jira_claude     # coverage
```

Real-Jira tests are a **separate layer** and are deselected by default. They
need `JIRA_REAL_TEST=1` plus real credentials plus `-m realjira`; any gate closed
means skipped, never failed. They are read-only.

```bash
jira-claude smoke-test PROJ-25   # the same checks, without pytest
```

See [docs/testing.md](docs/testing.md).

## Security

The API token is wrapped in a `Secret` that cannot be printed by accident —
`str()`, `repr()`, f-strings and `format()` all yield `***`, and pickling
raises. It travels in an HTTP header, never in a URL, a process argument, or a
file. Redirects are refused so it is never forwarded to another host.

```bash
jira-claude scan-secrets     # pre-commit hook material
```

See [docs/security.md](docs/security.md).

## Documentation

- [Installation](docs/installation.md) — pip, pipx, local, skill install
- [Configuration](docs/configuration.md) — project config and secrets
- [Architecture](docs/architecture.md) — the seven components and the layering
- [Testing](docs/testing.md) — mock testing vs real-Jira smoke testing
- [Security](docs/security.md) — credential containment
- [End-to-end example](docs/example-end-to-end.md) — a real run, verbatim
- [Changelog](CHANGELOG.md) — release history and the path to 1.0.0

## Licence

MIT.
