---
name: sprint-report
description: Generate a sprint health report by cross-referencing Jira tickets with code delivery status across all repos
---

# /sprint-report — Sprint Health Report

Pull every ticket from a Jira sprint, cross-reference with code delivery across all 6 repos, produce a markdown report.

## Data Integrity (MANDATORY — read before anything else)

1. **Always live.** Every invocation queries Jira (REST API or MCP) and GitHub (`gh`) in real time. There is no cache.
2. **Never read old files.** The `reports/` directory contains historical exports. NEVER read, cite, summarize, or reuse those files. They do not exist for this skill's purposes.
3. **Never fabricate data.** Every number, ticket key, PR URL, and status in the report must trace to an API response from this invocation. If a data source fails, report the error — do not fill in blanks.
4. **Fail loud.** If Jira access (API or MCP) or `gh` CLI is unavailable, STOP immediately and tell the user what's broken. Do not attempt to produce a partial report from memory or files.
5. **Timestamp check.** The "Generated" field must equal `now()`. If the report you're about to output has a stale timestamp, something went wrong — regenerate.

## Prerequisites

- `gh` CLI authenticated (`gh auth login`)
- `JIRA_EMAIL` + `JIRA_API_TOKEN` env vars set (primary Jira access)
- Atlassian MCP configured in Cursor (fallback for Jira, required for Confluence)

## Invocation

```
/sprint-report <team>
/sprint-report <team> --sprint "Sprint 45"
```

- `<team>` — Jira project key (`SQRN`) or alias from `config/teams.json` (`rhino`, `squad-rhino`).
- `--sprint` — Sprint name. Defaults to the active sprint.

## Domain Context

Read `config/sprint-report-context.json` once at the start of every invocation. It contains:
- Custom field IDs and their meanings
- Status-to-category mapping
- Issue hierarchy rules (parent vs Dev Task subtasks)
- Rollup rules for assignees, dev info, and platforms
- Fields to request from Jira

This avoids re-discovering custom fields and hierarchy rules every run.

## Workflow

### 0. Preflight (MUST run first — fail fast)

Before any data fetching, verify dependencies **in a single parallel batch** (and read team + context in the same batch):

1. **GitHub access:** `gh api repos/HumandDev/humand-main-api --jq '.full_name'`
2. **Jira API creds:** check that `JIRA_EMAIL` and `JIRA_API_TOKEN` env vars are set (or their aliases: `ATLASSIAN_EMAIL`, `ATLASSIAN_API_TOKEN`, etc.)
3. **Read `config/teams.json`** (local, no network)
4. **Read `config/sprint-report-context.json`** (local, no network)

Run all four simultaneously. If **either** external check fails, STOP immediately and report the broken dependency. Do NOT proceed — the whole point is to avoid spending minutes on round-trips only to discover `gh` or Jira creds are broken.

**If Jira API creds are missing**, fall back to Atlassian MCP: call `getAccessibleAtlassianResources()` → extract `cloudId`. If MCP also fails, STOP.

### 1. Resolve Team

Already loaded in step 0. Match `<team>`:

1. Top-level key in `teams.json` → use its `project_key`.
2. Matches any entry's `aliases` (case-insensitive) → use that entry's `project_key`.
3. Looks like a Jira key (uppercase, 2-6 chars) but isn't in the file → use directly.
4. Otherwise → ask: "I don't recognize team `<team>`. What's the Jira project key?"

### 2. Fetch Sprint Tickets from Jira

#### Primary: REST API via script (fast, handles pagination)

```bash
scripts/fetch-jira-sprint-issues.sh --project <KEY> > /tmp/<KEY>-raw.json
```

For a named sprint:
```bash
scripts/fetch-jira-sprint-issues.sh --project <KEY> --sprint "<Name>" > /tmp/<KEY>-raw.json
```

