| name | ticket |
| description | Turn in-context work into one OR MORE well-framed tickets. A background agent analyzes the scope and drafts PREMISE-FIRST issues (problem, intent/why, constraints, non-goals, acceptance signals — NOT an algorithm), you calibrate each via questions, then it posts to Linear as a sub-issue of the current ticket (inheriting labels/assignee/project/team/cycle) or a new issue — chosen from context. Triggers: "file a ticket", "make tickets", "open an issue for this", "ticket this up", "capture this as a ticket", "split this into tickets". |
| version | 1 |
| tier | lightweight |
| args | [<what to ticket / scope descriptor>] [-- <intent / framing override>] |
Translate a body of in-context work into one or more well-framed tickets. A background agent analyzes the scope, drafts PREMISE-FIRST tickets (the problem, the intent, the constraints, the non-goals, the acceptance signals — deliberately NOT a prescribed algorithm or solution), you calibrate each draft via questions, then it posts to the tracker. Sessionless: no session dir, no phases, no debrief — analyze → draft → calibrate → post, then stop. A building block: it drafts and files tickets from work; it never writes code, fixes bugs, or commits. When this run follows a /build or /experiment, it reuses that run's <slug> so the tickets cluster beside that work's report, and it feeds LESSONS.md so the tickets you file shape the next handoff.
This is distinct from just calling the tracker MCP directly. It front-loads a premise-first draft derived from actual work (so the ticket captures why and what, not a half-baked solution), runs an interactive per-ticket calibration gate (you own title, scope, labels, and placement), and makes the sub-issue-vs-new decision from context — inheriting the parent's metadata when it files under an existing ticket.
Tracker Note (Linear):
This project files to Linear, via the linear-server MCP tools (list_teams, list_issue_labels, list_issue_statuses, get_issue, list_issues, save_issue) — the tracker and its tool set are constant; only the issue-key prefix and team vary per project and come from CLAUDE.md § Tracker (resolve them in §1, inject into the subagent prompt). Tickets use that prefix — <PREFIX>-NNNN (finch: FIN, so keys look like FIN-3141). save_issue both creates and updates. On create, it takes human names/identifiers, not UUIDs — team, parentId (accepts "<PREFIX>-NNNN" directly), labels (label NAMES), assignee (name/email/"me"), project, cycle, state, priority (int), title, description (Markdown). There is no separate ID-resolution step. Load tool schemas on demand (ToolSearch linear). If no Linear MCP is connected, degrade gracefully: draft and calibrate as usual, then hand the user the finalized ticket bodies in chat to copy-paste, noting that posting was skipped.
/ticket Protocol
1. Scope & Goal
Establish the two anchors the run depends on: what work the ticket(s) come from, and the intent behind it.
Resolve the tracker config (do this first): Read CLAUDE.md's ## Tracker block (the orchestrator sees CLAUDE.md; the subagent will NOT, so you must resolve here and inject in §2). Resolve: the issue-key prefix — <PREFIX> uppercase for keys (<PREFIX>-NNNN) and its lowercase form for branches (<prefix>-NNNN-…) — and the team. Finch's block gives prefix FIN / team Finchclaims. Fallback — no ## Tracker block (unconfigured project): keep today's behavior — detect a FIN-style key (an uppercase-alpha prefix + -NNNN) from the branch/slug/conversation at lower confidence. The config is never a hard requirement; absent it, degrade to detection, don't error.
Scope — the body of work to turn into ticket(s). Resolve from args, else infer and confirm:
- (bare) / a descriptor → the active session's log/plan/
DIALOGUE.md (the current chunk / what we just did), else the recent conversation.
diff → the uncommitted working-tree diff (git status + git diff) — ticket the change or the follow-ups it implies.
session → the work described in the active session's artifacts.
- text after
-- → an intent/framing override (what these tickets are really for).
If scope is ambiguous, present the candidates via AskUserQuestion.
Detect the context ticket (drives placement in §3): Scan for a parent key <PREFIX>-NNNN (prefix from § Tracker) in the session slug (finch example, prefix=FIN: 2026_07_02_FIN_2737_... → FIN-2737), the git branch (lowercase prefix: <prefix>-2712-... → <PREFIX>-2712), or the conversation. Call get_issue now to resolve enough to display the parent's title · state · team at placement (and to read its labels/project/cycle for inheritance in §4). Note the confidence: a branch-derived key that contradicts a slug-derived key, or a parent that is Done/Canceled/archived, is a low-confidence CANDIDATE, not a default — §3 forces a pick in that case.
Resolve the trail (used in §2/§5): <trailDir> = <sessionDir>/builds/. Pick a <slug> once — a short kebab-case string of the scope (e.g., identity-model-fix, recap-flat-followups). If this run follows a /build or /experiment, reuse that run's slug so the tickets sit beside its report. Otherwise, before minting a fresh slug, ls <trailDir> — if an existing <slug>_*.md clearly matches this work (same chunk / ticket / topic), REUSE that slug so the trail clusters under one name; only mint a new one for genuinely new work.
Echo back in one line: Ticketing <scope> — intent: <intent>; context ticket: <<PREFIX>-NNNN "title" · state · team | none>; trail: <trailDir>/<slug>_TICKETS.md.
2. Draft — Spawn the Drafting Agent (Background)
Backgroundable & parallelizable. This sub-agent dispatch is a composable building block: it can run in the background (run_in_background: true) so the orchestrator keeps working while it runs, and when the work splits into independent chunks, several such sub-agents can be fanned out in parallel and reconciled.
Before dispatching — §CMD_LOG_SKILL_INVOCATION: log this dispatch to the session log (why + context-pack pointer + one-line re-tread) so a restarted session can re-tread it. Fire it as the last step before the Task/Agent handoff.
Spawn one background agent (a general-purpose/analyzer) to analyze the scope and draft the ticket(s). It does the heavy reading and framing; you keep the thread for calibration. Build its prompt so it is entirely self-contained. Use the exact prompt structure below:
System Prompt for Drafter:
You are drafting one or more premise-first tickets from a body of work. Your goal is to orient the future builder to the PROBLEM and its terrain, not to provide a line-by-line map. Point at the general areas / modules involved (dirs, file-groups — never an exhaustive path/line dump, which rots immediately), define "done" by observable outcome, and name the traps you already see. You cannot know everything up front and shouldn't pretend to — the implementor discovers the specifics. Your job is to bound the problem sharply and flag the landmines. Analyze the scope; do NOT write code or file anything.
Inputs:
- Tracker config (resolved from CLAUDE.md § Tracker — the orchestrator fills these; you cannot read CLAUDE.md): Linear (linear-server MCP) · issue-key prefix
<PREFIX> (keys <PREFIX>-NNNN, branches <prefix>-NNNN) · team <team>. Use this prefix wherever a key/placement appears; do NOT assume FIN.
- Intent (what this work is for):
<intent>
- Scope to analyze:
<scope + concrete pointers: session artifacts / git diff cmd / files / conversation digest>
- Read for context:
<session log / plan / build report / relevant modules>
Rules of Engagement:
- Decompose conservatively (SINGLE ticket by default). Draft ONE coherent ticket unless the work has genuinely independent deliverables. Split heuristic: split only when the work touches two decoupled systems that are deployable/assignable independently (e.g., a schema migration AND a separate frontend surface that ship on their own cadence) — otherwise ONE ticket, one premise. Never inflate the count; the user merges/splits at calibration.
- PREMISE-FIRST, not a solution. Each ticket states the PROBLEM and its BOUNDARIES, not how to code it. Fill the template fields (
assets/TEMPLATE_TICKET_DRAFT.md) meticulously; the terse roster and its load-bearing nuances: title (crisp, outcome-oriented) · plain-terms — ONE sentence, and it becomes the ticket's first line: what this is about in language someone outside the project understands, with zero coined vocabulary and no internal nouns. It is a comprehension gate, not a summary: if you cannot write it without project jargon, you do not yet understand the ticket well enough to file it, and the fix is to go understand it — not to reach for the jargon. A reader who has never seen this project should get the gist from this line alone · (bug/feature/chore/tech-debt/spike — maps to a label, drives the rest) · (grounded in the work) · · — (general areas/modules + observed symptom + provenance; a representative pointer or two, NOT an exhaustive dump that rots) · · · (observable outcomes, not an algorithm) · — : the one concrete action that starts the work, in a single line (the file or module to open first, the query or command to run, the question to go answer). This does not violate premise-not-algorithm and must not be allowed to drift into it: a first step says , an algorithm says . One line, never a sequence — if you are writing "then", you have crossed into prescribing. , where it is the first unknown to attack; a research ticket with open questions and no named starting point strands whoever picks it up · (conceptual "you'll be tempted to X, don't, because Y" — not a path dump) · — rather than guessing · (bugs only) · (incl. a split not taken) · (by DRAFT #: //) · — labels are — the drafter doesn't know the team yet · . Do NOT prescribe an implementation/algorithm; if the work implies one, note it as a brief optional "possible direction" at most (dropped from the filed ticket unless the user explicitly keeps it).
Prefer background execution (run_in_background: true) so the user can keep working; relay the summary when it lands. For a tiny, obvious single-ticket scope, you MAY draft inline instead of spawning — state this in one line if you do.
3. Calibrate — Walk the Drafts with the User
The interactive calibration gate is the core of /ticket — the user, not the model, finalizes each ticket before anything is posted. Run it explicitly; never post a draft as-is. This is §CMD_WALK_THROUGH_RESULTS.
-
Granularity Gate (One AskUserQuestion): "How do you want to walk the N draft ticket(s)?"
- Each (one at a time)
- Groups (batches of ≤4)
- Accept all as drafted (skip straight to the post gate).
Note: For a single ticket, skip this step and go straight to its calibration.
-
Per Ticket (or group of ≤4) — Context Block + AskUserQuestion:
First, present a 2-part context block in chat (§FMT_CONTEXT_BLOCK):
- Line 1:
[#]: <title> — proposed placement
- Line 2: The premise + the acceptance signals + suggested priority/labels.
Then, use
AskUserQuestion (header = ticket number) with this option tree:
- Approve: Take the draft as-is into the post set.
- Edit: Adjust title / premise / scope / acceptance / priority / labels. Capture the change, re-show the finalized block.
- Split: This should be N tickets; define the split (each gets its own premise).
- Merge: Fold into another draft (name which).
- Drop: Don't file this one.
-
Placement Per Ticket: For each ticket kept, confirm placement (this is the one decision /ticket makes from context, but the user always confirms it explicitly). Echo the resolved parent (<PREFIX>-NNNN "title" · state · team, from the §1 get_issue) so the decision shows what is being nested under:
- Caller-pinned placement — check this FIRST (a calling skill that owns its own filing model): when
/ticket is invoked by another skill that dictates where the issue must land — e.g. an inbox-grooming pass graduating a signal into a specific work-type milestone — that placement is authoritative and skips parent detection entirely. The caller passes project + milestone + "new issue, never a sub-issue"; echo it and confirm it like any other placement, but do not offer a detected parent as the default. The reason it must skip rather than merely outrank detection: the parent that would be detected there is the source artifact (a frozen collector ticket the signal arrived on), and nesting under it is forbidden by the caller's own model — the grooming skill parks such sub-issues straight back out again. This is the one branch where detection is bypassed rather than user-overridden.
After the walk, assemble the post set: each finalized ticket with its resolved placement + fields. If nothing survives, report that and stop.
4. Post — To Linear (Single Batch Confirm)
Reconcile, dedup, confirm once, then create — and wire dependencies in a second pass. save_issue takes human names/identifiers, not UUIDs; there is no ID-resolution dance.
-
Reconcile + Dedup (the team is now known from §3 placement):
- Labels: Pull the real team label set (
list_issue_labels, team-scoped) and reconcile each ticket's SUGGESTED labels against it. Keep matches; drop or flag any label not in the set (surface the drop so the user can create it or pick another). The drafter never knew the team, so this is where suggested labels become real ones.
- Dedup: Before building the create calls, run
list_issues (query = the draft's title keywords, scoped to the target team) for each draft. If a plausible existing match turns up, hold it — you will surface it at the batch confirm (step 3) as "possible duplicate of <PREFIX>-NNNN".
-
Build one save_issue per ticket — the real field shape:
save_issue({ team, parentId: "<PREFIX>-NNNN", title, description, priority, state, labels: [names], assignee, project, milestone, cycle })
milestone (a project milestone — NAME resolves directly, or its UUID): set it when a caller pinned one (§3's caller-pinned branch) or when the target project uses milestones as its routing model rather than as release markers. Omit otherwise. If a name is ambiguous or you are unsure it exists, run list_milestones on the project first — do not guess, and do not silently drop the field, because a project whose whole triage model is milestones will file the issue into nowhere visible.
- Sub-issue: Set
parentId: "<PREFIX>-NNNN" (the resolved-prefix identifier directly — no ID lookup) + the inherited team / labels / assignee / project / cycle (read off the parent via get_issue, used ONLY to copy those values), with the per-ticket title, description, and priority.
- New top-level: Set
team + the calibrated fields. Pass state: "backlog" — prefer the state TYPE (robust if a team renamed its backlog status) so the issue actually lands in Backlog rather than the team default (which is Triage when enabled); if unsure the team has one, verify via .
5. Report
Summarize in chat: what was filed (each <PREFIX>-NNNN + URL + placement), what was dropped/merged, and any create that failed. Link the drafts trail (§CMD_LINK_FILE).
Paper Trail: Update <trailDir>/<slug>_TICKETS.md — mark each draft with its outcome (→ filed <PREFIX>-NNNN <url> / merged into # / dropped). Append across the run so a killed/resumed run keeps the record.
Feed the Ledger (Compounding memory, same as /build+/scrutinize): Append the durable outcome to <trailDir>/LESSONS.md — one terse bullet: what was filed, the identifiers, and any framing decision worth carrying (e.g., "Split the cutover into <PREFIX>-A identity + <PREFIX>-B reader-swap; <PREFIX>-B blocked on <PREFIX>-A"). Use engine log. The next /build//scrutinize reads these, so the tickets you filed shape the next handoff. Do NOT commit anything.
Constraints
- Premise, not algorithm. Every ticket states the problem + boundaries + acceptance, never a prescribed implementation. If the work implies a solution, it's an optional one-liner at most — the ticket must survive a different approach.
- Single by default. Draft one coherent ticket unless the work genuinely splits (two decoupled, independently deployable/assignable systems); the user raises the count, the skill never inflates it.
- The user owns every ticket. The drafter proposes; title, scope, labels, placement, and the decision to file are all the user's via
AskUserQuestion. Never post an un-calibrated draft.
- Placement is context-driven, always confirmable. A high-confidence parent (open, same team) → sub-issue inheriting labels+assignee+team+project+cycle; a low-confidence one → an explicit candidate pick, never a silent default; no parent → new issue posted with
state: "backlog" (the baseline is enforced, not assumed — omitting state lets Linear fall to the team default), offering inferred labels/project. Placement is never swept up by "approve all"; the user confirms it explicitly and overrides either way.
- One batch confirm before posting. Posting is outward-facing and hard to undo — a single explicit confirm of the whole set is mandatory; there is no auto-post.
- Plain terms first, jargon never. Every body opens with the one-sentence
plain-terms line before any heading, written for someone outside the project: no coined vocabulary, no internal nouns, and skills/tools named concretely (/ticket, not "the ticket-filing skill" — a periphrasis is more cryptic, not more portable). A ticket that only its author can parse is not filed, it is buried.
/ticket writes the FIRST body; it does not own later edits. Material changes to a filed ticket's description go through /snapshot's drift flow — an evergreen rewrite carrying every prior ## Change history entry verbatim plus one new dated line, with the current description re-read immediately before the full-field replace. Do NOT freehand-overwrite a description, and do NOT stack correction blocks inside the body: a body that records its own edit history forces every future reader to reconstruct what is currently true. Routine progress is a comment, not a rewrite.
- Paper trail always. The drafts + outcomes persist to
<trailDir>/<slug>_TICKETS.md and feed LESSONS.md, mirroring /build+/scrutinize.