| name | tickets-audit |
| description | Audits tickets/issues in a repo against the actual codebase — surfaces stale, silently-completed, or no-longer-relevant work plus format, dependency, status drift, and content quality problems. Auto-detects ticket source (GitHub Issues, Airtable, TICKETS.md). Do not use automatically; invoke explicitly via /tickets-audit when the user wants to audit their backlog. |
Tickets Audit
A backlog of tickets rots in two ways. Syntactically — format drifts from the standard, labels lie about status, dependencies point to closed or non-existent issues. And semantically — the code moves on, features get silently shipped or removed, and the ticket text no longer matches reality.
This skill audits both. The output is a markdown report under docs/audits/ that a human can act on: which tickets to close, which to update, which to refile, which to investigate.
The flagship dimension is codebase relevance — for each ticket, an agent reads the ticket plus the actual code and judges whether the work is still real. That's where most of the value comes from. The other dimensions are syntactic gates run more cheaply.
When to use
This skill runs only when the user explicitly invokes /tickets-audit. Do not auto-trigger on ticket-related conversations.
Once invoked, the user wants one of:
- A full backlog audit ("audit our tickets")
- A focused stale-check ("are these still relevant?")
- A silent-completion sweep ("have any of these shipped without being closed?")
- A grooming pass before planning or a release
If the user invoked this skill but their actual intent is creating a new ticket, migrating TICKETS.md to a different backend, or auditing architecture contracts, redirect them to [[tickets-and-logbook]], [[tickets-migration]], or [[contracts-audit]] respectively.
Source detection
Detection is not just file-presence — read project docs first. Many projects have a TICKETS.md that is a stub pointer to a real backend (Airtable, Linear, etc.), or a CLAUDE.md / AGENTS.md that explicitly forbids one source and routes to another. File existence alone is a misleading signal.
Step 1 — read project intent. Open CLAUDE.md, AGENTS.md, README.md, and docs/TICKETS.md (read the whole file if it exists — likely short if it's a stub). Look for explicit statements like "tickets live in X", "no GitHub Issues", "migrated to Airtable", or backend ID patterns: Airtable base app[A-Za-z0-9]{14} / table tbl[A-Za-z0-9]{14}, Linear team prefixes, etc. The project's own docs are the highest-confidence signal — they override file/remote heuristics.
Step 2 — fall back to file / remote detection:
- Airtable — if Step 1 yielded an Airtable base ID, or the repo has
.airtablerc / AIRTABLE_BASE_ID env reference, use Airtable. The docs/TICKETS.md body often contains the base + table IDs explicitly — grep there before asking the user. See references/airtable.md.
- GitHub Issues — if
gh repo view succeeds and gh issue list --state all --limit 1 returns at least one issue, AND the project docs don't forbid GitHub Issues, use GitHub Issues. See references/github-issues.md.
- TICKETS.md (flat-file) — only when
docs/TICKETS.md exists AND is NOT a stub. Heuristics: >100 lines AND contains actual ## TICKET-NNNN (or similar) heading-style entries. In this case — and only this case — recommend at the top of the report that the project migrate via [[tickets-migration]]. Never recommend migration when TICKETS.md is a stub pointing at a real backend — that migration has already happened. See references/tickets-md.md.
If more than one real source exists (e.g., GitHub Issues and a real TICKETS.md), audit both and produce a single combined report. Note the duplication as a finding.
Per-source mechanics live in the references:
- GitHub Issues: read
references/github-issues.md
- Airtable: read
references/airtable.md
- TICKETS.md: read
references/tickets-md.md
The six dimensions
Run in this order — cheapest and most objective first, semantic codebase reasoning last, cross-ticket duplication last of all (it depends on dimension 5's verdicts). This lets you batch the syntactic checks across all tickets quickly, then concentrate the expensive judgment work on the tickets that are worth it.
1. Format compliance
The project's own schema takes precedence over the canonical one. Before applying any checklist, look for project-specific schema documentation in docs/TICKETS.md, AGENTS.md, CLAUDE.md, or a project-level skill. If the project documents its own ticket schema — its required fields, its status taxonomy, its label conventions — audit against THAT. The canonical [[tickets-and-logbook]] schema below is the fallback only when no project schema is documented.
When a project names a concept differently (e.g. Scope instead of Problem, or Acceptance instead of Acceptance Criteria), map the canonical concept to the project's field and check whether the concept is covered, not whether the field is named the canonical way. A "missing Problem" finding when the project's Scope field carries the problem is noise — it floods the report and trains the human to ignore it. Only flag truly-missing concepts.
Similarly, the canonical status names (proposed, ready, in-progress, blocked) are illustrative. Remap to the project's actual Status single-select values when judging staleness or readiness. The verdict structure of dimensions 3 and 5 still applies — only the labels change.
Canonical fallback (use only when the project has no documented schema):
- Required sections: Problem, Approach, Dependencies, Acceptance Criteria, Testing, Logbook
- Exactly one thematic label and one status label
- Title style: short imperative, no ticket ID prefix
Catches: truly-missing concepts (not naming differences), duplicated sections, malformed acceptance criteria (not checkbox lists in projects that require checkboxes), label-count violations against the project's documented convention, status values that don't match the project's documented taxonomy.
2. Dependency integrity
Builds the dependency graph from Depends on: #N lines.
Catches: refs to issues that don't exist; refs to closed issues from open ones (which may now be unblocked); cycles; ready-labeled issues with unresolved dependencies; blocked-labeled issues whose blockers are closed.
3. Status & logbook drift
Cross-references status labels with actual activity.
Catches: in-progress tickets with no commit, comment, or logbook entry in the last 30 days; closed tickets with no logbook entry; logbook entries that reference issues that don't exist; commits referencing #N for issues still labeled proposed.
4. Content quality & gaps
Reads the ticket body for substance.
Catches: vague acceptance criteria ("works well", "is fast"); missing Testing section content; Approach section that's just one bullet for non-trivial work; stale file paths or symbol references in the body that no longer exist in the repo.
(Near-duplicate detection used to live here as a one-liner. It now has its own dimension 6 — it's a relationship-between-tickets concern, not a per-ticket text concern, and it deserves real detection criteria.)
5. Codebase relevance — the flagship dimension
For each ticket — particularly those labeled proposed, ready, in-progress, or blocked — judge whether the work the ticket describes is still real, given the current state of the code.
Process per ticket:
- Read Problem + Approach + Acceptance Criteria.
- Identify the artifacts the ticket touches: file paths, module names, feature names, types, functions, routes, env vars, UI surfaces, data fields.
- For each artifact, check current state via Grep / Glob / Read:
- Does it still exist? (Stale if not.)
- Has its shape changed enough that the ticket no longer applies?
- Read git log since the ticket's creation date for commits touching the relevant paths. Skim the diffs.
- For each acceptance criterion, judge whether current code satisfies it. Be specific — cite files and lines.
- Land on exactly one verdict:
| Verdict | Meaning |
|---|
still-relevant | Work described is still needed; acceptance criteria not satisfied by current code |
silently-completed | Code shows the criteria are met but the ticket wasn't closed |
partially-completed | Some criteria done, others remain — note which |
stale | The feature/module/path the ticket references no longer exists or has changed shape such that the ticket is obsolete |
inconclusive | Can't decide from code alone — flag for human review with the specific question |
Evidence rule: vague claims like "looks done" don't count. Every verdict cites specific files, line numbers, and commits.
Scaling: if the repo has many open tickets (say, >50), prioritize this dimension for in-progress, then blocked, then ready, then proposed. Run dimensions 1–4 across all tickets regardless. If the user asked for a quick pass, say so in the report and list which tickets weren't deeply examined.
6. Cross-ticket duplication & overlap
Two tickets that describe essentially the same work surface waste backlog real estate, scatter related context, and lead to one being silently-completed by the other. Title-identical detection (does any other ticket have the literal same title?) catches only the easy cases. The hard cases are tickets with different titles, sometimes different Groups, that nonetheless cover the same change — those are what this dimension targets.
Run this dimension after dimension 5. Dimension 5's verdicts feed in: a duplicate of a done/closed ticket is the silently-completed half, which is a critical finding.
Vocabulary
| Category | Definition | Audit action |
|---|
| Duplicate | Two tickets cover essentially the same work. Closing one in favor of the other doesn't lose meaningful scope information. | Flag — recommend merge or close (Warning) |
| Silent-completion duplicate | An active ticket whose work was actually shipped under a different done/closed ticket. | Flag — recommend close as silently-completed (Critical) |
| Related-but-distinct | Two tickets touch the same surface but do different things — different layers, different concerns, orthogonal features on the same table or handler. | Do NOT flag. List in "Likely-related-but-distinct" so readers see what was considered and rejected. |
| Sister tickets | Parts of an explicit parent split — T-NNN.M-format sub-tickets under T-NNN, or a documented spawn batch from a gap audit (e.g. four tickets filed the same day from the same audit). | Do NOT flag. |
Detection signals
Field names below follow the project's schema — the examples use Airtable-flavored names (Scope, Deliverable, Group, Detected by). When auditing another source, map to its equivalents (canonical schema: Problem, Approach, Acceptance Criteria, thematic label), same as dimension 1's concept-over-name rule.
For each candidate pair (or cluster), score on:
- Same
Group / area / theme. Gating filter — only compare tickets in the same Group. Cross-group comparisons explode the search space and rarely surface real duplicates.
- Touched-file overlap. Extract file paths from
Scope, Deliverable, and Acceptance. >50% shared paths is a strong signal.
- Title fuzzy match. Word-overlap after stripping stopwords ("add", "fix", "implement", "wire", "update"). >50% content-word overlap is a strong signal.
- Scope-text semantic overlap. Read both Scopes. Are they describing the same change? This is the judgment call that distinguishes duplicate from related-but-distinct — and the most expensive signal to compute.
- Same
Detected by source. If both say "REM-040", "wave review 2026-05-21", or "audit 2026-05-28", they may be the same observation filed twice.
- Same
Acceptance criteria. Literal overlap in checkbox text.
A pair is a duplicate if signal 1 (same Group) is true AND at least 2 of signals 2–6 are strong.
Cross-check against done/closed
For every active duplicate cluster, also search done/closed tickets in the same Group with title-fuzzy + Scope-keyword matching. If the same work appears in a done/closed ticket, the active ticket is a silent-completion duplicate — escalate to Critical.
This is the value add over a pure dimension-5 silent-completion check: dimension 5 reads code, dimension 6 reads other tickets. Sometimes the easiest evidence that work shipped is another ticket that already says "closed" for it.
Sub-ticket awareness
T-NNN.M-format sub-tickets are intentional siblings under T-NNN. Do NOT flag siblings as duplicates of each other (it would be unusual for them to truly overlap). Do flag a sub-ticket as duplicate of a non-sibling that covers the same work.
Other intentional sibling patterns:
- A gap-audit spawn batch (e.g. multiple tickets filed the same day from a single audit, all in the same Group).
- A wave's parallel work-items called out in a wave plan.
When in doubt, search the logbook + linked plan docs for the parent context before flagging.
Output format
Report by cluster, not by pair. A cluster is N≥2 tickets in the same Group judged to cover the same work. Even when N=2, the cluster framing scales cleanly if a third or fourth duplicate surfaces later.
### Cluster N — <one-line theme>
Tickets in cluster:
- T-X (status, group) — <title>
- T-Y (status, group) — <title>
Evidence:
- Same Group: yes
- Touched files: <overlapping file paths>
- Title content-word overlap: <N%>
- Scope semantic overlap: <one-sentence judgment>
- Same Detected-by: yes/no
- Acceptance overlap: yes/no
Recommended action: close T-X in favor of T-Y. Retain T-Y because <broader scope / more downstream refs / older with logbook history / etc.>.
Confidence: high/medium/low.
After the duplicate clusters, include a brief "Likely-related-but-distinct" list — pairs that look like duplicates on title or Group but are actually distinct on inspection. This builds the audit's credibility by showing what was considered and rejected, and prevents the next audit from re-litigating the same pairs.
When to skip this dimension
- The active-ticket count is small (<25) and you've eyeballed the titles — duplicate detection has low yield.
- The user asked for a "quick" pass — dimensions 1–4 are mechanical; dimension 6 is expensive (judgment on Scope overlap).
- The project explicitly uses sub-tickets / spawn batches heavily — the noise floor is high.
If you skip it, say so in the report's methodology note. Don't silently drop a dimension.
Workflow
-
Detect the source(s) and tell the user what you found, including any TICKETS.md migration recommendation.
-
Snapshot the tickets to a temp working file so a long audit doesn't re-fetch repeatedly. For GitHub: gh issue list --state all --json number,title,body,state,labels,createdAt,updatedAt,comments. Include closed tickets too — they're needed for dependency and logbook cross-references.
-
Read the logbook (docs/LOGBOOK.md or LOGBOOK.md) into context.
-
Run dimensions 1–4 across all tickets in batch. These are mostly mechanical and can be checked by reading the snapshot.
-
Run dimension 5 per ticket, prioritized as above. This is where most of the time goes. For each ticket, before opening files, write one sentence about what you're looking for — it keeps the investigation tight.
-
Run dimension 6 across active tickets, by Group. Use dimension 5's verdicts as input — a candidate cluster's silent-completion check is easier when you already know which closed tickets shipped what. Skip if the active set is too small to be worth it (say so in the methodology note).
-
Aggregate findings. Group by severity (critical / warning / notice) and by dimension. Note tickets where multiple dimensions agree (a ticket flagged for both content quality and codebase relevance is high-confidence; a ticket flagged for both codebase relevance and duplicate-of-closed is a near-certain silent completion).
-
Write the report to docs/audits/tickets-audit-YYYY-MM-DD.md using the template in references/report-template.md. Use the date the audit ran, not the date the repo was last touched.
-
Tell the user what was written and the top 3–5 recommended actions, ranked by confidence × impact. Don't auto-edit any tickets or run gh issue close — this skill produces a report, not changes. Closing tickets is a human decision.
Severity assignment
| Severity | Use for |
|---|
| Critical | Ticket is clearly silently-completed and still labeled as active work; ticket references a feature/module that no longer exists; dependency cycle; dangling dependency refs to deleted records; silent-completion duplicate (active ticket whose work shipped under a different closed ticket) |
| Warning | Format violations that block automated tools; stale active-status tickets (no commit/activity in >30 days); vague acceptance criteria on tickets queued for pickup; missing or duplicate primary-field IDs in Airtable; live duplicate (2+ active tickets covering the same work) |
| Notice | Minor format drift, missing Testing detail, inconclusive codebase-relevance verdicts |
Bias toward fewer high-severity items. A report with 40 "criticals" is unreadable. If you find yourself there, raise the bar.
Report output
Write to docs/audits/tickets-audit-YYYY-MM-DD.md. If the directory doesn't exist, create it. If a file with that date already exists, append -2, -3, etc. — never overwrite.
The full template is in references/report-template.md. The shape:
# Tickets Audit — YYYY-MM-DD
## Summary
[source, count, finding totals, top recommendations]
## Findings by severity
### Critical
### Warning
### Notice
## Findings by ticket
[per-ticket detail with evidence and recommended action]
## Non-discriminating dimensions
[dimensions where every ticket passed — useful to note for confidence]
## Out-of-scope observations
[anything noticed but not part of the audit dimensions — surface, don't act]
Working at scale
For repos with more than ~100 tickets or snapshots over 200KB, the audit cannot pull every ticket body into a single context. Adapt:
- Parse the snapshot once with a small script or
jq filter into a compact list (ID, title, status, label set, dependency refs only — no bodies).
- Dimensions 1–4 run from that compact list — no need to read every body.
- Dimension 5 reads the full body only for the prioritized subset you actually examine. Fetch each body individually.
- If you're delegating to a subagent, pass the path to the snapshot and the working directory — not the snapshot contents. Have the subagent do the slicing.
A 290-ticket Airtable dump is ~650KB on a single line. Don't try to inline it.
General principles
- Evidence over assertion. Every finding cites a file, line, commit, or ticket field. "This is stale" without a cite is not a finding.
- The audit doesn't fix anything. It writes a report. Closing tickets, editing bodies, or filing follow-ups is the human's call — they may have context the audit can't see.
- Codebase relevance is the hard part. Spend your time there. Dimensions 1–4 should feel mechanical.
- Inconclusive is a real answer. If the code doesn't tell you whether a ticket is done, say so and frame the question for the human. Don't guess.
- Stay in scope. If you notice problems in the code (bugs, dead code, security holes), don't file them as audit findings — surface them in the "Out-of-scope observations" section so the human can decide whether to spin off separate tickets.
- One report per run. Don't sprinkle findings into individual tickets as comments. The report is the artifact.