The script uses `JIRA_EMAIL` + `JIRA_API_TOKEN`, requests all fields from the context file (including `parent` and `customfield_10020`), and outputs a JSON object with an `issues` array.

#### Fallback: Atlassian MCP (only if API creds are unavailable)

```
mcp: searchJiraIssuesUsingJql(
  cloudId,
  jql: "sprint in openSprints() AND project = <KEY> ORDER BY status ASC, priority DESC",
  fields: ["summary","issuetype","status","priority","assignee","parent","customfield_10020","customfield_10021","customfield_10028","customfield_10000","customfield_10097"],
  maxResults: 100
)
```

If `total > maxResults`, fetch additional pages. Save all raw JSON response files.

**CRITICAL: `parent` field.** Requesting `parent` lets subtasks be linked to their parent issue for rollup. Without it, the script falls back to title matching (less reliable).

### 3. Process Data (script pipeline — replaces manual JSON parsing)

**Save** the raw MCP JSON response(s) to temp files, then run:

```bash
python3 scripts/process-sprint-data.py page1.json [page2.json ...] \
  --tickets-out /tmp/<KEY>-tickets.json \
  --sprint-out /tmp/<KEY>-sprint.json
```

The script outputs to **stdout** a compact digest:
1. **SPRINT line** — name, dates (from `customfield_10020`)
2. **TOTAL** — issue counts (parents vs subtasks)
3. **GITHUB_ENRICH** — exact `gh` commands for open-PR branches needing review status
4. **BACKFILL_SUBTASKS** — JQL to fetch missing subtasks (parents with 0 subtasks in sprint data)
5. **Digest table** — one line per parent issue with rolled-up assignees, dev status, repos, platforms

**Read the stdout digest to understand the sprint.** Do NOT try to manually parse the raw MCP JSON.

### 3b. Backfill subtasks (conditional — only if script says so)

If the digest includes a `BACKFILL_SUBTASKS` section with a JQL query:

**Primary (API):** Use curl directly since `fetch-jira-sprint-issues.sh` only supports sprint queries:
```bash
curl -sS -u "${JIRA_EMAIL}:${JIRA_API_TOKEN}" \
  -H "Accept: application/json" \
  --get "https://humand.atlassian.net/rest/api/3/search/jql" \
  --data-urlencode "jql=parent in (KEY-1, KEY-2, ...) AND issuetype = 'Dev Task'" \
  --data-urlencode "maxResults=100" \
  --data-urlencode "fields=summary,issuetype,status,priority,assignee,parent,customfield_10020,customfield_10021,customfield_10028,customfield_10000,customfield_10097" \
  > /tmp/<KEY>-backfill.json
```

**Fallback (MCP):**
```
mcp: searchJiraIssuesUsingJql(
  cloudId,
  jql: "parent in (KEY-1, KEY-2, ...) AND issuetype = 'Dev Task'",
  fields: [...same fields...],
  maxResults: 100
)
```

Save this response and **re-run** the process script with all files:

```bash
python3 scripts/process-sprint-data.py /tmp/<KEY>-raw.json /tmp/<KEY>-backfill.json \
  --tickets-out /tmp/<KEY>-tickets.json \
  --sprint-out /tmp/<KEY>-sprint.json
```

This ensures parents like SQRN-1659 correctly reflect their subtasks' code activity even when subtasks aren't explicitly on the sprint.

### 4. Enrich Open PRs from GitHub (only if needed)

The digest's `GITHUB_ENRICH` section lists exact `gh pr list` commands. Run them **in parallel**:

```bash
gh pr list --repo HumandDev/<repo> \
  --search "head:<branch-name>" \
  --state open --limit 5 \
  --json number,url,isDraft,reviewDecision,statusCheckRollup
```

Classify:
- Approved + checks green → ready to merge
- Changes requested or checks red → needs attention
- Pending review → waiting

If `GITHUB_ENRICH: none needed` → skip this step entirely.

