agentleFS
flopperjflopperj/dotfiles-core/.claude/skills/ticket-pickup/SKILL.md

ticket-pickup

Skill3 starsChanged 9 days agoMITView on GitHub

Fetch a Jira ticket, enrich it with codebase context, classify its complexity, and route it to the right pipeline depth. Use when the user types /ticket-pickup, starts a prompt with 'Pickup', or asks to 'pick up a ticket', 'grab the next bug', 'work on PROJ-1234', or 'what's my next ticket'. Single-ticket entry point for the agent pipeline.

The file

Show its rules and text

We split this skill into the rules it gives an agent

58 rules. About 0 are common, 32 are distinctive, and 26 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈32

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 26

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If no ticket can be resolved, ask the user for a ticket key.

    line 85
  2. Distinctive:

    Use the Atlassian MCP tools.

    line 102
  3. Very specific:

    Call getJiraIssue with the ticket key.

    line 106
  4. Very specific:

    If the fetched ticket's labels include exactly one repo:<name> label, the ticket is bound to a specific repository checkout (it is …

    line 131
  5. Distinctive:

    Autonomous mode: treat as a blocker — stop this ticket and surface the failure.

    line 156
  6. Distinctive:

    If the resolved path is outside the session's authorized working directories: warn that permission prompts are expected (gated mode) or …

    line 166
  7. Very specific:

    If the ticket carries multiple repo: labels: warn and ignore them all (ambiguous binding) — continue in the current repo.

    line 170
  8. Distinctive:

    After fetching the ticket, check if it's a parent ticket (Story or Epic) with children that should be swarmed instead of processed …

    line 180
  9. Very specific:

    Check the issuetype field from getJiraIssue.

    line 183
  10. Distinctive:

    If type is Story or Epic:

    line 184
  11. Distinctive:

    If children exist and at least 2 are in a workable status ("To Do", "Open", "Reopened"):

    line 195
  12. Distinctive:

    If no children exist (or all children are already Done/Closed):

    line 200
  13. Distinctive:

    Treat the Story/Epic as a regular ticket (it may be a standalone task mislabeled as a Story)

    line 201
  14. Very specific:

    If the Story needs sub-tickets created, use /create-jira-ticket which handles story points, Epic assignment, and description templates

    line 204
  15. Distinctive:

    In gated mode: Wait for user input.

    line 229
  16. Very specific:

    If the user picks swarm (or autonomous mode auto-selects it):

    line 235
  17. Very specific:

    Add a comment to the parent Story: 🤖 Starting on this Story's sub-tickets in parallel: {list}.

    line 248
  18. Distinctive:

    All Jira operations are best-effort.

    line 251
  19. Distinctive:

    After hierarchy detection (Step 2.5), check whether an open handoff exists for the branch associated with this ticket.

    line 257
  20. Distinctive:

    If the ticket-pickup branch resolution (Step 5.5) has already determined the branch (matched existing or about-to-create) → use that branch …

    line 263
  21. Distinctive:

    If no candidate branch resolves → skip Step 2.6 silently.

    line 265
  22. Very specific:

    Check ~/.claude/tasks/<project>/handoff/<task-key>.md.

    line 271
  23. Distinctive:

    If present, run a lightweight version of the read flow (same predicate evaluation, same partitioning as the handoff read half) and append …

    line 275
  24. Very specific:

    If N >= 3 (three or more holding rejections), nudge the complexity classification up one tier (Simple→Medium, Medium→Complex).

    line 290
  25. Distinctive:

    Component names: Search for classes, modules, or services mentioned in the ticket.

    line 305
  26. Very specific:

    Related PRs: If the ticket links to merged PRs, fetch the PR diff summaries via gh pr view <number> --json title,body,files.

    line 307
  27. Very specific:

    When a registry is present, run bash ~/.claude/skills/ticket-pickup/scripts/repo-registry.sh list.

    line 314
  28. Distinctive:

    If the registry helper fails (exit 2, 3, or 4), degrade to current-repo-only enrichment with a logged note — partial enrichment is better …

    line 327
  29. Distinctive:

    Follow CLAUDE.md error handling defaults.

    line 355
  30. Very specific:

    In autonomous mode: If any resolved repo is invalid (helper exit 2) or its path is outside the authorized working directories, treat it as …

    line 432
  31. Very specific:

    After the gate resolves or the notice prints, emit a multi_repo_split event.

    line 478
  32. Distinctive:

    Never block the pipeline on metrics.

    line 496
  33. Distinctive:

    All of these must be true:

    line 505
  34. Very specific:

    Only when Step 2 found a points field and the ticket has no value in it.

    line 530
  35. Very specific:

    In gated mode: Wait for user input. Choosing g, s, or e approves the assignee change shown, including taking the ticket from a teammate.

    line 587
  36. Very specific:

    After the classification gate resolves (user choice or autonomous auto-select), emit a ticket_classified event.

    line 596
  37. Distinctive:

    If emit fails, log and continue.

    line 606
  38. Distinctive:

    Before launching the pipeline, ensure the working branch is set up.

    line 612
  39. Very specific:

    Detect if already on a matching branch: If the current branch name contains the ticket key (e.g., branch PROJ-1234-fix-npe matches ticket …

    line 617
  40. Very specific:

    Check for a pre-existing branch: Run git branch --list "*{ticket-key}*".

    line 620
  41. Distinctive:

    Create a new branch: if no matching branch exists:

    line 623
  42. Very specific:

    Fallback: if newbranch is not available (non-dotfiles environment): detect the default branch manually via git symbolic-ref …

    line 638
  43. Very specific:

    When the downstream pipeline (Cyrus / Optimus+Cyrus / Aristotle+Optimus+Cyrus) returns, emit a pipeline_complete event before proceeding to …

    line 685
  44. Distinctive:

    If emit fails, log and continue.

    line 707
  45. Very specific:

    Add a Jira comment: 🤖 Looked into this one and the answer isn't a code change.

    line 721
  46. Distinctive:

    Leave the ticket status unchanged (it is already In Progress from Step 7, or from ticket-swarm Step 4.5 in swarm mode).

    line 724
  47. Distinctive:

    Do not transition to In Review.

    line 725
  48. Distinctive:

    Do not treat an advisory outcome as a failure, retry it, or escalate it through smart failure analysis.

    line 730
  49. Distinctive:

    Already that person, or the user asked for no change: do nothing.

    line 752
  50. Very specific:

    Unassigned: call editJiraIssue with fields={"assignee": {"accountId": "<id>"}}.

    line 753
  51. Very specific:

    Call getTransitionsForJiraIssue to find the "In Progress" transition ID.

    line 765
  52. Very specific:

    Call transitionJiraIssue with the transition ID.

    line 766
  53. Very specific:

    Add a comment: 🤖 Picked this one up.

    line 767
  54. Distinctive:

    Use the phrase matching the classification from Step 4.

    line 771
  55. Distinctive:

    Autonomous mode still gates on ambiguous classification: When in doubt, escalate complexity one level.

    line 788
  56. Distinctive:

    A picked-up ticket with no story points gets an estimate: when its issue type has a points field.

    line 796
  57. Distinctive:

    Fallback gracefully: If Jira is unavailable, if codebase search finds nothing, if enrichment is thin — still present what you have and let …

    line 798
  58. Distinctive:

    If you discover something during this task that would improve this skill, propose the change and ask me to confirm before saving it.

    line 808

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Audience for Jira comments

Jira comments posted by this skill are read by everyone on the ticket — PMs, designers, QA engineers, junior engineers, and the engineer who's actually doing the work. Write them in plain language. Avoid agent-internal vocabulary (no "Optimus", "Cyrus", "Ranger", "Scout", "pipeline", "team-lead", "sentinel", "max turns", "cap hit"). Describe what just happened and what comes next in concrete actions, not labels. Keep them short — these are Jira comments, not status pages.

Voice: write the assistant's comments in first person (e.g., "Picked this one up.", "Had to stop on this one.") — not third person ("Automated assistant has…"). The 🤖 prefix is the machine-marker; the first-person voice keeps the comments readable as a brief status update rather than a system notification. The 🤖 prefix on every comment is required by the canonical rule in ~/.claude/AGENTS.md § "Automated Comment Marker — 🤖 prefix" (applies to all agent-posted Jira and GitHub comments); the example strings below already carry it.

Ticket Pickup

Fetch a single Jira ticket, enrich it with codebase context, classify complexity, and route to the appropriate pipeline depth.

This skill is the entry point for ticket-driven work. It replaces manually copy-pasting ticket context into prompts.


Responsibility boundaries

Ticket Pickup is an orchestrator — it fetches, enriches, classifies, and routes. It never does any of the downstream agents' jobs.

<!-- BEGIN RESPONSIBILITY BOUNDARIES --><!-- EXTRA_ROWS: ticket-pickup -->
AgentSole responsibilityNEVER does
Ticket PickupFetch ticket, enrich with codebase context, classify complexity, route to pipelineWrite code, produce plans, perform strategic analysis, review PRs
AristotleStrategic analysis — assumptions, first principles, highest-leverage directionName file paths, produce code, plan execution, review PRs
OptimusExecution planning — file paths, step sequencing, risk assessment, architectureWrite code, make file edits, run commands, re-litigate upstream strategic decisions, review PRs
CyrusTDD implementation — write tests first, then code, hit 80%+ coverageRedesign architecture, question strategic direction, skip tests, review PRs
RangerStaff-level PR review — analyze diffs, score issues, report findingsWrite code, make edits, implement fixes, plan architecture, perform strategic analysis
ScoutPR review — analyze diffs, score issues, report findingsWrite code, make edits, implement fixes, plan architecture, perform strategic analysis
AuditorCode complexity analysis, reviewer routingWrite code, review code, post to GitHub, implement fixes
<!-- END RESPONSIBILITY BOUNDARIES -->

Execution modes

Ticket Pickup supports two execution modes, passed by the caller:

  • gated (default) — present classification to user, wait for approval at every pipeline transition. Used when invoked directly by the user.
  • autonomous — present classification to user for batch approval, then auto-approve all pipeline transitions except PR creation. Used when invoked by ticket-swarm.

Step 1: Resolve the ticket

Parse the user's input to determine the ticket:

  • Explicit ticket key (e.g., PROJ-1234) — use directly.
  • Branch name — extract ticket key from the current git branch (pattern: ABC-1234). Run git branch --show-current if needed.
  • "Pick my next bug" — query Jira for the user's highest-priority assigned bug:
    searchJiraIssuesUsingJql(
      jql='assignee = currentUser() AND type = Bug AND status in ("To Do", "Open") ORDER BY priority ASC, created ASC',
      maxResults=1
    )
  • JQL override — if the user provides a JQL query, execute it and pick the first result.

If no ticket can be resolved, ask the user for a ticket key. If no ticket exists yet, offer to create one via /create-jira-ticket.

Also resolve the assignee target from the user's request. Step 7 applies it.

  • Default: the user. Picking up a ticket means taking it.
  • A named person ("assign it to Alex", "for @alex"): that person.
  • No change ("don't assign it", "leave the assignee"): skip assignment entirely.

A caller-passed assignee (ticket-swarm hands one down) replaces parsing the request.


Step 2: Fetch ticket context

Use the Atlassian MCP tools. All Jira operations are best-effort — never block the pipeline on a Jira failure.

Primary: Atlassian MCP plugin

  1. Call getJiraIssue with the ticket key. Extract:
    • Summary, description, acceptance criteria
    • Comments (especially reproduction steps, stack traces, linked PRs)
    • Status, priority, labels, components
    • Current assignee (display name and account ID, or unassigned)
    • Linked issues (blockers, duplicates, related)
    • Attachments (note URLs for reference)
  2. Find the story points field. It is a custom field whose ID differs between Jira sites, so look it up by name: call getJiraIssueTypeMetaWithFields for the ticket's project and issue type and match the field named "Story Points" or "Story point estimate" (case-insensitive; prefer "Story Points" if both exist). Record its ID and read the ticket's current value for it. No match means the issue type has no points field (sub-tasks often don't); skip story points for this ticket.

For company-specific Jira fallback, consult ## Jira fallback in ~/.claude/overlay-context.md. If that file is absent, proceed with the primary tool only.

<!-- BEGIN OVERLAY-FRAGMENT: ticket-pickup-jira-fallback --><!-- END OVERLAY-FRAGMENT: ticket-pickup-jira-fallback -->

Step 2.4: Resolve repo label

If the fetched ticket's labels include exactly one repo:<name> label, the ticket is bound to a specific repository checkout (it is typically one half of an earlier repo split, picked up cold in a later session). Resolve the binding:

bash ~/.claude/skills/ticket-pickup/scripts/repo-registry.sh resolve <name>

Handle the result by exit code:

  • Exit 0 (absolute path printed): if the path differs from git rev-parse --show-toplevel, announce the binding — "This ticket is bound to <name> — running in /abs/path" — and use that path as the repo root for all subsequent git operations and for the Step 6 pipeline prompt (include Repo root: /abs/path). If it matches the current repo, continue silently.
  • Exit 1 or 3 (name not in registry / no registry): warn that the repo:<name> label cannot be resolved, naming the missing piece (no registry section vs. no entry for <name>), and continue in the current repo.
  • Exit 2 (entry invalid at resolution time — path missing, or not a git repo):
    • Gated mode: show the validation message and ask whether to continue in the current repo or stop.
    • Autonomous mode: treat as a blocker — stop this ticket and surface the failure.
  • Exit 4 (malformed registry entry — reachable even when the entry for <name> is valid, because any malformed line in the section aborts the parse; the registry needs a human fix):
    • Gated mode: show the parser's message and ask whether to continue in the current repo or stop.
    • Autonomous mode: treat as a blocker — stop this ticket and surface the failure.

If the resolved path is outside the session's authorized working directories: warn that permission prompts are expected (gated mode) or treat it as a blocker (autonomous mode).

If the ticket carries multiple repo: labels: warn and ignore them all (ambiguous binding) — continue in the current repo.

In swarm mode: Skip this step — ticket-swarm resolves the repo root before dispatching to ticket-pickup.


Step 2.5: Detect ticket hierarchy

After fetching the ticket, check if it's a parent ticket (Story or Epic) with children that should be swarmed instead of processed individually.

  1. Check the issuetype field from getJiraIssue.
  2. If type is Story or Epic:
    • Fetch sub-tasks via JQL:
      searchJiraIssuesUsingJql(
        jql='parent = {ticket-key}',
        fields=["summary", "description", "status", "priority", "issuetype", "components", "labels"],
        maxResults=50
      )
    • Also check for linked issues with "is parent of" or "has sub-task" link types in the ticket's linked issues.
  3. If children exist and at least 2 are in a workable status ("To Do", "Open", "Reopened"):
    • Skip Steps 3-7 (enrichment + classification for a container ticket is meaningless)
    • Present the story decomposition gate (below)
  4. If no children exist (or all children are already Done/Closed):
    • Treat the Story/Epic as a regular ticket (it may be a standalone task mislabeled as a Story)
    • Proceed with normal enrichment and classification at Step 3
    • If the Story needs sub-tickets created, use /create-jira-ticket which handles story points, Epic assignment, and description templates

Story decomposition gate

Story Detected: PROJ-1000 — Multi-account billing support

  Type: Story | Status: To Do | Priority: P2
  Sub-tickets: 5 found

  #  Ticket      Type      Status   Summary
  1  PROJ-1001   Sub-task  To Do    Add account selector to billing page
  2  PROJ-1002   Sub-task  To Do    Update billing API for multi-account
  3  PROJ-1003   Sub-task  To Do    Add billing_account_id to invoices table
  4  PROJ-1004   Sub-task  To Do    Write migration for existing accounts
  5  PROJ-1005   Sub-task  To Do    Update billing tests for multi-account

  -> swarm     = Swarm all sub-tickets (enrich, classify, parallel pipelines)
  -> swarm 1,2 = Swarm specific sub-tickets
  -> single N  = Pick up a single sub-ticket normally
  -> story     = Treat the Story itself as a single complex ticket
  -> x         = Cancel

In gated mode: Wait for user input.

In autonomous mode: Auto-select swarm for all workable sub-tickets.

Delegating to ticket-swarm

If the user picks swarm (or autonomous mode auto-selects it):

  1. Launch the ticket-swarm skill with:
    • child_tickets: the list of selected sub-ticket keys
    • parent_story_key: the Story/Epic ticket key
    • execution_mode: forward from caller (or default gated)
    • swarm_mode: forward from caller if present
    • unbounded: forward from caller if present
    • assignee: the assignee target from Step 1 (the user, a named person, or no change)
  2. ticket-swarm skips its JQL harvest (Step 1 Source A) and uses the provided child tickets directly.
  3. Transition the parent Story to "In Progress" if available.
  4. Add a comment to the parent Story: 🤖 Starting on this Story's sub-tickets in parallel: {list}. Each one will post its own updates as it goes.

All Jira operations are best-effort.


Step 2.6: Handoff context check

After hierarchy detection (Step 2.5), check whether an open handoff exists for the branch associated with this ticket.

Resolve the candidate branch

Two paths:

  1. If the ticket-pickup branch resolution (Step 5.5) has already determined the branch (matched existing or about-to-create) → use that branch name.
  2. Else, derive a candidate branch from the ticket key: git branch --list "*<ticket-key>*" — use the match if exactly one result.
  3. If no candidate branch resolves → skip Step 2.6 silently.

Check for a handoff artifact

Resolve <project> as basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)". Slug-safe the candidate branch (replace / with -) to form <task-key>.

Check ~/.claude/tasks/<project>/handoff/<task-key>.md. If absent → skip.

Fold into enrichment brief

If present, run a lightweight version of the read flow (same predicate evaluation, same partitioning as the handoff read half) and append to the enrichment bundle:

Open handoff for this branch:
  Last intent: <intent>
  Holding rejections (gate the pipeline): <N>
    [<kind>] <claim>
    ...
  Surfaced rejections (ground shifted, reconsider): <M>
    [<kind>] <claim>
    ...

Complexity nudge

If N >= 3 (three or more holding rejections), nudge the complexity classification up one tier (Simple→Medium, Medium→Complex). Add a one-line rationale: "Handoff has 3+ holding rejections — empirically this ticket is harder than its scope suggests."

This enrichment is non-blocking: if the handoff file is malformed or the read fails, log it and continue.


Step 3: Enrich with codebase context

Launch an explore subagent to search the codebase for references from the ticket:

  • Stack traces: Extract file paths, class names, line numbers from the ticket description and comments. Search the codebase to confirm they exist and pull surrounding context.
  • Error messages: Grep for exact error strings to find the origin.
  • Component names: Search for classes, modules, or services mentioned in the ticket.
  • Related PRs: If the ticket links to merged PRs, fetch the PR diff summaries via gh pr view <number> --json title,body,files.

Cross-repo resolution

For cross-repo resolution, consult ## Repo registry in ~/.claude/overlay-context.md. If that file or section is absent, search only the current repo.

When a registry is present, run bash ~/.claude/skills/ticket-pickup/scripts/repo-registry.sh list. For each ticket reference (file path, class/component name, exact error string) that was not found in the current repo, search each registered checkout — scope grep/glob by that entry's prefixes when present, whole repo otherwise — and record hits per repo.

In the brief's Codebase references found: list, qualify each non-current hit with the repo it resolved in, e.g. - [repo: other-repo] src/handlers/add_product.py — handler referenced in the stack trace. Current-repo hits stay unqualified — a single-repo ticket's brief is byte-identical with or without a registry.

If the registry helper fails (exit 2, 3, or 4), degrade to current-repo-only enrichment with a logged note — partial enrichment is better than no enrichment.

Compile the enrichment into a structured brief:

Ticket: PROJ-1234 — Campaign preview returns 404
Priority: P1 | Status: To Do | Components: campaigns, preview

Description:
  [Ticket description, trimmed to essentials]

Reproduction:
  [Steps from comments, if available]

Codebase references found:
  - app/controllers/CampaignPreviewController.php (line 142 — route handler)
  - app/lib/MC/Campaign/Preview.php (line 89 — render method)
  - tests_phpunit/MC/Campaign/PreviewTest.php (existing test coverage)

Related:
  - PROJ-1200 (duplicate, closed — previous fix reverted)
  - PR #4521 (merged — original preview implementation)

If gh or enrichment fails: Log the failure, continue with whatever context was gathered. Partial enrichment is better than no enrichment. Follow CLAUDE.md error handling defaults.


Step 3.5: Detect multi-repo scope

Decide whether this ticket spans more than one repository and, if so, gate a repo split into per-repo sub-tasks.

Skip conditions (checked first, in order)

  1. Swarm mode — skip this step entirely; splits are decided before swarm dispatch.
  2. The ticket already carries a repo: label — it IS one half of an earlier split; never re-split it.
  3. Step 2.5 fired — story decomposition takes precedence. If the ticket is a Story/Epic with workable children, Step 2.5 already skipped Steps 3-7, so this step is never reached; if both could ever apply, Step 2.5 wins.

Detection rule

Fire the gate only on resolution evidence: Step 3's cross-repo enrichment recorded at least one reference hit in a registered, non-current repo. Corroborating signals — ticket Components matching a registry entry's components=, repo names in the description, linked PRs in other repos — strengthen the evidence display but NEVER fire the gate on their own.

Notice path

Corroboration-only signals, detections none of which resolved in a registered non-current checkout, or signals with no registry take the notice path instead of the gate. Partial resolution still gates: once the detection rule fires on at least one resolved hit, remaining unresolved repos appear on the gate's Unresolved: line (and in the event's repos_unresolved) rather than forcing the notice path. Print one line, e.g.:

