Use to open, advance, close, or report on the external tracker ticket (Jira, Notion, or Asana) that tracks a task. Triggers at task classification, at every state change in the work, at task close, and on "what did I ship this week" or "how long does this tier take".
Install
npx skillscat add nullvoidundefined/agent-governance/ticket-lifecycle Install via the SkillsCat registry.
Ticket Lifecycle
Open a ticket when a task is classified. Move it when the work moves. Close it with measured actuals. Read the history back for rollups and estimates.
Announce at start: "I'm using the ticket-lifecycle skill to [open|advance|close|report on] the tracker ticket for this work."
Design: docs/superpowers/specs/2026-09-17-ticket-lifecycle-design.md.
Who runs it
The main session runs every operation itself, as direct tracker MCP calls, never through a subagent (owner decision 2026-09-24, IAN-345, amending IAN-333: a subagent spent about 136k tokens opening one ticket, while direct calls cost a few thousand each). Load the tracker's tools with ToolSearch once, then make one call per write. After open, run task-tier.sh set <tier> "<reason>" --ticket <key> with the key the call returned. Only a report or an estimate over a long history, which reads many tickets, may go to a background subagent on sonnet.
Instance config
Read ~/.claude/TICKET-TRACKER.json before any operation. It names the active tracker, its MCP tool names, its container, and the canonical-state mapping. The template is ~/.claude/TICKET-TRACKER.template.json; the real file is gitignored.
Config absent: say once, in this turn, "No tracker is configured (~/.claude/TICKET-TRACKER.json is absent; copy TICKET-TRACKER.template.json and fill it in)." Then record the same field set in docs/session-handoff/session-handoff.md and continue the work. Do not repeat the warning per operation.
Never type a provider status name that is not in the config's states map. An unmapped canonical state stops the operation and asks.
Canonical states
| State | Enter when |
|---|---|
backlog |
Captured, not started. |
specced |
Spec written and accepted (complex, saga). |
planned |
Plan written and accepted (complex, saga). |
in-progress |
First slice opens (tdd.sh open), or first edit lands on a trivial task. |
in-review |
Branch pushed, review or PR open. |
blocked |
Waiting on an answer or an external dependency. |
done |
Squash-merged and task-cleanup reported clean. |
dropped |
Abandoned. |
Trivial and standard tiers go backlog to in-progress directly. blocked returns to the state it interrupted. done and dropped are terminal: reopening means a new ticket linking the old key. By default advance runs only at in-progress, in-review, blocked, done, and dropped; specced and planned are written only when the owner asks.
Canonical fields
| Field | Source |
|---|---|
title |
Imperative summary of the task. |
tier |
task-start classification (R-901). |
assist |
llm when Claude drove the implementation, human otherwise. |
model |
Routing decision (R-903). |
estimate_minutes |
The estimate after the R-906 division. From estimate <tier> below when history allows. |
human_estimate_minutes |
How long a senior engineer who knows the repository would take to do the task alone, with tests, set at open before any work starts and never revised afterwards. Write n/a for work that only exists because agents do the work (harness process, handoffs, agent guards). |
repo |
Repository name only. Never a local filesystem path (R-106). |
branch |
feat/<slug>, or the branch the work lands on. |
spec_link, plan_link |
Repo-relative doc paths, when the tier produced them. |
pr_link |
PR or review URL, when one exists. |
started_at, completed_at |
UTC ISO-8601. |
actual_minutes |
Attributable working minutes inside the sessions that worked the task. Never calendar elapsed time between open and close. |
rework_count |
Times a green slice went back to red, or a review sent the work back. |
estimate_ratio |
actual_minutes / estimate_minutes, computed at close. |
human_speedup |
human_estimate_minutes / actual_minutes, computed at close; n/a when the human estimate is n/a. |
Operation: open
Run at the end of task-start Step 1, after the tier is announced and before setup.
- Skip entirely for the trivial tier unless the user asks for a ticket. A typo fix does not earn a work item.
- Require
title,tier,assist,estimate_minutes,human_estimate_minutes, andrepo. Any missing: name the missing field and stop. - Search the tracker for an open ticket carrying this
branchvalue. One hit: report the key and stop, no second ticket. Several hits: ask which is live, open nothing. - Read
started_atfrom the## Session start (R-503)block thathooks/session-start.shinjects at every session start, compaction included, whenever the SessionStart payload carries a transcript path (Claude Code passes the real one; the Cursor adapter passes a synthetic one per conversation, so under Cursor the value is the first start's hook clock). Block summarized away: read the file itsRecord:line names,session-start.<session-id>in the session's project directory under the Claude home. Neither present: leavestarted_atempty and say so in the report. Never estimate it, never round it, and never substitute the current time. - Create the ticket in state
backlog, orin-progresswhen work starts in the same turn. Write every known field. Sanitize the body first: secrets to[REDACTED], PII to[PII], internal URLs to[INTERNAL_URL](R-104). - Record the key in task-start's ledger on the task's branch before any file edit:
bash ~/.claude/skills/task-start/scripts/task-tier.sh set <tier> "<reason>" --ticket <ticket-key>.ticket-at-start-gate.shdenies edits and commits until it is there, so writing the spec or handoff first would be refused. - Report the ticket key and URL. Write the key onto the spec's
**Ticket:**line when a spec exists, and into the handoff doc. - Add
Refs: <ticket-key>as a trailer on every commit for this task.
Operation: advance
Run at each of those events, in the same turn as the event.
- Resolve the ticket key from the spec, the user story, the handoff doc, or the last
Refs:trailer on the branch. No key found: say so and offeropen. - Map the target canonical state through the config. Unmapped: stop and ask.
- Write the status change.
- Write a comment:
<from> -> <to> at <UTC ISO-8601>, plus the reason onblockedanddropped. - Increment
rework_countwhen the transition isin-reviewback toin-progress. - Status written and comment failed: report the ticket as advanced with the audit trail incomplete, naming the missed comment. Do not report plain success.
Operation: close
Run inside task-cleanup Step 2, after the verification gate and the merge decision.
- Refuse while tests, build, or lint are not green (R-509). A
doneticket asserts the work shipped. - Compute
actual_minutesfrom the R-503 start timestamp and the working time in any prior session recorded on the ticket. Exclude wall-clock gaps where nothing was running. - Compute
estimate_ratioasactual_minutes / estimate_minutes, andhuman_speedupashuman_estimate_minutes / actual_minutes. - Write
completed_at,actual_minutes,rework_count,estimate_ratio,human_speedup,pr_link, and thedonestatus in one update. Never leave adoneticket with actuals missing. - Report the ratio in the
task-cleanuptable, and state the recalibration R-906 asks for: which direction the tier's estimate moves next time.
Operation: report
Invoked as report <day|week|month> <range>; "what did I ship this week" means report day over the current week.
- Query tickets whose
completed_atfalls in the range. - Bucket by the requested granularity.
- Per bucket: ticket count, summed
actual_minutes, count by tier, medianestimate_ratio, and medianhuman_speedupover the tickets that have one. - Count tickets with no
completed_atseparately as open. Never fold them into a bucket. - Output one table, newest bucket last, and one line naming the largest single contributor to the total.
Operation: estimate
Invoked as estimate <tier>, and by task-start when it needs estimate_minutes.
- Query closed tickets matching
tierandassist. - Fewer than five samples: return no number from history. Say the sample is too small (
n=<count>), fall back to the R-906 heuristic, and label it a heuristic. - Five or more: return the median and the 80th percentile of
actual_minutes, withn. Recommend the median for a task that resembles the sample and the 80th percentile for one with an unknown dependency. - State the sample's date range. An estimate drawn from work older than a quarter is stale; say so.
Provider mapping
The config carries the tool names. The capability each operation needs, per tracker:
| Capability | Jira | Notion | Asana |
|---|---|---|---|
| Container | Project key | Data source ID of the ticket database | Project GID |
| Create | Create-issue tool | notion-create-pages |
Create-task tool |
| Update fields and status | Update-issue tool (status via transition) | notion-update-page (status is a select property) |
Update-task tool (status via section or completed) |
| Comment | Add-comment tool | notion-create-comment |
Add-comment or story tool |
| Search by branch or field | JQL search tool | notion-query-data-sources |
Search-tasks tool |
| Read one ticket | Get-issue tool | notion-fetch |
Get-task tool |
Tracker missing a canonical field (a Notion database without an estimate_minutes property, for instance): name every missing field and the type each needs, write the fields that do exist, and stop. Creating properties in the user's database is a schema change and waits for them.
Confirmation posture
Every write here is a create, update, save, add, or comment MCP call. R-105 was narrowed on 2026-09-17 so the write class passes without a prompt on the Linear server alone, while a Notion, Jira, or Asana write still asks, which is what makes advancing a ticket at every state change affordable; hooks/mcp-action-guard.sh still asks on a tracker call that lands code, submits for review, uploads, or applies, on anything the destroy or transmit classes match (delete_comment, retire_issue_label, share_issue), and on every other server. Cursor reaches the same posture by a longer route: its adapter rewrites a bare MCP tool name to mcp__cursor__<tool> before the hook runs, so the server identity is gone, and the guard resolves that synthetic segment against the active tracker's own tools map in ~/.claude/TICKET-TRACKER.json. A bare name listed there is treated as that tracker's call and follows the same narrowing; a name that is not listed keeps the synthetic segment and still asks. When it does ask, one call per confirmation: never batch several writes behind one prompt. A denial is a decision: do not re-ask for the same write in the same turn, and note in the close report that the ticket is behind the work. Never suppress the guard and never ask to bypass it (R-203).
A tracker failure (server down, auth expired, denial) never blocks the engineering work. Report the failed call, record the intended field set in the handoff doc, retry at the next lifecycle event.
Common Mistakes
- Opening a second ticket for a branch that already has one. Search first, always.
- Writing
donewith the actuals missing. One update carries both. - Recording calendar elapsed time as
actual_minutes. A ticket opened Monday and closed Friday is not four days of work, and one such row distorts every estimate drawn from that tier. - Estimating from a sample of two and reporting a number as if it came from history.
- Mixing
assistvalues in one sample. LLM-driven and hand-written work are not comparable. - Guessing a provider status name when the config has no mapping for the canonical state.
- Putting a local filesystem path, a client name, or a secret in a ticket body.
- Leaving the ticket in
in-progressand the key nowhere in the repo. The handoff doc carries it, or the next session cannot find it.
Integration
- Called by: task-start (
open,estimate), feature-create (advanceat the canonical events above), task-cleanup (close), the user directly (report,estimate); every call is a direct tracker call from the main session (R-605) - Composes with: tdd-gated-dispatch (a slice opening is the
in-progressevent), superpowers:finishing-a-development-branch (the merge is thedoneevent) - Rules: R-605 (a ticket per task above trivial), R-606 (actuals at close), R-901 (tier), R-903 (model), R-906 (estimate recalibration), R-105 (confirmation per write, except the Linear server's write class), R-106 (nothing client-identifying in this repo)