---
name: lisa-prd-ticket-coverage
description: "Verifies that every requirement in a PRD (Notion, Confluence, Linear, or GitHub Issues) is covered by at least one created destination ticket (JIRA, GitHub Issues, or Linear) — no silent drops. Parses the PRD into atomic items (goals, user stories, functional/non-functional requirements, acceptance criteria, important notes), maps each to the created tickets, and produces a coverage matrix and verdict (COMPLETE / COMPLETE_WITH_SCOPE_CREEP / GAPS_FOUND / NO_TICKETS_FOUND). Used by notion-prd-intake / confluence-prd-intake / linear-prd-intake / github-prd-intake post-write to gate the Ticketed transition; can also be invoked standalone for after-the-fact audits."
allowed-tools: ["Skill", "Bash"]
---

# PRD Ticket Coverage Audit: $ARGUMENTS

`$ARGUMENTS` is one of:

1. A PRD URL alone — auto-discover created tickets via the PRD's epic remote link.
2. A PRD URL plus an explicit list of ticket keys — `<PRD URL> tickets=[KEY-1,KEY-2,...]`. Use this when called from `lisa-notion-prd-intake` or `lisa-confluence-prd-intake` (which know the keys they just created).

The PRD URL can be a **Notion page URL**, a **Confluence page URL**, a **Linear project URL**, or a **GitHub issue URL**. Detect the vendor from the host:

- `notion.so` / `notion.site` → Notion. Fetch with `mcp__claude_ai_Notion__notion-fetch` (`include_discussions: true`) and `mcp__claude_ai_Notion__notion-get-comments`.
- Atlassian Confluence host (e.g. `*.atlassian.net/wiki/...`) → Confluence. Through `lisa-atlassian-access`, fetch the page, its descendants (for child epic pages), and its footer + inline comments including nested replies. State the operation, not a vendor tool name — the access skill owns substrate selection (`integration-access-layer`).
- `linear.app` host → Linear. Fetch with `lisa-linear-access operation: get-project` (capture description, labels, state, attached resources), `lisa-linear-access operation: list-documents({projectId})` + `lisa-linear-access operation: get-document` per attached document, `lisa-linear-access operation: list-issues({project})` for sub-issues that act as child epics / user stories, and `lisa-linear-access operation: list-comments({issueId})` per sub-issue for decisions and engineering notes. Comments do not exist on the project itself in the MCP surface — sub-issue comments are the substitute.
- `github.com` host → GitHub Issues. Fetch with the `gh` CLI (no GitHub MCP — Lisa uses the CLI exclusively for GitHub):
  - `gh issue view <number> --repo <org>/<repo> --json number,title,body,labels,milestone,assignees,author,createdAt,comments,url` for the PRD body and comments.
  - `gh api graphql` with the `subIssues` field for native sub-issue children (these stand in for child epic pages — see `lisa-github-read-issue` Phase 3 for the exact query).
  - `gh issue view <child-num> --repo <org>/<repo> --json body,comments` per child sub-issue, recursively to depth 3.

All four vendors produce the same downstream artifact-extraction and coverage-matrix logic — only the fetch surface differs. The rest of this skill is vendor-agnostic.

Verify that every atomic item in the PRD is covered by at least one of the listed/discovered destination tickets. The output gates whether the PRD's lifecycle should remain at `Ticketed` (Notion `Status = Ticketed`, Confluence / Linear / GitHub `prd-ticketed` label) or revert to `Blocked`.

The destination tickets are read via `lisa-tracker-read` (which dispatches to `lisa-jira-read-ticket` or `lisa-github-read-issue` per `.lisa.config.json` `tracker`) so this skill itself stays agnostic of which tracker hosts the work.

## Why this exists

Per-ticket gates (`lisa-jira-validate-ticket`) prove each created ticket is well-formed in isolation. They do NOT prove the *set* of created tickets is complete relative to the source PRD. Silent drops happen — an agent generates 8 tickets when the PRD called for 9, and nothing notices. This skill is the catch.