Note: this ticket also references other-repo — no local checkout registered; handling my-repo only. To enable cross-repo splits, consult `## Repo registry` in `~/.claude/overlay-context.md` and add the missing repo there.

Then emit the metrics event with mode: "notice" (see below) and continue to Step 4 unchanged.

Repo decomposition gate

Multi-repo scope detected: PROJ-1234 — Fix product image sync

  Current repo: my-repo (/Users/me/Development/my-repo)

  #  Repo         Path                              Evidence
  1  other-repo   /Users/me/Development/other-repo  2 file refs, component match
  2  (current)    my-repo                           3 file refs

  Unresolved: legacy-api — no registry entry (notice only)
  Proposed order (provider -> consumer): other-repo -> my-repo
  ! /Users/me/Development/other-repo is outside this session's working
    directories — expect permission prompts, or add it before confirming.

  -> split      = Create one sub-task per repo, blocks-linked, then swarm
  -> order 2,1  = Split with a different order
  -> only N     = Narrow to repo N (pick up normally there)
  -> single     = Treat as single-repo in the current checkout
  -> x          = Cancel

Proposed order: registry depends_on hints first (a repo that others depend on comes earlier), then cross-reference direction (the repo whose symbols the other references comes first), else current-repo-last. The ! warning line appears only when a resolved path is outside the session's authorized working directories.