### 5. Fallback GitHub search (only for tickets with no dev info at all)

After rollup, if any parent still has `—:0` dev and no branch, search via:

```bash
echo "KEY-1 KEY-2 KEY-3" | scripts/search-prs-for-keys.sh -
```

Only pass specific keys that lack dev info — never all keys.

### 6. Categorize & Generate the Report

Use the digest table to categorize each parent issue. First match wins:

| Category | Rule |
|----------|------|
| **Blocked** | Jira flagged/impediment set |
| **Shipped** | Status category "Done", OR all PRs merged |
| **In Review** | At least one open non-draft PR |
| **In Progress** | Status category "In Progress", or has branch/draft PR but no open PR |
| **Not Started** | Everything else |

Edge cases:
- PRs merged in some repos, open in others → **In Review**
- "Done" in Jira, no PRs → **Shipped** (non-code task)
- Branch only, no PR → **In Progress**

Output markdown following the template below.

### 7. Create Notion page

Using the **Notion MCP**, create a page with the generated markdown report.

1. Search for an existing page titled `Sprint Report — <KEY>` in the workspace. If found, **update it** instead of creating a new one.
2. If not found, **create a new page**.
3. Page title: `Sprint Report — <KEY> — <Sprint Name> (<YYYY-MM-DD>)`
4. Page body: the full markdown report.
5. Return the Notion page URL to the user.

### 8. Optional: Post to Jira

Triggered by user saying "post to jira" after report is displayed. Add a comment on each ticket with code activity:

```
mcp: addCommentToJiraIssue(cloudId, issueIdOrKey: "<KEY>", body: "...")
```

### 9. Optional: Post to Confluence

Triggered by user saying "post to confluence --space <KEY> --parent <ID>". Create or update a page:

```
mcp: createConfluencePage(cloudId, spaceKey, title: "Sprint Report: <Sprint> — <KEY>", body: "<report>", parentPageId)
```

If page exists, update instead.

---

## Report Template

**This is a FORMAT TEMPLATE with fake data. Do not copy these numbers into a real report. Every value must come from live Jira/GitHub queries.**

```markdown
# Reporte de Sprint: <Nombre del Sprint>

**Proyecto:** <KEY>
**Fechas:** <inicio> — <fin>
**Generado:** <timestamp actual — DEBE ser de esta invocación>

## Salud

| | Cantidad | Puntos |
|---|----------|--------|
| ✅ Entregado | X | Y |
| 👀 En Revisión | X | Y |
| 🔨 En Progreso | X | Y |
| 🚫 Bloqueado | X | Y |
| ⏳ No Iniciado | X | Y |
| **Total** | **X** | **Y** |

**Progreso del sprint: X% del tiempo transcurrido**
**Entrega: X% de tickets entregados (X% por puntos)**

---

## ✅ Entregado

| Ticket | Título | Tipo | Responsable | Código |
|--------|--------|------|-------------|--------|
| SQRN-101 https://humand.atlassian.net/browse/SQRN-101 | Ejemplo: Fix avatar crop | Bug | Ana | Mergeado en api, web |

## 👀 En Revisión

| Ticket | Título | Tipo | Responsable | PRs | Estado de revisión |
|--------|--------|------|-------------|-----|--------------------|
| SQRN-110 https://humand.atlassian.net/browse/SQRN-110 | Ejemplo: Feed redesign | Story | Lucas | web#790 https://github.com/…/pull/790 | web: aprobado ✓ |

## 🔨 En Progreso

| Ticket | Título | Tipo | Responsable | Actividad |
|--------|--------|------|-------------|-----------|
| SQRN-120 https://humand.atlassian.net/browse/SQRN-120 | Ejemplo: Dashboard metrics | Story | Mati | Branch en web, sin PR aún |

## 🚫 Bloqueado

| Ticket | Título | Responsable | Notas |
|--------|--------|-------------|-------|
| SQRN-130 https://humand.atlassian.net/browse/SQRN-130 | Ejemplo: Payment integration | Seba | Flaggeado en Jira |

## ⏳ No Iniciado

| Ticket | Título | Tipo | Responsable |
|--------|--------|------|-------------|
| SQRN-140 https://humand.atlassian.net/browse/SQRN-140 | Ejemplo: Translation fixes | Task | — |

---

## Desglose por Repo

| Repo | Mergeados | PRs Abiertos | Branches WIP |
|------|-----------|--------------|--------------|
| humand-main-api | 0 | 0 | 0 |
| humand-web | 0 | 0 | 0 |
| humand-mobile | 0 | 0 | 0 |
| material-hu | 0 | 0 | 0 |
| humand-backoffice | 0 | 0 | 0 |
| hu-translations | 0 | 0 | 0 |

---

## Observaciones

- Bullet list of data-backed findings: Jira/code discrepancies, assignee gaps, risk assessment, etc.
- Always present, never speculative.

```