## Phases

### Phase 1 — Resolve inputs

1. Parse `$ARGUMENTS`:
   - PRD URL → detect vendor from host, extract page ID.
   - Optional `tickets=[...]` → list of explicit ticket keys.
2. Fetch the PRD using the vendor-appropriate tool surface:
   - **Notion**: `notion-fetch` with `include_discussions: true`. Capture: title, body, child Epic pages, all comment threads.
   - **Confluence**: `getConfluencePage` (capture title, body, labels), `getConfluencePageFooterComments` + `getConfluencePageInlineComments` (capture all comments; walk replies via `getConfluenceCommentChildren` for any thread with children).
   - **Linear**: `get_project` (capture name, description, labels, state, attached resources). Capture sub-issues via `list_issues({project})` and per-issue comments via `list_comments({issueId})`.
   - **GitHub**: `gh issue view --json` for the source PRD issue (title, body, labels, comments). Capture sub-issues via the GraphQL `subIssues` traversal.
3. If the PRD has child Epic sub-pages / sub-issues (a multi-epic PRD), fetch each in parallel:
   - **Notion**: `notion-fetch` per child page with `include_discussions: true`.
   - **Confluence**: enumerate descendants via `getConfluencePageDescendants`, then `getConfluencePage` per child plus its comment streams.
   - **Linear**: enumerate attached documents via `list_documents({projectId})` and fetch each via `get_document`. Treat each document as an extra body source. Sub-issues themselves stand in for "child epic pages" — their descriptions and comments are already captured in step 2.
   - **GitHub**: enumerate native sub-issues via `gh api graphql` (`subIssues` field), then `gh issue view <child-num> --json body,comments` per child. Recurse to depth 3.
   The audit walks the full PRD tree.
4. If `tickets=[...]` not provided, locate the destination Epic by:
   - Looking for a destination URL in the PRD body, comments, or the PRD's most recent "Ticketed by Claude" comment posted by `lisa-notion-prd-intake` / `lisa-confluence-prd-intake` / `lisa-linear-prd-intake` / `lisa-github-prd-intake` (for Linear, this comment lives on the project's sentinel feedback issue; for the others, it lives on the PRD page / issue itself).
   - Searching the destination tracker via `lisa-tracker-read` (or directly via `searchJiraIssuesUsingJql` / `gh issue list --search`) for an epic whose summary or description references the PRD title or project ID.
   - If no epic found, return verdict `NO_TICKETS_FOUND` with a clear remediation — coverage cannot be assessed without the ticket set.
5. Once the epic is known, fetch all child stories and sub-tasks:
   - **JIRA destination**: JQL `"Epic Link" = <EPIC-KEY>` and recursively for sub-tasks.
   - **GitHub destination**: `gh api graphql` `subIssues` traversal of the Epic, recursively for sub-tasks.

### Phase 2 — Extract atomic PRD items

Walk the PRD content and produce a list of **atomic items** — testable, ticketable units of work. Each item gets a stable identifier so the matrix is auditable.

The item types to extract:

| Type | Where it appears in the PRD | Example identifier |
|------|----------------------------|--------------------|
| `goal` | `## Goals` section bullets | `goal:1`, `goal:2`, ... |
| `non-goal` | `## Non-goals` (for scope-creep detection) | `non-goal:1` |
| `user-story` | Per-Epic page, "User Story" sub-headings | `epic-1.story-1.1` |
| `functional-req` | "Functional Requirements" sub-section | `epic-1.story-1.1.fr-1` |
| `non-functional-req` | "Non-functional Requirements" sub-section | `epic-1.story-1.1.nfr-1` |
| `acceptance-criterion` | Inline AC under a user story | `epic-1.story-1.1.ac-1` |
| `important-note` | Bold "Important note:" callouts | `note:1` |
| `mobile-spec` | Mobile-specific behavior callouts | `epic-1.story-1.1.mobile-1` |
| `state` | Empty / error / loading state notes | `epic-2.story-2.1.state:empty` |
| `permission` | Role-scoped permission notes | `epic-2.story-2.1.perm:admin` |
| `decision` | Confirmed decisions in comments (e.g. "Engineering: ...") | `comment:42` |