In gated mode: Wait for user input.

In autonomous mode: If any resolved repo is invalid (helper exit 2) or its path is outside the authorized working directories, treat it as a blocker — stop this ticket and surface the failure. Otherwise auto-select split.

On split (or order)

  1. Build one sub-task spec per repo, in the confirmed order:
    • Summary: <parent summary> (<repo>)
    • Description: the parent description, plus that repo's tagged references from the Step 3 brief, plus one sentence stating the interface expectation toward its counterpart repo.
  2. Invoke create-jira-ticket's "Batch mode: repo-split sub-tasks" with the parent key, the project key, and the ordered {repo, summary, description} list (provider first — the list order is the final merge order).
  3. If batch mode clean-stops (any creation failed): surface its report exactly — which keys were created, which entries were not. Do NOT delegate to swarm. Do NOT report success.
  4. On success: delegate exactly like Step 2.5's "Delegating to ticket-swarm" block — child_tickets = the created keys, parent_story_key = the parent ticket key, execution_mode, swarm_mode, unbounded and assignee forwarded from the caller. Transition the parent to "In Progress" and add a plain-language parent comment naming the split and merge order, e.g.: 🤖 This ticket spans two repositories, so I split it into one sub-task per repo: PROJ-1235 (other-repo), PROJ-1236 (my-repo). Merge order: other-repo first, then my-repo. Each sub-task will post its own updates. All Jira operations are best-effort.

