Imported from yalesites-org/yalesites-claude-plugins (
plugins/yalesites-product/skills/ticket/SKILL.md). Install upstream withnpx skills add yalesites-org/yalesites-claude-plugins --skill ticket. Copyright stays with the author.
YaleSites Ticket Skill
Overview
This skill helps create well-formed GitHub issues for the YaleSites backlog — both single tickets and multi-ticket epics — and groom existing stub tickets into complete, actionable ones. Every issue should be readable by anyone on the team — content editors, site owners, project managers — regardless of technical background.
Repositories available for developer context, relevant code, and existing patterns:
yalesites-org/yalesites-projectyalesites-org/component-library-twigyalesites-org/atomic
Search these when you need implementation context, but always write the issue description in plain English first. Technical details belong in the acceptance criteria, not the description.
GitHub write-permission scope: The GitHub connector can create and comment on issues in yalesites-org/YaleSites-Internal, but it cannot write comments to yalesites-org/yalesites-project (read-only there — good for searching code and reading PRs, not for posting PR comments). If a task calls for commenting on a yalesites-project PR, draft the comment text and hand it to the user to paste in rather than retrying the write.
GitHub workflow notes:
-
Comments and reviews can only be added, never edited in place — there's no tool to revise a comment that's already posted. Post a follow-up comment instead of trying to update one.
-
An epic's native GitHub progress bar only reflects open/closed sub-issue state. It has no awareness of custom Project board Status fields (e.g., "Ready for Deployment"), so don't expect it to show a custom workflow stage as complete — that needs to be tracked separately if it matters.
-
Some larger reworks run as "the whole epic lives in one PR" — nothing merges until the full scope is done and approved, rather than the usual merge-then-follow-up-tickets model. Confirm which model applies to a given epic before drafting PR review feedback that assumes work will continue after merge.
-
If a ticket should go to someone not yet onboarded to GitHub (no handle yet), assign it to the requester as a placeholder rather than leaving it unassigned, and swap in the real assignee once they're set up.
-
Status, Priority, and Size aren't set through issue creation. These are GitHub Projects V2 custom fields, and the issue-creation/update tools (
mcp__github__create_issue/update_issue) can't write to them directly — see "Writing Status, Priority, and Size to the Board" below for how to actually set them. -
The Project V2 Status field isn't queryable via the REST API either —
get_issue/search_issues/list_issueswon't return it. If asked to audit tickets by board status, the closest available proxy is PR review-state labels (pass code review/pass functional review/pass design review,needs review,needs work) on companion PRs acrossyalesites-project,atomic, andcomponent-library-twig— not a direct status query. -
GitHub's native Issue Type field (Task/Feature/Bug/Epic/Communications/AI, distinct from the
feature/bug/task/epiclabels used in Step 5 above) is settable from the CLI, and should be set on every ticket. It is not a Project v2 field, sogh project item-editcannot touch it, andmcp__github__create_issue/update_issuedo not expose it either. Use theupdateIssueGraphQL mutation, which takes anissueTypeId:# 1. list the org's types and their ids (ids are stable; look them up once) gh api graphql -f query='query{organization(login:"yalesites-org"){issueTypes(first:20){nodes{id name}}}}' # 2. get the issue's NODE id (not its number) ID=$(gh api graphql -f query='query{repository(owner:"yalesites-org",name:"YaleSites-Internal"){issue(number:1583){id}}}' --jq '.data.repository.issue.id') # 3. set the type gh api graphql -f query='mutation($id:ID!,$t:ID!){updateIssue(input:{id:$id,issueTypeId:$t}){issue{number issueType{name}}}}' -F id="$ID" -F t="<type-id>"Type ids as of 2026-08-20: Task
IT_kwDOA_XQ-s4Av2Na, BugIT_kwDOA_XQ-s4Av2Nd, FeatureIT_kwDOA_XQ-s4Av2Nf, Vendor/Supported BuildsIT_kwDOA_XQ-s4BtnpX, CommunicationsIT_kwDOA_XQ-s4BwK1h, AIIT_kwDOA_XQ-s4BwLHQ, EpicIT_kwDOA_XQ-s4CE0Px. Re-run step 1 rather than trusting this list if a type looks missing.This note previously said Issue Type had to be set manually in the UI. That was wrong, and it meant tickets created through this skill were shipping with no Type set at all.
-
Sub-issue linking may not be automatic. A
- [ ] #XXXXchecklist reference in an issue body creates a backlink, but don't assume it always registers as a tracked GitHub sub-issue (the mechanism that drives the parent's progress bar) — if the epic's progress bar isn't reflecting a child ticket, check whether it needs to be linked explicitly via "Add sub-issue" in the GitHub UI. -
Before recommending a ticket as "available" for pickup (by a person or an unmoderated agent), check more than the absence of an assignment label — confirm there's no assignee set and no
in-review-tag/forming-style in-progress signal on it. A ticket can look open on one signal and still be actively claimed. -
Also check for merged PRs, not just open ones, before recommending a ticket as available. A
claude-tagged or open-looking ticket can already be done — the PR merged but the issue was never closed (the team's PR body format isn't a GitHub-recognized closing keyword, so merges don't auto-close issues). A backlog sweep that only checks for open PRs will hand out already-finished work. Cross-reference against merged PR activity in the relevant repo(s) before calling a batch of tickets "ready." -
Don't close a ticket as stale or duplicate without first checking whether it has a live, unmerged PR against it — closing out from under active work is an easy mistake during a backlog sweep.
-
Cross-repo issue/PR references need the full
owner/repo#numberform. A bare#1299in an issue body always resolves to that number in the current repo, even if the surrounding text names a different repo. Linking ayalesites-projectPR from aYaleSites-Internalissue with just#1299will silently link to YaleSites-Internal's own issue #1299 instead. Always write the full cross-repo reference (e.g.yalesites-org/yalesites-project#1299) when linking across repos.
Clarify Missing Fields Before Starting
Before doing any grooming or drafting work on a single ticket, check whether the user's prompt covered Status, Priority, Size, Milestone, Assignee, and whether they want the claude label. Ask about anything they haven't already answered, using the AskUserQuestion tool.
Batch them, but mind the caps. AskUserQuestion allows at most 4 questions per call and at most 4 options per question. Both are hard validation, so a call asking six questions is rejected outright and the user sees nothing. Six unanswered fields therefore go in two calls:
- Board fields: Status, Priority, Size, Milestone.
- Routing: Assignee, and the
claudelabel.
Two calls instead of six round trips. Drop whatever the user already answered, and if four or fewer are left, ask in one call.
The option cap bites just as easily: Status has eight valid values, Size has five, and the assignee table below lists nine handles. Offer the few most likely for this ticket and let the user take Other, which AskUserQuestion supplies on its own. Never list every valid value as an option just because it is valid.
Do not guess or default these values silently. Status, Priority, and Size decide how the ticket is prioritized and sequenced on the project board; Milestone decides which release it ships in; Assignee and the claude label decide who (or what) actually picks it up.
Status (project board column)
Valid options, exactly as configured on the YaleSites Board, in board order. Match this capitalization exactly — these strings get passed straight to gh project item-edit, and several are not title-cased. This is a convenience copy; references/board-status.md is canonical and wins if they ever disagree.
| Status | Meaning |
|---|---|
Backlog |
This item hasn't been started |
Ready For Work |
Work that is up next |
To Do |
This is ready to be picked up |
Blocked |
Work is blocked and cannot move forward |
In progress |
This is actively being worked on |
In review |
This item is in review |
Ready for Release (in dev) |
This work is done, but has not been released yet |
Done |
This has been completed |
Ready For Work and To Do are both pre-start states and are easy to confuse. Ready For Work means queued as up-next; To Do means cleared for someone to pick up now. If the user hasn't said which they mean and the distinction matters, ask rather than guessing.
If not specified, ask: "What status should this ticket be set to on the project board?" Eight valid values against a 4-option cap, so offer the four that actually fit the ticket's shape, usually Backlog, Ready For Work, To Do, and whichever in-flight state applies. Other covers the rest.
If the board's options ever change, re-check them rather than trusting this list:
gh project field-list 6 --owner yalesites-org
Priority
Valid options: Hotfix · High · Medium · Low
If not specified, ask: "What priority should this be — Hotfix, High, Medium, or Low?"
If the answer is Hotfix, the title needs the Hotfix: prefix stacked in front of its type prefix. See "Title" below.
Size
Valid options: XS · S · M · L · XL
If not specified, ask: "What size estimate feels right — XS, S, M, L, or XL?" Five values against a 4-option cap, so offer the four that bracket your own estimate and let Other carry the fifth.
Milestone
Every ticket should land in a milestone. Ask which one, and lead with the next upcoming feature release as the recommended answer: the open milestone whose title is a dated release and whose due date is the soonest one still ahead of today.
"Dated release" means MM-DD-YY <anything> Release, not MM-DD-YY Feature Release specifically. Non-feature releases use the same shape, as 07-07-26 Drupal 10.6 Release does, and matching only on "Feature" would skip it and schedule an upgrade ticket a release late. Match on the leading date and the trailing Release instead. (Milestone #1 is 11-24-2025 Feature Release, with a four-digit year. It is closed and in the past, so it cannot win this sort, but allow for both year formats if you ever reuse the pattern for a broader sweep.)
Read the live list rather than working from memory, because the dates move and releases close:
gh api repos/yalesites-org/YaleSites-Internal/milestones --paginate \
-q '.[] | "\(.number)\t\(.title)\tdue=\(.due_on)\topen=\(.open_issues)"'
Three kinds of milestone show up in that list, and only the first is ever a valid default:
| Kind | Examples | When it's right |
|---|---|---|
| Dated release | 12-08-26 Feature Release, 07-07-26 Drupal 10.6 Release |
The normal case. This is what "next upcoming release" means. |
| Initiative | Views Block Rework, Drupal AI Migration |
Only when the ticket is a child of that epic or initiative. Don't pick one just because it happens to be open. |
| Catch-all bucket | Ready For Work, Backlog |
The work is real but genuinely unscheduled. These carry sentinel due dates far in the future (2030), so they'll never win a soonest-due-date sort. Don't let one become the default by accident. |
Two traps when working out which release is "next":
- A due date that has already passed, or is today, is not upcoming. The current release milestone stays open through and past its due date while the release ships and late fixes land, so the smallest due date in the list is often the one that's already going out the door. Sort only on due dates still ahead of today.
- Don't drop new feature work into a release that's already in its RC window. Once RC testing has started, only fixes for that release belong in it; new feature work goes to the release after. If you can't tell whether a release is locked, ask instead of assuming.
If not specified, ask: "What milestone should this go in?" Offer the next upcoming feature release first as the recommendation, with the current in-flight release and "no milestone for now" as the alternatives.
For an epic, the milestone is a scoping decision rather than a routine field, and the epic's own clarifying questions already cover it (Step 1 under "Creating an Epic": YaleSites epics ship as a single unit in one milestone). Don't ask for it twice.
Assignee
Always ask whether the ticket should go to someone, rather than creating it unassigned by default. An unassigned ticket is easy to lose in the backlog.
Ask: "Who should this be assigned to?"
Offer at most four options, per the cap above: the three handles most likely for this ticket's shape, chosen with the routing habits below, plus "leave it unassigned for now." The table is routing reference, not the option list. Other covers anyone else, and the gh api fallback below covers spelling.
| Handle | Who | Usually gets |
|---|---|---|
dblanken-yale |
David Blankenship, lead developer (Yale) | Development work. The default for anything that needs code written. |
vinmassaro |
Vincent Massaro, senior developer (Yale) | CI/CD and Drupal version upgrades: build and deploy pipelines, Pantheon workflow, core and contrib major-version updates. |
laura-johnson |
Laura Johnson, Four Kitchens | Development work. Four Kitchens is our outside dev partner, so weigh in-house capacity before routing here by default. |
atiddei |
April | UX work: flows, interface design, research-driven changes. |
ntay90 |
Nick Taylor | Accessibility. Anything needing WCAG 2.1 AA validation or an a11y review. |
alyssadechiaro |
Alyssa DeChiaro | Events and Localist: channels, imports, event content and display. |
chrissuquie |
Chris Suquilanda | Communications workstream (release emails, training emails, office hours, yalesites.yale.edu content), and a lot of feature requests and bug reports, since he works the ServiceNow queue. |
rachel-is-at-yale |
Rachel | Same shape as Chris: support intake off the ServiceNow queue, so a lot of feature requests and bug reports, plus mailing list and support-page work. |
miketullo95 |
Mike Tullo, Product Manager | PM-owned tickets, release coordination, ticket grooming follow-ups. |
Three routing habits worth keeping:
- Accessibility is easy to leave unrouted. If a ticket carries an accessibility acceptance criterion or the
accessibility-buglabel and nobody is named, suggestntay90. - Upgrade and pipeline work has its own owner. If a ticket is a Drupal core or contrib major-version upgrade, a PHP version bump, or a change to CI, build, or deploy tooling, suggest
vinmassarorather than routing it to general development. - A ticket that came in through ServiceNow usually stays with whoever filed it.
Feature Request:andBug:tickets often originate with Chris or Rachel working the support queue. If the user is grooming one of theirs, keep them on it rather than reassigning, since they hold the reporter context and will be the one closing the loop with the requester.
Keep "leave it unassigned for now" as one of the four. That is a legitimate answer while grooming a backlog, and it is better than parking the ticket on someone who isn't going to do it.
If the handle you want isn't one of these, pull the current list rather than guessing at spelling:
gh api repos/yalesites-org/YaleSites-Internal/assignees --jq '.[].login'
If the ticket belongs to someone who isn't on GitHub yet, assign it to the requester as a placeholder (see the workflow notes above) and say that's what you did.
Claude pickup (the claude label)
The claude label is defined on the repo as "tickets that are fully groomed and can be taken from a claude agent to be worked on asynchronously." Applying it puts the ticket in the pool the dev team's Claude agent pulls work from, so it is an actual handoff, not a category tag.
Always ask before applying it: "Do you want to tag this claude, so the dev team's Claude agent can pick it up and work it asynchronously?"
Two things have to be true before it goes on:
- The ticket is genuinely groomed. Description, a complete Acceptance Criteria list, Priority, Size, and Type all present, and specific enough that an agent with no other context could implement it and know when it's done. If the ticket is still a stub, say so and offer to finish grooming it first instead of tagging it as-is.
- Nothing in it is waiting on a human decision. An open design question, an unresolved debate preserved as an acceptance-criteria item (see "Acceptance Criteria" below), a "conflicts with platform direction" flag from Step 3, or work gated on a spike should not go into the async pool.
On epics, the label belongs on individual child tickets, never the parent. The parent is a container, not a unit of work.
Assignee and claude are independent: a ticket can be both assigned to a person and tagged for agent pickup, so ask both questions rather than treating one as the answer to the other.
Once the values are confirmed, proceed with grooming.
Note: if the request is actually epic-shaped (see "Creating an Epic" below), don't apply Status/Priority/Size to the parent epic ticket the same way — those apply to each child ticket individually. Use the epic's own clarifying-question flow instead.
Writing Status, Priority, and Size to the Board
Once the issue exists and Status/Priority/Size are confirmed, write them to the YaleSites Board project (yalesites-org, project number 6). Try the gh CLI first; fall back to the trigger-label workflow if it's not usable in this session (e.g. Cowork sessions that don't have gh configured).
Preferred: gh CLI — writes the Project v2 fields directly. No label workaround, no waiting on the GitHub Action to migrate it.
- Check it's usable before relying on it:
gh auth status.ghneeds to be installed, authenticated, and its token needs theprojectscope specifically —read:projectalone can read the board but can't write to it. If any of that isn't true, stop and use the MCP fallback below instead of troubleshooting the user'sghsetup mid-task. - Make sure the issue is on the board (a no-op if it's already there):
gh project item-add 6 --owner yalesites-org --url <issue-url> - Set each field by name — no need to look up field or option IDs:
Use the exact option text from the "Clarify Missing Fields" section above, including its capitalization —gh project item-edit 6 --owner yalesites-org --url <issue-url> --field "Status" --value "Ready For Work" gh project item-edit 6 --owner yalesites-org --url <issue-url> --field "Priority" --value "High" gh project item-edit 6 --owner yalesites-org --url <issue-url> --field "Size" --value "M"ghmatches--valueagainst the field's configured options, and several Status options are not title-cased (In progress,In review,Ready for Release (in dev)). - If any
gh projectcommand fails for any reason (auth, scope, a renamed option, anything), don't retry — fall back to the label workflow below and tell the userghwasn't available so they can fix it later.
Fallback: MCP + trigger labels — for sessions without a working gh. Apply the status:*/priority:*/size:* trigger label via mcp__github__update_issue (e.g. status:ready-for-work, priority:high, size:m). A GitHub Action reads the label, writes the corresponding Project v2 field, and deletes the label — so don't expect the label to persist as a way to check the value later. Note update_issue replaces the whole label array, so fetch current labels first and send the complete list.
Writing the milestone, assignee, and claude label
None of these three is a Project v2 field, so none of them needs gh project item-edit. All three live on the issue itself:
gh issue edit <number> --repo yalesites-org/YaleSites-Internal --milestone "12-08-26 Feature Release"
gh issue edit <number> --repo yalesites-org/YaleSites-Internal --add-assignee dblanken-yale
gh issue edit <number> --repo yalesites-org/YaleSites-Internal --add-label claude
gh issue edit --milestone takes the milestone's title, matched exactly. mcp__github__create_issue/update_issue take the milestone's number instead, so grab it from the gh api .../milestones listing above rather than passing the title.
All three can also be set at creation time via mcp__github__create_issue (milestone, assignees, labels), or after the fact with mcp__github__update_issue. If you use update_issue, remember it replaces the whole label array: fetch the issue's current labels first and send the complete list, or you'll silently drop the ones already on it.
For the full board reference — reading current values, the project scope requirement, which skill owns which lifecycle transition, and the known gaps — see references/board-status.md. Only set the fields this skill is responsible for (the ones the user confirmed at creation/grooming time). Don't advance a ticket through the workflow as a side effect of grooming it.
Platform Fit Check (Do This First)
Before drafting or grooming any ticket — including each child ticket inside an epic — cross-reference the request against the YaleSites platform knowledge base. This catches overlap, suggests extensions over net-new work, and surfaces documentation implications early.
Step 1: Check what already exists
Load the relevant yalesites skill references and ask:
-
Does a similar block, component, or content type already exist? Check
blocks-reference.mdandparagraphs-reference.md. If the request is for something like "a new way to display a list of items," check whether an existing block (e.g., Collection, Custom Card Collection) already does this or could be extended. -
Does an existing content type already support this use case? Check
content-types-reference.md. If the request adds a field or behavior to Pages, Posts, Events, etc., confirm whether the field already exists or if this is genuinely additive. -
Does an existing setting or configuration option already cover this? Check
settings-reference.md. Some "feature requests" are already available but undiscovered by the requester. -
Are there existing Views or display modes that could be repurposed? Check
views-reference.md.
Step 1b: Check who the change affects
Do this for any ticket that changes what users see or do — features, UX changes, editorial workflow changes, and bugs that affect an editor-facing behavior. Skip it for purely internal work (dependency bumps, CI, refactors with no user-visible surface) and say you're skipping it rather than silently omitting it.
Load the yalesites-ux-research skill and answer three questions in the ticket:
- Which archetype(s) does this affect? Name them explicitly. The six are Multi-Site Manager, Part-Time Owner, The Maintainer, Transitioning Team, New Recruit, and Solo Starter.
- Does it map to a known pain point? Check the pain point table. A request matching a high-count pain point (confusing terminology at 67%, misleading search results at 61%, poor UI discoverability at 56%) carries real evidence behind it, and the ticket should say so.
- Does it put a current delight at risk? Check the delights table before scoping anything that changes learning resources, the Views GUI, release communications, or brand-compliant output.
Two traps worth naming, both from the 2026 research:
- Don't "simplify" for the Part-Time Owner. They are time-poor, not skill-poor. Reducing steps helps them; removing capability doesn't.
- New-user cost is the usual hidden cost. The five most-cited pain points all land hardest on Solo Starter and New Recruit. If a change adds a new term, setting, or interface surface, say what it costs those two archetypes even when it's aimed at someone else.
Where this lands in the ticket: name the affected archetypes and the reasoning in the collapsed Background and analysis block (see "How this reads on GitHub" above), and turn archetype-specific expectations into acceptance criteria (for example, "labels should be understandable to a New Recruit with no YaleSites training"), which stay visible. Don't paste archetype summaries in wholesale.
Step 2: Determine the scope recommendation
Based on the platform check, classify the ticket as one of:
| Classification | What it means | Action |
|---|---|---|
| Extend existing | A current block/component/type already handles the core need; this request adds an option, field, or variant | Note the existing feature in the ticket description; scope the work as an enhancement to that feature |
| New, distinct feature | Nothing on the platform does this; the request is genuinely additive | Proceed as a new feature ticket |
| Already exists | The request is already supported on the platform | Flag this in the ticket; note where the feature lives and consider closing as "works as intended" or converting to a docs/training ticket |
| Conflicts with platform direction | The request would add complexity that cuts against platform constraints or brand consistency goals | Flag the concern in the ticket; surface it for PM discussion before scoping |
Step 3: Surface the findings in the ticket
Incorporate the platform check findings into the ticket. The full write-up goes in the collapsed Background and analysis block, but these three cases also get a short visible note right under the TL;DR, because they change whether or how the ticket should be scoped:
- If extending existing: The description should name the existing feature ("This adds a new layout option to the existing Action Banner block…"). The acceptance criteria should note which existing docs need updating rather than pointing to new docs.
- If already exists: Short note under the TL;DR ("Note: this functionality may already exist via [X]. Recommend confirming before scoping.").
- If conflicts: Short note under the TL;DR flagging the concern for the PM to review before development is assigned.
Issue Format
Every issue needs a clear title, a TL;DR line, then these six fields, in this order.
How this reads on GitHub
An issue is opened and skimmed by people across the team, most of them non-technical. Compose the body per two files in references/: github-communication-format.md for the structure, and github-writing.md for the words. Read both before drafting.
github-writing.md is where the plain-language rules live: 20-word acceptance criteria, 25-word descriptive sentences, active voice, the word-swap table, and a 400-word budget on the visible layer of a ticket. The short version is that depth is free once it is collapsed, and the visible layer has to earn every word.
- First line of the body is a TL;DR — one or two plain-language sentences saying what the work is and why it matters. No Drupal jargon. This is the part everyone actually reads.
- Keep visible: the TL;DR, the Description, and the full Acceptance Criteria. Developers and QA work straight from the Acceptance Criteria, so it never gets collapsed.
- Collapse into a named
<details>block (for example<summary><b>Background and analysis</b></summary>): the Platform Fit Check write-up, the archetype and UX-research reasoning from Step 1b, implementation context pulled from the repos, and links to prior art. The conclusions that change the work still surface in the Description and Acceptance Criteria; the supporting analysis sits in the collapsed block. - One exception stays visible: an "already exists" or "conflicts with platform direction" flag from Step 3 (see below) goes as a short note directly under the TL;DR, not inside the collapsed block, because it gates whether the ticket should be scoped at all.
- Close with a
<!-- yalesites:agent -->block carrying the refs the next agent would otherwise re-derive: related issue and PR numbers, the epic or child links, and the platform-fit verdict as a flag. It is invisible on the rendered page. See the machine-only block section ofgithub-communication-format.mdfor the rules, including what must never go in it.
Check the draft before posting:
python3 scripts/check-github-text.py draft.md --surface ticket
It flags em dashes, over-length criteria, banned words, a missing or bloated TL;DR, <details> blocks that will not render, and anything unsafe in the machine block. Findings are candidates, not verdicts: fix what is real, and say so if you are overriding one.
Title
The title is what shows up in board views, search results, and notification emails — it needs to tell someone what the work is without opening the issue. Write (or rewrite) the title so it names the specific thing being built, fixed, or requested, not a vague category. "Fix embed bug" tells nobody anything; "RC: Embed block: missing title attribute on Bluesky, Instagram, and event map embeds" tells the reader exactly what's broken and where.
When grooming an existing stub ticket, check the title along with the other fields — a placeholder or overly generic title ("Bug in Layout Builder", "Update embeds") should be rewritten to reflect what the description actually says once it's filled in. If rewriting a title would change what the issue appears to be about (e.g., the original title implies a different bug than what's described), flag that for the user rather than silently reinterpreting.
Common title prefixes. The backlog uses a set of recurring prefixes that make similar tickets easy to spot and filter on. Apply one if it fits; don't force one onto a ticket that doesn't match any of these patterns — a plain, descriptive title is better than a mismatched prefix.
| Prefix | When to use |
|---|---|
Hotfix: |
A ticket set to Hotfix priority. Unlike the others, this one stacks in front of the type prefix rather than replacing it, so a hotfix bug reads Hotfix: Bug: <what's broken>. See the stacking note below the table. |
RC: |
A bug found during release-candidate testing, before the release ships. Use when the prompt says something like "make an RC ticket" or references RC/QA testing. (Occasionally numbered — RC2:, RC3: — for issues found in a later regression pass on the same release.) |
Epic: |
The parent ticket for a multi-ticket initiative. See "Creating an Epic" below. |
Bug: |
A defect found outside of RC testing (reported by an editor, caught in normal use, etc.). |
Embed: |
New or changed support for a specific embeddable content type (calendars, maps, social posts). |
Embed Request: |
An intake ticket for a new embed type someone has asked for but that isn't scoped/dev-ready yet. |
Feature Request: |
A new-functionality ask, typically as raised by a stakeholder, before it's fully scoped. |
UX Research: / UX Discovery: / UX Evaluation: |
Research-workstream tickets — study prep, participant outreach, interview scheduling, evaluating an existing flow. |
Design: |
A visual/design deliverable with no dev work implied (mockups, icon sets, etc.). |
Web: |
Work on the yalesites.yale.edu marketing/content site itself, not the platform. |
Docs: |
A documentation-only ticket. |
Migration Tool: |
Work on content migration tooling (CSV import/export, etc.). |
Community Spotlight: |
A specific Community Spotlight content ticket. |
YS-Email: |
YaleSites training or release email tickets. |
Hotfix: stacks; the rest don't. Every other prefix in the table is one-per-ticket. Hotfix: is the exception: it goes in front of whatever prefix the ticket already earned, so you get Hotfix: Bug: ... or Hotfix: View: .... This matters because Priority is a Projects v2 field, which means it doesn't appear in search results, notification emails, or most board views. The title prefix is the only place the urgency is visible to someone skimming. Whenever Priority comes back as Hotfix, prepend it. Reference examples: YaleSites-Internal #1729, #1730, #1732, #1381.
Epic child tickets follow a related but distinct pattern: they're prefixed with a short form of their epic's name rather than one of the prefixes above, so they group together visually in the backlog (e.g., Views Block Rework: Authoring Form UX/UI Audit, Publications: Teaser Display, Wave 3: Interactive molecules). When scoping child tickets for a new epic, pick one short, consistent name and apply it to every child ticket — see "Creating an Epic" below.
TL;DR (first line of the body)
One or two sentences, plain language, leading with what the work is and why it matters. Write it last, once the Description and Acceptance Criteria are settled. Example: "Editors can't set alt text on gallery images, so galleries fail accessibility review. This adds an alt-text field to each gallery item." Keep it above the Description; it is the only part guaranteed to be read.
1. Description
Briefly describe the feature, bug, or improvement in plain language. Avoid developer jargon. Focus on what it is and why it matters to the user or platform. Anyone on the team, regardless of technical background, should be able to read this and understand the work.
2. Acceptance Criteria
A single bulleted list of everything required to close the issue. Cover all relevant angles:
- Developer tasks and technical requirements
- UX considerations (field labels, design option names, UI behavior). Where Step 1b identified an affected archetype, write the expectation as a testable criterion rather than a general aspiration — "a New Recruit can complete this without asking a teammate" is checkable, "should be intuitive" isn't.
- Accessibility: flag anything that needs WCAG 2.1 AA validation for the accessibility engineer
- Documentation: note if any existing docs need updating or new docs need to be created
If an existing ticket has an unresolved internal debate (e.g., a past comment thread arguing both sides of a design question), don't resolve it unilaterally while grooming — preserve it as an explicit open acceptance-criteria item so it gets a real decision rather than getting silently closed over.
Documentation tickets for features that haven't shipped yet: these can still be marked Ready For Work so the doc can be drafted and staged in parallel with development — mark the ticket as blocked by the feature ticket, and add an explicit acceptance criterion not to publish until the feature ships.
3. Priority
Choose one based on user and platform impact:
| Priority | When to use |
|---|---|
| Hotfix | Critical issue breaking core functionality or accessibility. Immediate deployment required. Also prepend the Hotfix: title prefix (see "Title" above). |
| High | Significantly impacts user experience or blocks key workflows. Soonest milestone priority. |
| Medium | Improves platform usability or addresses moderate user pain points. |
| Low | Nice-to-have enhancement with minimal user impact. Future consideration. |
4. Size
Estimate relative effort and complexity:
| Size | Definition |
|---|---|
| XS | Single file change or quick setting adjustment. < 1 hour |
| S | Minor feature update or simple block modification. 1–4 hours |
| M | New block component or moderate platform enhancement. 1–2 days |
| L | Complex feature with multiple components or significant UX changes. 3–5 days |
| XL | Major platform addition requiring design system updates and extensive testing. 1–2 weeks |
5. Type
Choose one:
| Type | When to use |
|---|---|
| AI | Work related to the Beacon AI chatbot |
| Bug | An unexpected problem or behavior |
| Communications | Work that falls under the Communications workstream. Includes documentation written for the public yalesites.yale.edu site (user guide pages, resource pages, release documentation), even when the title carries the Docs: prefix. A Docs: prefix does not imply Task. |
| Feature | A request, idea, or new functionality |
| Task | A specific piece of work |
| Vendor/Supported Builds | Websites being built by vendors or supported by the YaleSites team |
6. Labels
Apply all relevant labels: ai-engine feedback vendor-build accessibility-bug opac (add others as applicable)
claude is a workflow label, not a descriptive one. It only goes on when the user has explicitly confirmed it and the ticket clears the bar in "Claude pickup" above. Never add it as part of a routine "apply all relevant labels" sweep.
Creating an Epic
Use an epic when a request is genuinely too large for one ticket — it touches multiple components or content types, needs several developers, or will clearly span multiple sprints. If you can describe the work in one well-scoped Acceptance Criteria list, it's a ticket, not an epic. A useful gut-check from how epics are usually scoped: if you can't state the outcome in a sentence or two, or the work looks like it's more than about 4–6 weeks, it's epic-shaped.
The reference example is yalesites-org/YaleSites-Internal#1161 (Views Block Architectural Rework) — the team's lead developer called this one out as working really well. When in doubt about how much detail to include or how to structure something, look at how #1161 handled it.
Step 1: Ask clarifying questions before drafting
Epics fail most often because scope was never pinned down before work started (see anti-patterns below). Before drafting anything, use AskUserQuestion to gather whatever the user hasn't already told you:
- Problem/goal — What isn't working today, or what opportunity does this open up? What's the one-sentence outcome this epic delivers?
- Scope boundaries — What's explicitly in scope? Is there adjacent work that's tempting to fold in but should really be its own follow-up?
- Success criteria — How will you know the epic is done? An epic with no closure criteria never closes.
- Milestone/timeline — Does this need to land in a specific release, or is timing open? YaleSites epics ship as a single unit in one milestone once all child tickets are done — they don't span multiple releases. A scoping/spike ticket can land in an earlier milestone on its own if the epic itself isn't ready to commit to a release yet.
- Known dependencies — Anything that has to happen first, or any other epic/ticket this depends on?
Stay within PM scope: these are product-goal and boundary questions, not implementation questions. If the technical approach is still unknown, that's expected — that's what the spike ticket in Step 2 is for. Only ask about what's genuinely still ambiguous; skip anything the user already answered in their prompt.
Step 2: Decide if a spike/ADR ticket comes first
If the technical approach isn't settled, make the first child ticket a spike or ADR (architecture decision record) — audit the current state and propose a design, to be reviewed and approved before the rest of the child tickets are written or estimated. This was #1162 in the Views Rework epic, and it's why the later child tickets could be scoped accurately. Don't force full detail into every child ticket up front if the design is still an open question — scope what you can, and let the spike inform the rest.
Step 3: Write the epic (parent ticket)
Use this structure, modeled on #1161. Same GitHub-reading rules as a regular ticket (see "How this reads on GitHub" above): lead with a TL;DR, keep Summary / Scope / Child Tickets visible, and collapse the longer context (Current State, Proposed Approach) into a named <details> block.
**TL;DR:** [One or two sentences: the outcome this epic delivers and why. Plain language.]
## Summary
[What is being built or changed, and why — the outcome, not just the mechanism]
<details>
<summary><b>Current state and proposed approach</b></summary>
### Current State
[Optional, valuable for refactors/rework: what exists today and why it's a problem]
### Proposed Approach
[High-level shape of the solution — enough for the team to orient, not a full technical design. Point to the spike/ADR ticket for the detailed design.]
</details>
## Scope
[What's in scope — content types, features, user flows, etc. affected]
## Child Tickets (intended order)
- [ ] #XXXX -- [Spike/ADR: design approval before further work, if applicable]
- [ ] #XXXX -- [Child ticket 1]
- [ ] #XXXX -- [Child ticket 2]
## Follow-up Work (Post-Epic)
[Explicitly out-of-scope items this epic unblocks or surfaces. Tag with a `<epic-name>-followup` label so they're easy to find later, and note they shouldn't be pulled into active work until the epic's milestone closes.]
Create the child tickets as real GitHub issues first, then reference their numbers in the parent's checklist — GitHub automatically tracks checklist items that reference issue numbers as sub-issues, which is what gives the parent a clean progress view (this is what made #1161 easy to follow).
Step 4: Apply the epic label
Always apply the epic label to the parent ticket. This is how the epic is distinguished from a regular issue on the board and in filtered views — don't skip it, and double-check it's set before treating the epic ticket as done.
Step 5: Write each child ticket using the standard Issue Format
Every child ticket still follows the six-field format above (Description, Acceptance Criteria, Priority, Size, Type, Labels). An epic doesn't change how individual tickets are written — it just adds a parent that ties them together and gives the whole body of work a shared goal.
Avoid these epic anti-patterns
- No defined outcome. If you can't say what "done" looks like, the epic will never close out. Pin this down in Step 1.
- The dumping ground. Don't let unrelated bugs, requests, or "while we're in there" ideas get folded into an in-flight epic — route them to the Follow-up Work section or a separate ticket instead.
- Scope creep without re-scoping. If new child tickets keep getting added without the goal itself changing, the epic was probably under-scoped at the start. Pause and revisit Step 1 rather than continuing to bolt things on.
- Spanning multiple releases. YaleSites epics ship as one unit in one milestone. If it's genuinely going to take multiple releases, it likely needs to be split into two epics that each deliver independent value.
Epic Quality Bar
Before treating an epic as ready:
- Body opens with a TL;DR line, and the longer context (Current State, Proposed Approach) is in a collapsed block
- Problem/goal and success criteria are stated in the Summary
- Scope is explicit — in-scope, and out-of-scope/follow-up if relevant
-
epiclabel is applied to the parent ticket - Child tickets exist and are checklisted in the parent
- If the technical approach is unsettled, a spike/ADR ticket is first in the child list
- Milestone reflects a single release, not an open-ended span
-
ScopeandChild Ticketsreflect current reality — if any child ticket has diverged since the epic was written, run theticket-syncskill before calling the epic ready
Grooming Existing Stub Tickets
When grooming an existing ticket that's missing fields or has a thin description:
- Read the existing issue body — preserve anything already written
- Identify what's missing (description too vague, no acceptance criteria, no size/priority, etc.) — including whether the title is vague, generic, or doesn't match what the description actually says
- Search the relevant repos for context if the work is technical
- Fill in the missing fields following the format above, rewriting the title if it doesn't clearly state the work (see "Title" above)
- If the ticket has no TL;DR line, add one at the top. If it carries a block of platform-fit or archetype analysis inline above the Description, move it into a collapsed Background and analysis block — this is a reformat, not a rewrite; preserve the words
- Use
mcp__github__update_issueonyalesites-org/YaleSites-Internal— append or replace only the missing sections, never overwrite content the team has already written
Checking for Drift, Not Just Gaps
Grooming isn't only about filling in missing fields. A ticket can have a complete Description and Acceptance Criteria and still be wrong, because the work moved past what's written and nobody wrote the change back. This shows up most on epics and other long-running tickets, where scope gets resolved in PR reviews or side conversations that never make it into the ticket body itself.
This applies to an established ticket with real history — comments, linked PRs, sub-issues — not the blank-stub case the gap-filling steps above cover. Before treating that kind of ticket as groomed, load the ticket-sync skill and run its check. It compares what's written against what's actually been decided and tells you whether anything needs a comment (context only) or an edit (scope/acceptance criteria actually changed). Run this in addition to the gap-filling steps above, not instead of them — see the Quality Bar below.
Quality Bar
Before submitting or updating an issue, check:
- Title clearly states the specific work, and uses a matching prefix if one applies
- If Priority is Hotfix, the title carries the
Hotfix:prefix in front of its type prefix - Body opens with a TL;DR line: one or two plain-language sentences, no jargon
- Platform-fit and archetype analysis is in a collapsed Background and analysis block, not inline above the Description (an "already exists" / "conflicts" flag excepted — that stays visible under the TL;DR)
- Description is jargon-free and makes sense to a non-developer
- Acceptance criteria covers dev, UX, accessibility, and docs angles
- For user-facing work, affected archetypes are named and any new-user cost is stated (Step 1b), or the check is explicitly noted as not applicable
- Priority reflects actual user/platform impact (don't default to Medium)
- Size is realistic — if unsure, err toward larger
- Type is set — as the native Issue Type field on the issue, not just stated in the body (see the
updateIssuemutation in the workflow notes above) - Relevant labels are applied
- Milestone is set to the release the work is actually going into, not whichever open milestone sorted first
- Assignee is set, or the user explicitly chose to leave it unassigned
-
claudelabel is on only if the user asked for it and the ticket is complete enough for an agent to work it unattended - For an established ticket with real history, Description and Acceptance Criteria still match what's actually been decided — run the
ticket-syncskill (see "Checking for Drift, Not Just Gaps" above) if unsure
For epics specifically, also run through the Epic Quality Bar above.