# Decompose Epic

> Analyze a Linear epic description and create sub-tickets as Linear children

- Skill: `omninode-ai/decompose-epic` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add omninode-ai/decompose-epic`
- Raw SKILL.md: https://api.skillmd.com/api/skills/omninode-ai/decompose-epic/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: OmniNode-ai (https://skillmd.com/u/omninode-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/omninode-ai/decompose-epic

---


# Decompose Epic

## Dispatch Surface

**Target**: Agent Teams

## Overview

Analyze a Linear epic's description, goals, and context to generate a set of actionable
sub-tickets. Creates each sub-ticket as a Linear child of the epic, assigns repo hints
from the repo manifest, and returns `ModelSkillResult` with created ticket details.

**Announce at start:** "I'm using the decompose-epic skill to create sub-tickets for {epic_id}."

**Implements**: decompose-epic skill

- **Mode A** (no `--repos`): reads epic description, infers repo breakdown from `repo_manifest.yaml`, creates sub-tickets with one per identified work area matched to its owning repo
- **Mode B** (`--repos omniclaude,omnibase_core,...`): repos are pre-determined; creates one focused sub-ticket per repo, scoped to that repo's concerns

## Usage Examples

```
/decompose-epic EPIC-ID
/decompose-epic EPIC-ID --dry-run
```

## Decomposition Flow

1. Fetch epic from Linear: `tracker.get_issue({epic_id}, includeRelations=true)`
2. Read `plugins/onex/skills/epic-team/repo_manifest.yaml` for keyword-to-repo mapping
3. Analyze epic description + goals:
   - Identify distinct workstreams (one ticket per independent deliverable)
   - Keep tickets atomic: each ticket = one thing, one repo, one PR
   - Assign repo hint based on keywords in the work description
   - Generate title, description, requirements, and DoD for each ticket
4. If `--dry-run`: print plan, exit with `status: dry_run`
5. Create each ticket via `tracker.create_issue`:
   - `parentId`: epic's Linear ID
   - `team`: same team as epic
   - `labels`: ["omniclaude"] (or appropriate repo label)
6. **Post-decomposition: generate contracts for ALL child tickets.**
   For each created ticket:
   a. Fetch ticket details from Linear
   b. Extract DoD/acceptance criteria from description via dod_parser for ticket-specific checks.
      **Exception — never emit `test -f docs/plans/` checks:** the `no-planning-docs` pre-commit
      hook in `onex_change_control` forbids planning docs from existing in that repo (they live
      in the canonical registry's `docs/plans/`). Any `check_value` containing `test -f docs/plans/` or
      `test -f docs/plans/` variants would be structurally unsatisfiable and must be omitted.
      Other ticket-specific acceptance criteria extracted by dod_parser are retained normally.
   c. Generate contract YAML (stub for non-seam, full for seam tickets)
   d. Validate each contract (YAML lint + schema check) before writing
   e. Write to `$ONEX_CC_REPO_PATH/contracts/{ticket_id}.yaml`
   f. Commit all contracts in a single batch via branch + PR:
      ```
      cd $ONEX_CC_REPO_PATH
      git checkout -b auto/contracts-{epic_id}
      git add contracts/OMN-*.yaml
      git commit -m "feat: auto-generate contracts for {epic_id} decomposition ({N} tickets)"
      git push origin auto/contracts-{epic_id}
      gh pr create --title "auto: contracts for {epic_id} ({N} tickets)" \
        --body "Auto-generated ticket contracts from decompose-epic" --auto
      ```
   This ensures tickets created by decompose-epic have the same contract coverage
   as tickets created by plan-to-tickets.
7. Write result and exit

## Ticket Creation Contract

```python
tracker.create_issue(
    title="{ticket_title}",
    team="{epic_team}",
    parentId="{epic_linear_id}",
    description="""
## Summary
{what_this_ticket_implements}

## Requirements
{functional_requirements}

## Definition of Done
- [ ] Implementation complete
- [ ] Tests passing
- [ ] PR merged

## Repo Hint
{repo_name}: {rationale}
    """,
    labels=["{repo_label}"]
)
```

## Repo Manifest

Loaded from `plugins/onex/skills/epic-team/repo_manifest.yaml`:

```yaml
repos:
  - name: omniclaude
    path: ~/Code/omniclaude
    keywords: [hooks, skills, agents, claude, plugin]
  - name: omnibase_core
    path: ~/Code/omnibase_core
    keywords: [nodes, contracts, runtime, onex]
```

## Skill Result Output

**Output contract:** `ModelSkillResult` from `omnibase_core.models.skill`

> **Note: This contract reference is behavioral guidance for the LLM executing this skill. Runtime validation not yet implemented.**

Write to: `$ONEX_STATE_DIR/skill-results/{context_id}/decompose_epic.json`

| Field | Value |
|-------|-------|
| `skill_name` | `"decompose-epic"` |
| `status` | One of the canonical string values: `"success"`, `"dry_run"`, `"error"` (see mapping below) |
| `extra_status` | Domain-specific status string (see mapping below) |
| `run_id` | Correlation ID |
| `extra` | `{"epic_id": str, "created_tickets": list[{"id": str, "title": str, "repo_hint": str}], "count": int}` |

> **Note on `context_id`:** Prior schema versions included `context_id` as a top-level field. This field is not part of `ModelSkillResult` — it belongs to the file path convention (`$ONEX_STATE_DIR/skill-results/{context_id}/decompose_epic.json`). Consumers should derive context from the file path, not from `context_id` in the result body.

**Status mapping:**

| Current status | Canonical `status` (string value) | `extra_status` |
|----------------|-----------------------------------|----------------|
| `created` | `"success"` (`EnumSkillResultStatus.SUCCESS`) | `"created"` |
| `dry_run` | `"dry_run"` (`EnumSkillResultStatus.DRY_RUN`) | `null` |
| `error` | `"error"` (`EnumSkillResultStatus.ERROR`) | `null` |

**Behaviorally significant `extra_status` values:**
- `"created"` → ticket-pipeline (cross-repo split path) proceeds to invoke epic-team with parent epic ID; the `extra["created_tickets"]` list contains the sub-ticket IDs passed to epic-team
- `null` (dry_run) → ticket-pipeline does not advance; decomposition plan is logged for human review only

**Promotion rule for `extra` fields:** If a field appears in 3+ producer skills, open a ticket to evaluate promotion to a first-class field. If any orchestrator consumer (epic-team, ticket-pipeline) branches on `extra["x"]`, that field MUST be promoted. Note: ticket-pipeline already branches on `extra["created_tickets"]` — evaluate promotion.

Example result:

```json
{
  "skill_name": "decompose_epic",
  "status": "success",
  "extra_status": "created",
  "run_id": "pipeline-1709856000-EPIC-ID",
  "extra": {
    "epic_id": "EPIC-ID",
    "created_tickets": [
      {"id": "TICKET-1", "title": "Implement X", "repo_hint": "omniclaude"},
      {"id": "TICKET-2", "title": "Add node Y", "repo_hint": "omnibase_core"}
    ],
    "count": 2
  }
}
```

**Status values**: `success` (`extra_status: "created"`) | `dry_run` | `error`

## See Also

- `epic-team` skill (invokes decompose-epic when epic has 0 child tickets)
- `ticket-pipeline` skill (planned: invokes decompose-epic on cross-repo auto-split)
- `plugins/onex/skills/epic-team/repo_manifest.yaml` — repo keyword mapping