On only N

Continue this pickup normally, bound to repo N — reuse Step 2.4's repo-root binding mechanics (announce the path, use it as the repo root for all subsequent git operations and the Step 6 pipeline prompt).

On single

Continue unchanged — proceed to Step 4 as a single-repo ticket in the current checkout.

On x

Emit the multi_repo_split event below with gate_choice: "cancel", then stop.

Metrics emit (both paths)

After the gate resolves or the notice prints, emit a multi_repo_split event. See the metrics-emit library skill for the canonical event shape and emit instructions.

Data to capture:

  • mode: "gate" or "notice"
  • repos_detected, repos_resolved, repos_unresolved
  • gate_choice: "split" / "reorder" / "only" / "single" / "cancel" — null when mode is "notice". Menu mapping: the order N,M keypress emits "reorder"; x emits "cancel".
  • subtask_keys: created keys, in final merge order (empty array when no split happened)
  • proposed_order, final_order — final_order is empty when mode is "notice"; on "cancel"/"only"/"single" emit final_order = proposed_order (no reorder happened); it differs from proposed_order only on "reorder"

Emit via emit-metric.sh ... || true. If emit fails, log and continue. Never block the pipeline on metrics.


Step 4: Classify complexity

Evaluate the enriched ticket to determine pipeline depth:

