---
description: Generate a prioritized master ticket plan from Linear — fetches all active tickets, resolves blocking relationships, and outputs an actionable backlog sorted by priority
version: 1.0.0
level: advanced
debug: false
category: workflow
tags:
  - linear
  - planning
  - tickets
  - backlog
author: OmniClaude Team
args:
  - name: team
    description: "Linear team name to query (default: Omninode)"
    required: false
  - name: --sync
    description: "Sync MASTER_TICKET_PLAN.md with current Linear state. Reproduces former ticket-plan-sync behavior."
    required: false
  - name: --mode
    description: "Sync mode: 'full' (regenerate entire document) or 'patch' (targeted update of specific sections). Only used with --sync."
    required: false
  - name: --output-path
    description: "Custom output path for the synced plan document. Only used with --sync."
    required: false
  - name: --dry-run
    description: "Preview changes without writing. Only used with --sync."
    required: false
mode: full
---

# Ticket Plan

Fetches all active Linear tickets, resolves blocking relationships, and generates a prioritized
master plan grouped into Available Now / Blocked / In Review.

**Announce at start:** "I'm using the ticket-plan skill to generate a prioritized backlog."

## Usage

```
/ticket-plan
/ticket-plan Engineering
```

## Output

Prints a markdown table to the screen grouped by:

1. **Available Now** — no active blockers, sorted by priority
2. **Blocked** — has unresolved blockers, sorted by priority
3. **In Review** — pending merge/review

## Priority Legend

| Value | Label |
|-------|-------|
| 1 | URGENT |
| 2 | HIGH |
| 3 | NORMAL |
| 4 | LOW |
| 0 / null | NONE |

---

## Sync Mode (`--sync`)

<!-- Absorbed from ticket-plan-sync -->

When `--sync` is passed, the skill syncs MASTER_TICKET_PLAN.md with current Linear state
instead of generating a new ticket planning template. This reproduces the former
`ticket-plan-sync` skill behavior.

**Announce at start:** "I'm using the ticket-plan skill (sync mode) to update the tracking doc."

**Imports:** `@_lib/contracts/helpers.md`

**Target file:** `docs/tracking/MASTER_TICKET_PLAN.md`

### Quick Start (sync)

```
/ticket-plan --sync                       # auto-select mode
/ticket-plan --sync --mode full           # full regeneration
/ticket-plan --sync --mode patch          # update changed rows only
/ticket-plan --sync --dry-run             # preview changes
```

### Mode Selection

#### Auto-select logic

```python
def select_mode(plan_path):
    if not plan_path.exists():
        return "full"

    mtime = plan_path.stat().st_mtime
    age_days = (time.time() - mtime) / 86400

    if age_days > 7:
        return "full"   # file is stale — full refresh
    return "patch"      # file is recent — targeted update
```

Override with `--mode full` or `--mode patch`.

### Full Mode

Complete regeneration from Linear state.

#### 1. Fetch all epics

```
tracker.list_issues(
  state="not done",
  limit=250
)
```

Filter to epic-level tickets (tickets with children OR tickets explicitly marked as epics).
Also include epics with `state="Done"` that have In-Progress children (epic closed but
children still open -- these are data quality issues to surface, not hide).

#### 2. Fetch children for each epic

```
tracker.list_issues(
  parentId=epic_id,
  limit=50
)
```

Also fetch orphaned non-done tickets (no parentId) to list at the bottom.

#### 3. Map to EpicContract

For each epic, build:

```yaml
id: "OMN-XXXX"
title: "..."
emoji: "{from title prefix or label, else 🔵}"
status: "{Backlog|In Progress}"
priority: "{High|Medium|Low}"
scope: "{first sentence of description}"
repos: ["{from label or title prefix}"]
children: ["OMN-YYYY", "OMN-ZZZZ", ...]
```

**Priority sort order for the summary table:**
1. In Progress, High priority
2. In Progress, Medium priority
3. Backlog, High priority
4. Backlog, Medium/Low priority

#### 4. Build MASTER_TICKET_PLAN.md

Emit the file in canonical format (see **File Format** below).

#### 5. Write atomically

Write to a temp file, verify it's valid markdown with expected sections, then rename.
Never overwrite the target with a partially-written file.

### Patch Mode

Targeted update -- only update rows that changed in Linear since the file was last written.

#### 1. Read current file

Parse MASTER_TICKET_PLAN.md to extract all `OMN-XXXX` ticket IDs mentioned.

#### 2. Fetch only those tickets from Linear

```
For each ticket_id in parsed_ids:
    tracker.get_issue(id=ticket_id)
```