**Items NOT to extract** (these are not coverage gaps if missing):
- Open Questions / `[Needs validation]` items — these are PRD-side blockers, not ticket scope.
- Original concept thesis or annex/historical content — context, not requirements.
- "Out of scope" items in the PRD — explicitly excluded by product.

For each extracted item, capture: `{ id, type, source (PRD section / line), text (concise summary), keywords (3-5 terms for matching) }`.

### Phase 3 — Map items to tickets

For each created ticket (epic + each story + each sub-task), capture: `{ key, summary, description, acceptance_criteria, scope_signals (keywords from summary + AC) }`.

Build a coverage matrix:

```text
PRD item id  →  [ticket keys that cover it]
```

Matching rules (in priority order):

0. **Declared trace (deterministic — preferred)**: tickets created after
   requirement traceability landed carry a `Source Requirement` section
   (verbatim requirement quotes + register `R-ids`), and the PRD's
   `## Tickets` section carries the same ids in its `lisa:gw … reqs=`
   tokens. When present, the declared trace IS the mapping — match the
   quoted text against the extracted atomic items (match on the verbatim
   quote, not the `R-id`: ids are per-generation and may have shifted if
   the PRD was re-planned; quotes are the durable anchor). A declared
   trace is definitive: do not second-guess it with keyword inference,
   but DO flag a declared quote that no longer appears anywhere in the
   PRD (the requirement was edited or removed — surface it as a drift
   finding, not silently). The derived-work form ("Derived work
   supporting R3, R7") counts as a trace to the named requirements.
   Fall back to rules 1–4 only for tickets with no Source Requirement
   section (created before traceability existed).
1. **Direct quote / strong keyword overlap**: the ticket's summary or AC explicitly names the PRD item's keywords. High confidence.
2. **Domain match**: PRD item describes a UI affordance ("Tasks widget") and a ticket scopes that affordance (`[CU-2.1] Tasks widget — empty state`). Medium-high confidence.
3. **Scope inheritance**: PRD item is a sub-detail of a parent (e.g. an AC under a user story); the ticket covers the parent user story. Medium confidence — flag for review if no more specific ticket exists.
4. **Cross-ticket coverage**: PRD item spans multiple tickets (e.g. a permission rule that applies to several widgets). Each contributing ticket is recorded.

Items with **zero** matching tickets are coverage gaps.

### Phase 4 — Detect scope creep (informational)

For each created ticket, identify any tickets whose scope_signals do NOT trace back to a PRD item, AND are not justifiable as standard infrastructure tasks (e.g. `X.0 Setup` stories for data model / migrations are typically infrastructure scaffolding, not scope creep).

With declared traces this becomes precise: a ticket whose `Source Requirement` section is absent (on a post-traceability ticket set), or whose declared quotes match nothing in the PRD, is an orphan — the strongest scope-creep signal available. A ticket using the derived-work form is by definition not scope creep; it declares which requirements it supports.

Scope creep is informational, not blocking — but worth surfacing because it usually indicates the agent invented work.

### Phase 5 — Determine verdict

| Condition | Verdict |
|-----------|---------|
| All extracted PRD items have ≥1 matching ticket; no scope creep | `COMPLETE` |
| All extracted PRD items have ≥1 matching ticket; one or more scope-creep tickets | `COMPLETE_WITH_SCOPE_CREEP` |
| One or more PRD items have zero matching tickets | `GAPS_FOUND` |
| The created-tickets list is empty or unfetchable | `NO_TICKETS_FOUND` |

`GAPS_FOUND` is the only verdict that should gate the PRD's `Status`. Scope creep is advisory — surface it, but do not block.

### Phase 6 — Emit report

Output a single fenced text block. Callers parse it; do not add free-form prose around it.

```text
## prd-ticket-coverage: <PRD title>

PRD page: <URL>
Tickets audited: <epic-key> + <story-count> stories + <subtask-count> sub-tasks
Atomic PRD items extracted: <n>

### Coverage matrix
| PRD item | Tickets |
|----------|---------|
| <id> (<type>) — <text> | <ticket-key>, <ticket-key> |
| <id> (<type>) — <text> | <ticket-key> |
| <id> (<type>) — <text> | **(none)** |
| ... | ... |

### Gaps  (PRD items with zero ticket coverage — blocks Ticketed status)
- item: <item-id> (<type>)
  text: <text>
  prd_section: "<heading text from the PRD>"
  prd_anchor: "<first ~10 chars>...<last ~10 chars>"   # for selection_with_ellipsis; null if no specific section
  category: <product-clarity|acceptance-criteria|design-ux|scope|dependency|data|technical|structural>
  what: <plain-language description of the gap, no JIRA jargon — written for the product team>
  recommendation: <1–3 candidate resolutions: add a ticket scoped to X / extend ticket Y to cover this / mark out-of-scope explicitly. Never "clarify this".>

### Scope creep  (tickets without PRD trace — informational, does not block)
- <ticket-key> — <summary>
  - *Why flagged:* <reason — e.g. "no matching item in PRD; not an infra task">

### Verdict: COMPLETE | COMPLETE_WITH_SCOPE_CREEP | GAPS_FOUND | NO_TICKETS_FOUND
### Gap count: <n>
### Scope-creep count: <n>
```

`prd_anchor` and `prd_section` are built the same way as in `lisa-notion-to-tracker` / `lisa-confluence-to-tracker` / `lisa-linear-to-tracker` / `lisa-github-to-tracker`. For Notion, `prd_anchor` is the `selection_with_ellipsis` start/end snippet; for Confluence, it's the inline-comment selection text accepted by `createConfluenceInlineComment`; for Linear, it's a sub-issue identifier (e.g. `LIN-123`) when the gap maps to a specific issue, otherwise `null` (the caller posts unanchored Linear gaps on the project's sentinel feedback issue); for GitHub, it's the section heading from the PRD issue body when the gap traces to a specific section, otherwise `null` (the caller approximates inline anchoring by quoting a body excerpt at the top of the comment). The downstream caller knows which vendor it's writing to and uses the right API; this skill just emits the anchor that vendor expects.

`category` is drawn from the same fixed taxonomy used by `lisa-jira-validate-ticket` so downstream callers can apply one consistent comment-formatting policy. Most coverage gaps map to `scope` (item not represented in any ticket) or `product-clarity` (item too vague to map). Use `acceptance-criteria` for missing pass/fail conditions and `design-ux` for missing visuals.

## Rules

- Read-only — never write to JIRA, never write to Notion (callers do that based on the verdict).
- Never silently drop a PRD item from extraction. If an item is ambiguous about whether it's scope, include it in extraction with type `ambiguous` and let the matching phase resolve it. The point of the audit is to catch silent drops; the audit can't have its own.
- Be explicit about confidence in matches — the matrix is for humans to skim; vague matches help no one. If a match is rule-3 ("scope inheritance"), say so.
- Scope creep is INFORMATIONAL. It is normal for an agent to add infra tickets (`X.0 Setup`) the PRD doesn't explicitly enumerate. Only flag scope creep when the ticket genuinely doesn't trace to PRD content AND isn't standard scaffolding.
- The `GAPS_FOUND` verdict is the gate. The caller (e.g. `lisa-notion-prd-intake`, `lisa-confluence-prd-intake`, `lisa-linear-prd-intake`, `lisa-github-prd-intake`) uses it to decide whether to revert the lifecycle from `Ticketed` to `Blocked`.