Simple (Cyrus direct)

All of these must be true:

  • Single file or 2-3 closely related files affected
  • Clear stack trace or error message pointing to the root cause
  • Fix pattern is obvious (null check, off-by-one, typo, missing flag check)
  • No architectural implications
  • Existing test coverage exists to validate against

Medium (Optimus -> Cyrus)

Any of these:

  • 2-5 files across multiple directories
  • Needs a plan but the strategic direction is obvious
  • Requires new test files or significant test additions
  • Touches a well-understood pattern (controller + service + test)

Complex (Aristotle -> Optimus -> Cyrus)

Any of these:

  • Root cause is unclear or debatable
  • Multiple possible fix strategies with different trade-offs
  • Architectural implications (changes how systems interact)
  • Race conditions, concurrency issues, or timing-dependent bugs
  • Cross-service or cross-system boundaries
  • Previous fix attempts failed (linked duplicate/reverted tickets)

Story point estimate

Only when Step 2 found a points field and the ticket has no value in it. Estimate on the same Fibonacci scale /create-jira-ticket uses, 1, 2, 3, 5, 8, and never other values:

  • 1 pt: Trivial change — a single config update, copy change, or one-liner fix
  • 2 pts: Small, well-defined task — a single file change with minimal risk
  • 3 pts: Medium task — touches 2-3 files, straightforward implementation
  • 5 pts: Larger task — multiple files, some complexity or unknowns
  • 8 pts: Significant effort — cross-cutting changes, new patterns, or substantial unknowns

