PRD Ticket Coverage Audit: $ARGUMENTS
$ARGUMENTS is one of:
- A PRD URL alone — auto-discover created tickets via the PRD's epic remote link.
- A PRD URL plus an explicit list of ticket keys —
<PRD URL> tickets=[KEY-1,KEY-2,...]. Use this when called fromlisa-notion-prd-intakeorlisa-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 withmcp__claude_ai_Notion__notion-fetch(include_discussions: true) andmcp__claude_ai_Notion__notion-get-comments.- Atlassian Confluence host (e.g.
*.atlassian.net/wiki/...) → Confluence. Throughlisa-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.apphost → Linear. Fetch withlisa-linear-access operation: get-project(capture description, labels, state, attached resources),lisa-linear-access operation: list-documents({projectId})+lisa-linear-access operation: get-documentper attached document,lisa-linear-access operation: list-issues({project})for sub-issues that act as child epics / user stories, andlisa-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.comhost → GitHub Issues. Fetch with theghCLI (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,urlfor the PRD body and comments.gh api graphqlwith thesubIssuesfield for native sub-issue children (these stand in for child epic pages — seelisa-github-read-issuePhase 3 for the exact query).gh issue view <child-num> --repo <org>/<repo> --json body,commentsper 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
- Parse
$ARGUMENTS:- PRD URL → detect vendor from host, extract page ID.
- Optional
tickets=[...]→ list of explicit ticket keys.
- Fetch the PRD using the vendor-appropriate tool surface:
- Notion:
notion-fetchwithinclude_discussions: true. Capture: title, body, child Epic pages, all comment threads. - Confluence:
getConfluencePage(capture title, body, labels),getConfluencePageFooterComments+getConfluencePageInlineComments(capture all comments; walk replies viagetConfluenceCommentChildrenfor any thread with children). - Linear:
get_project(capture name, description, labels, state, attached resources). Capture sub-issues vialist_issues({project})and per-issue comments vialist_comments({issueId}). - GitHub:
gh issue view --jsonfor the source PRD issue (title, body, labels, comments). Capture sub-issues via the GraphQLsubIssuestraversal.
- Notion:
- If the PRD has child Epic sub-pages / sub-issues (a multi-epic PRD), fetch each in parallel:
- Notion:
notion-fetchper child page withinclude_discussions: true. - Confluence: enumerate descendants via
getConfluencePageDescendants, thengetConfluencePageper child plus its comment streams. - Linear: enumerate attached documents via
list_documents({projectId})and fetch each viaget_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(subIssuesfield), thengh issue view <child-num> --json body,commentsper child. Recurse to depth 3. The audit walks the full PRD tree.
- Notion:
- 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 every vendor, this comment lives on the PRD page / project / issue itself). - Searching the destination tracker via
lisa-tracker-read(or directly viasearchJiraIssuesUsingJql/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_FOUNDwith a clear remediation — coverage cannot be assessed without the ticket set.
- Looking for a destination URL in the PRD body, comments, or the PRD's most recent "Ticketed by Claude" comment posted by
- 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 graphqlsubIssuestraversal of the Epic, recursively for sub-tasks.
- JIRA destination: JQL
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:
PRD item id → [ticket keys that cover it]
Matching rules (in priority order):
- Declared trace (deterministic — preferred): tickets created after
requirement traceability landed carry a
Source Requirementsection (verbatim requirement quotes + registerR-ids), and the PRD's## Ticketssection carries the same ids in itslisa: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 theR-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). - Direct quote / strong keyword overlap: the ticket's summary or AC explicitly names the PRD item's keywords. High confidence.
- 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. - 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.
- 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.
## 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 as a project-level comment); 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
ambiguousand 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_FOUNDverdict 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 fromTicketedtoBlocked.