Batch in groups of 20 to avoid rate limits.

#### 3. Diff states

Compare `current_state_in_file` vs `live_state_from_linear`:

```python
def rows_needing_update(file_tickets, linear_tickets):
    changed = []
    for ticket_id, file_state in file_tickets.items():
        live_state = linear_tickets.get(ticket_id)
        if live_state and live_state.state != file_state:
            changed.append((ticket_id, file_state, live_state.state))
    return changed
```

#### 4. Apply in-place patches

For each changed row, update the `| Status |` cell in the markdown table using
find-and-replace on the exact row pattern:

```python
import re

def patch_row(content, ticket_id, new_state):
    # Match: | Old Status | OMN-XXXX | ...
    pattern = rf'\| [^\|]+ \| {re.escape(ticket_id)} \|'
    def replace(m):
        row = m.group(0)
        # Replace first cell (status) with new_state
        return re.sub(r'\| [^\|]+ \|', f'| {new_state} |', row, count=1)
    return re.sub(pattern, replace, content)
```

If a ticket no longer exists in any table row (it may have moved epics), skip silently
and note it in the patch summary.

#### 5. Check for new epics

If any epic IDs are in Linear but not in the file, append them to the appropriate section.
If any epic IDs are in the file but not in Linear (deleted), mark them as `(removed)` in
the summary but do NOT remove from file -- flag for human review.

#### 6. Update header

Update the `Last updated: YYYY-MM-DD` line at the top of the file.

### File Format

The canonical MASTER_TICKET_PLAN.md format. Deviate from this and the patch parser breaks.

```markdown
## Open Epics (priority order)

Only includes work that remains to be done (Backlog or In Progress). No Done tickets.
Last updated: YYYY-MM-DD

| Status | Epic | Title | Scope | Repos |
|--------|------|-------|-------|-------|
| In Progress | 🔧 OMN-XXXX | Epic Title | Scope description | repo1, repo2 |
| Backlog | 🔵 OMN-YYYY | Another Epic | Description | repo |

---

## Epic: {emoji} OMN-XXXX -- {Title} ({Priority})

{Optional 1-2 sentence description of what this epic delivers.}

| Status | Ticket | Repo | Title | Notes |
|--------|--------|------|-------|-------|
| In Progress | OMN-XXXX | omniclaude | Ticket title | Optional note |
| Backlog | OMN-YYYY | omnibase_core | Another ticket | -- |

---

## Orphaned Tickets (no parent epic)

{Only included if orphans exist. Omit section header if none.}

| Status | Ticket | Repo | Title |
|--------|--------|------|-------|
| In Progress | OMN-XXXX | omniclaude | Ticket without epic |
```

#### Status values (canonical)

| Linear State | File value |
|-------------|------------|
| Backlog | `Backlog` |
| In Progress | `In Progress` |
| In Review | `In Review` |
| Done | *(omitted from file)* |
| Cancelled | *(omitted from file)* |

**"IP"** is an abbreviation for "In Progress" that appears in legacy entries. Normalize
to `In Progress` on any full regeneration. Preserve on patch mode (don't change stable rows).

#### Priority string (canonical)

| Linear Priority | File value |
|----------------|------------|
| 1 (Urgent) | `(Urgent)` |
| 2 (High) | `(High)` |
| 3 (Normal) | `(Medium)` |
| 4 (Low) | `(Low)` |

### Dry-Run Output (sync)

When `--dry-run`, print a diff-like summary:

```
Ticket Plan Sync -- DRY RUN (patch mode)

Would update 5 rows:

  OMN-2068  "In Progress" -> "Done"
  OMN-1452  "In Review" -> "Done"
  OMN-2700  "Backlog" -> "In Review"
  OMN-554   "In Progress" -> "In Review"
  OMN-555   "In Review" -> "Done"

Would add 1 new epic:
  + OMN-2800 "[omniclaude] DB-SPLIT" (5 children)

No file changes written.
```

### Composable Output (sync)

When invoked as a sub-skill (e.g., from `linear-housekeeping`):
- Write updated file to target path
- Return `rows_updated` count
- Exit 0 on success, 1 on write failure

---

## See Also

- `/plan-ticket` -- Generate a YAML contract template for a single new ticket
- `/ticket-work` -- Execute a specific ticket through contract-driven phases
- `/create-followup-tickets` -- Create Linear tickets from code review issues
- `@_lib/contracts/helpers.md` -- EpicContract schema (used by sync mode)
- `linear-triage` skill -- run before sync to clean up Done tickets first
- `linear-housekeeping` skill -- parent orchestrator