Base it on the enrichment from Step 3, not on the classification alone. As a sanity check, Simple tickets usually land at 1-2, Medium at 3-5, and Complex at 5-8. Never overwrite an existing value.


Step 5: Present and gate

Show the enriched ticket with classification:

Ticket Pickup: PROJ-1234

  Summary:    Campaign preview returns 404
  Priority:   P1
  Assignee:   Alex Kim -> you (reassigning)
  Points:     none -> 3 (estimated)
  Complexity: Medium
  Pipeline:   Optimus -> Cyrus (2-3 files, needs a plan)

  Codebase context:
    - CampaignPreviewController.php:142 — route handler
    - MC/Campaign/Preview.php:89 — render method
    - PreviewTest.php — existing coverage

  Enrichment:
    - Previous fix (PR #4521) was reverted in PROJ-1200
    - Stack trace points to null $campaign in render()

  -> g  = Go — launch the pipeline as classified
  -> s  = Simplify — I think this is simpler, route to Cyrus directly
  -> e  = Escalate — this is more complex, route through Aristotle
  -> k  = Keep the current assignee, then go
  -> p N = Use N story points instead (1, 2, 3, 5, or 8)
  -> c  = Add context (provide additional information)
  -> x  = Cancel

The Points line reads none -> N (estimated) when Step 7 will set an estimate and N (no change) when the ticket already has one. Omit it, and the p option, when the issue type has no points field.

The Assignee line shows the change Step 7 will make: Unassigned -> you, Alex Kim -> you (reassigning), or you (no change). With an explicit no-change request it reads Alex Kim (left as-is), and the k option is omitted.

In gated mode: Wait for user input. Choosing g, s, or e approves the assignee change shown, including taking the ticket from a teammate.

In autonomous mode: Log the classification and auto-select g unless the complexity is ambiguous (e.g., borderline Simple/Medium). If ambiguous, escalate one level rather than risk under-routing.

Metrics emit (after classification)

After the classification gate resolves (user choice or autonomous auto-select), emit a ticket_classified event. See the metrics-emit library skill for format and emit instructions.

Data to capture:

  • classification: the final classification (Simple/Medium/Complex)
  • pipeline: the pipeline path (cyrus / optimus-cyrus / aristotle-optimus-cyrus)
  • enrichment_sources: which sources produced enrichment (e.g. ["jira", "codebase", "gh"])
  • override: null if no override, "simplified" if user chose s, "escalated" if user chose e

If emit fails, log and continue. Never block the pipeline on metrics.


Step 5.5: Ensure branch exists

Before launching the pipeline, ensure the working branch is set up. This step handles branch creation automatically so the user never needs to manually create a feature branch.

  1. Check current branch: git branch --show-current
  2. Detect if already on a matching branch: If the current branch name contains the ticket key (e.g., branch PROJ-1234-fix-npe matches ticket PROJ-1234), use it as-is. Skip branch creation.
  3. Check for a pre-existing branch: Run git branch --list "*{ticket-key}*". If a matching branch exists but is not currently checked out, check it out and pull latest.
  4. Create a new branch if no matching branch exists:
    • Determine the branch name: {ticket-key}-{slug} (e.g., PROJ-1234-fix-campaign-preview). Slugify from the ticket summary.
    • If a repo root was bound earlier (Step 2.4 resolution or a Step 3.5 only N choice, and the path differs from the current working directory): do NOT use the newbranch alias — it is an optional user-overlay alias (from .aliases.local, not part of core) and is cwd-bound, so it would act on the wrong repo. Use the fallback commands below with every command run via git -C /abs/path.
    • Otherwise, use the newbranch shell alias if available (from .aliases.local): newbranch {ticket-key}-{slug}. This handles default branch detection (queries origin/HEAD, supports any branch name), checkout, git pull origin, and new branch creation in one command.
    • Fallback if newbranch is not available (non-dotfiles environment): detect the default branch manually via git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' (fall back to main then master via git show-ref), then: git checkout {default} && git pull origin {default} && git checkout -b {ticket-key}-{slug}

In swarm mode: Skip this step — ticket-swarm handles branch creation before dispatching to ticket-pickup.


Step 6: Launch pipeline

Based on classification and user choice:

Simple -> Cyrus direct: Launch the cyrus-tdd-engineer skill. Pass:

  • The enriched ticket brief (from Step 3)
  • Jira ticket key for commit messages
  • Execution mode (from caller or default gated)
  • swarm_mode flag if invoked by ticket-swarm
  • unbounded flag (forward from caller if present)

Medium -> Optimus -> Cyrus: Launch the optimus-planner skill. Pass:

  • The enriched ticket brief as the problem statement
  • Jira ticket key
  • Execution mode
  • swarm_mode flag if applicable
  • unbounded flag (forward from caller if present)

The Optimus skill handles its own gate and Cyrus handoff.

Complex -> Aristotle -> Optimus -> Cyrus: Launch the aristotle-deconstructor skill. Pass:

  • The enriched ticket brief as the problem to deconstruct
  • Jira ticket key
  • Execution mode
  • swarm_mode flag if applicable
  • unbounded flag (forward from caller if present)

The Aristotle skill handles its own gates and downstream handoffs.

CI fix loop: When Jenkins MCP is configured, Cyrus includes an autonomous CI failure diagnosis and fix loop after pushing code. Default behavior: Cyrus reports "Pushed. CI running" and stops; the fix loop triggers on the next interaction that reveals a failure. In swarm mode (swarm_mode: true), Cyrus polls Jenkins via mcp__jenkins-mcp__check_build and enters the fix loop automatically if the build fails (up to 3 iterations). The wait for any one build is capped at about 30 minutes; if CI is still pending then, Cyrus reports "CI still pending" with the build link and stops. See the Cyrus agent definition for details and guardrails.

Metrics emit (after pipeline completes)

When the downstream pipeline (Cyrus / Optimus+Cyrus / Aristotle+Optimus+Cyrus) returns, emit a pipeline_complete event before proceeding to Step 7.

Data to capture:

  • classification, pipeline: same as the ticket_classified event
  • duration_seconds: wall-clock seconds from pipeline launch to return
  • tests_passed: true/false from Cyrus's report
  • coverage_percent: coverage number from Cyrus's report (null if not reported)
  • ci_fix_attempts: number of CI fix iterations (0 if none)
  • first_pass: derived as tests_passed && ci_fix_attempts == 0, per the metrics-emit skill's canonical rule — additionally false when the pipeline needed a retry (ticket-pickup's better-information case, since it sees retries that predate CI and Cyrus's own report may not capture)
  • files_changed: count of files modified
  • outcome: implemented normally. advisory when the pipeline ended deliberately with no code change (see below); then tests_passed and first_pass are null, ci_fix_attempts is 0, files_changed is 0, and advisory_reason carries the one-line verdict. The Aristotle skill returns that verdict under the key verdict; the metrics event names it advisory_reason. Copy the value across — an event that carries verdict instead of advisory_reason fails schema validation.

If emit fails, log and continue.

Advisory outcome (pipeline ended with a correct no-code answer)

The Complex route can end without code and without failure: Aristotle's Phase 0 judges first principles the wrong tool and answers by analogy, or the Aristotelian Move states that no code changes are needed. The aristotle-deconstructor skill reports either back as an advisory outcome with a one-line verdict and the answer text.

This is the pipeline's third terminal outcome, alongside PR created and blocked. It is a success. Handle it as:

  1. Emit pipeline_complete with outcome: advisory as described above.
  2. Add a Jira comment: 🤖 Looked into this one and the answer isn't a code change. {verdict}. Full reasoning is in the pipeline output — leaving the ticket as-is for a human to decide what to do with it.
  3. Leave the ticket status unchanged (it is already In Progress from Step 7, or from ticket-swarm Step 4.5 in swarm mode). Do not transition to In Review; there is no PR to review.
  4. In swarm mode, return outcome: advisory and the verdict to the caller instead of a PR URL, so ticket-swarm counts it under Advisory rather than Blocked.

Do not treat an advisory outcome as a failure, retry it, or escalate it through smart failure analysis.

IMPORTANT: Outside swarm mode, immediately after launching the pipeline, proceed to Step 7 (Jira transition). Do not skip it.


Step 7: Jira status transition (MANDATORY)

In swarm mode: Skip this step — ticket-swarm Step 4.5 assigns, sets story points, transitions and comments, so a story-points change made at its triage gate is not overwritten.

You MUST transition the ticket to "In Progress" IMMEDIATELY after launching the pipeline. Do not defer this. Do not skip it.

  1. Assign the ticket to the assignee target from Step 1:
    • Resolve the account ID. For the user, call atlassianUserInfo. For a named person, call lookupJiraAccountId. If the lookup returns zero or several matches, ask which person in gated mode. In autonomous mode, skip assignment and log why.
    • Already that person, or the user asked for no change: do nothing.
    • Unassigned: call editJiraIssue with fields={"assignee": {"accountId": "<id>"}}.
    • Assigned to someone else: in gated mode, reassign as the Step 5 gate showed unless the user chose k. In autonomous mode, reassign only when the caller named the assignee. Otherwise leave the teammate in place and log the ticket key and current assignee so the summary can list it. Nobody approved taking that teammate's ticket.
  2. Set story points if Step 4 made an estimate: call editJiraIssue with fields={"<points-field-id>": <N>}, using the field ID from Step 2 and the value from the gate (the user's p N choice, else the estimate). Autonomous mode sets the estimate without asking. Skip this when the ticket already has points or has no points field.
  3. Call getTransitionsForJiraIssue to find the "In Progress" transition ID.
  4. Call transitionJiraIssue with the transition ID.
  5. Add a comment: 🤖 Picked this one up. {action_phrase} — where {action_phrase} is one of:
    • For Simple tickets: Going straight to implementing.
    • For Medium tickets: Planning the work first, then implementing.
    • For Complex tickets: Doing some strategic analysis first, then planning, then implementing. Use the phrase matching the classification from Step 4. If classification is uncertain, omit the action phrase entirely (the comment still reads sanely as "🤖 Picked this one up.").

Verify: Confirm the assignment, the story points, and the transition succeeded by checking the responses. If any fails (no permission to assign, points field not on the edit screen, no valid transition, Jira unavailable), log the failure but do not block the pipeline.

This step is not optional. The user expects ticket status to reflect pipeline activity in real time.


Gate rules

  • Ticket Pickup never writes code. It fetches, enriches, classifies, and routes. The downstream skill handles everything else.
  • Classification is a recommendation. The user can override at the gate.
  • Autonomous mode still gates on ambiguous classification. When in doubt, escalate complexity one level.
  • Each downstream pipeline runs as a subagent to keep context clean.
  • NEVER skip Jira transitions. They happen at pipeline launch (Step 7, or ticket-swarm Step 4.5 in swarm mode), not later. The user expects real-time status updates.
  • A picked-up ticket is assigned to the user unless they named someone else or asked for no change. Take a ticket from a teammate only when the user approved it at the gate or named the assignee.
  • A picked-up ticket with no story points gets an estimate when its issue type has a points field. Never overwrite existing points.
  • Fallback gracefully. If Jira is unavailable, if codebase search finds nothing, if enrichment is thin — still present what you have and let the user decide.
  • Three terminal outcomes, not two. PR created, advisory (correct no-code answer), blocked (something went wrong). Never file an advisory outcome as blocked; it pollutes the first-pass metric and tells a human something failed when it did not.

Maintenance

If you discover something during this task that would improve this skill, propose the change and ask me to confirm before saving it.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.