## Formatting Rules

- **No markdown links.** Show raw URLs: `SQRN-101 https://humand.atlassian.net/browse/SQRN-101`. For PRs: `web#790 https://github.com/…/pull/790`.
- **Code/Activity/PRs columns** — short summaries: "Merged in api, web" or "Draft web#805 <url>".
- **Sort within categories** by team then priority. Team order: frontend (web, backoffice, material-hu) → backend (api) → mobile → translations.
- Omit Points column if no tickets have story points.
- Omit empty categories.
- Jira URLs: `https://humand.atlassian.net/browse/<KEY>`
- **Observations** section is always present.
- **Sprint elapsed %** = `min(100, round((today - start) / (end - start) * 100))`. Omit entirely if dates unavailable.

## Repos Checked

| Repo | GitHub |
|------|--------|
| humand-main-api | `HumandDev/humand-main-api` |
| material-hu | `HumandDev/material-hu` |
| humand-web | `HumandDev/humand-web` |
| humand-backoffice | `HumandDev/humand-backoffice` |
| humand-mobile | `HumandDev/humand-mobile` |
| hu-translations | `HumandDev/hu-translations` |

## Error Handling

| Condition | Action |
|-----------|--------|
| Jira API creds missing + MCP unavailable | STOP. Suggest setting `JIRA_EMAIL` + `JIRA_API_TOKEN`, or configuring Atlassian MCP. |
| `gh` not authenticated | STOP. Suggest `gh auth login`. |
| No issues in sprint | Report: "No tickets found for `<KEY>`. Check project key and sprint status." |
| No GitHub activity | Valid — report shows Jira-only status. |
| Unknown team | Ask for Jira project key. |
| Sprint name not found | Report error, suggest checking Jira. |
| GitHub rate limit | Warn, output partial results. |

## Reusable Scripts

| Script | Purpose |
|--------|---------|
| `process-sprint-data.py` | **Primary data pipeline.** Merges MCP pages, performs parent/subtask rollup, outputs compact digest + processed tickets JSON. Handles missing `parent` field via title-matching fallback. Identifies subtasks needing backfill. |
| `generate-sprint-report.py` | Jira JSON + PR JSON → categorized markdown report. Also supports `--format csv` and `--format json`. |
| `search-prs-for-keys.sh` | Batch-search PRs across 6 repos for specific ticket keys via `gh`. |
| `fetch-jira-dev-info.sh` | Query Jira dev-status REST API for linked PRs/branches. Requires `JIRA_EMAIL` + `JIRA_API_TOKEN`. |
| `fetch-jira-sprint-issues.sh` | **Primary Jira data source.** Fetches sprint issues via REST API. Requires `JIRA_EMAIL` + `JIRA_API_TOKEN`. |
| `run-sprint-report.py` | End-to-end wrapper: resolves team, fetches Jira, searches PRs, calls `generate-sprint-report.py`. Always live. |
