| name | github-work-decomposition |
| description | Decomposes large GitHub work into a milestone → umbrella → epic → subtask hierarchy using gh CLI, native sub-issues, issue types, and Projects v2, with a deterministic slice-scoring engine that decides how many children each level should have. Use when breaking down a roadmap item, planning a milestone, splitting an umbrella into epics, splitting an epic into subtasks, right-sizing a backlog, or deciding how many pieces work should be cut into. Triggers include: 'create a milestone with umbrellas', 'decompose this umbrella into epics', 'split this epic into subtasks', 'how many slices should this be', 'break down this feature', 'right-size this backlog'. Do NOT use for authoring issue prose alone, triaging existing issues, release automation, or CI workflow authoring. |
| metadata | {"version":"0.1.0","category":"dev-tooling","requires":"gh >= 2.94.0, jq, python3 >= 3.8"} |
GitHub Work Decomposition
Overview
Turns one large intent into a governed four-level tree on GitHub:
Milestone ──▶ Umbrella issue ──▶ Epic issue ──▶ Subtask issue
(REST) (type: Umbrella) (type: Epic) (type: Task)
Every parent→child link is a native GitHub sub-issue, not a task-list checkbox, so
progress rolls up automatically and Projects v2 shows real parent/sub-issue fields.
The distinguishing part is that fan-out is computed, not guessed. scripts/slice_score.py
scores each item on six SPIDR-derived axes plus a coupling axis, then emits a recommended
child count, the gates that fired, and a confidence rating. The agent never invents "let's
make 5 epics."
When to use
- "Create milestone Q3-Auth with umbrellas for login, SSO, and recovery"
- "Decompose the SSO umbrella into epics"
- "Split epic #412 into subtasks"
- "How many slices should this feature be cut into?"
- "Right-size this backlog — some of these epics look too fat"
When NOT to use
- Writing the prose of a single issue → plain
gh issue create
- Triaging or labelling issues that already exist → an issue-triage skill
- Cutting releases, tagging, changelogs → a release-automation skill
- Authoring GitHub Actions workflows → a CI skill
Vocabulary → GitHub primitive
| Level | GitHub primitive | Created by | Typical fan-out |
|---|
| Milestone | Repo milestone (REST only) | create-milestone.sh | 2–5 umbrellas |
| Umbrella | Issue, type Umbrella (fallback Epic), assigned to milestone | create-umbrella.sh | 3–7 epics |
| Epic | Issue, type Epic (fallback Feature), sub-issue of umbrella | decompose-umbrella.sh | 3–9 subtasks |
| Subtask | Issue, type Task, sub-issue of epic | decompose-epic.sh | 0 (leaf) |
Issue types are organization-level. On personal repos or orgs without custom types,
preflight downgrades to labels (umbrella, epic) and records the downgrade in the plan.
Phase 0 — Preflight (never skip)
scripts/preflight.sh --repo OWNER/REPO --json > .cache/capabilities.json
Detects and writes: gh version, whether native --parent/--type flags exist, auth scopes
(project scope is separate — gh auth refresh -s project), available issue types, whether
the repo has sub-issues enabled, and the linking strategy to use.
Gate P0: if gh < 2.94.0 the skill uses the REST/GraphQL fallback linker. If auth lacks
project scope and the request involves a board, stop and tell the user the exact refresh
command rather than half-completing the tree.
Phase 1 — Milestone with umbrellas
- Score the milestone intent with
slice_score.py --level milestone.
- Draft the umbrella set as a plan file (see
references/plan-schema.md). No writes yet.
- Validate:
scripts/validate_plan.py plan.json — checks band limits, fan-out caps,
title uniqueness, and that every umbrella has a distinct user-visible outcome.
- Present the plan to the user. Get approval.
- Apply:
scripts/create-milestone.sh then scripts/create-umbrella.sh per umbrella.
Gate P1: if scoring recommends more than 5 umbrellas, the milestone is over-scoped.
Recommend splitting the milestone itself instead of accepting the fan-out.
Phase 2 — Umbrella → epics (one umbrella at a time)
Process umbrellas sequentially, never in parallel. Each pass:
slice_score.py --level umbrella --input umbrella-NNN.json
- Apply the chosen SPIDR cut axis to generate candidate epics.
- Vertical-slice test on every candidate: could this ship on its own? If candidate B
only works once candidate A lands, it is a horizontal split in disguise — merge them back
and re-cut on a different axis.
- Validate → present → apply via
decompose-umbrella.sh.
- Re-score the umbrella. If residual scope remains, loop back to step 2.
Gate P2: if no axis yields an independently shippable slice, do not split. Report
that this is a prioritization question, not a splitting one, and stop.
Phase 3 — Epic → subtasks
Same loop at finer grain, with --level epic. Subtasks are leaves: they are not decomposed
further. Horizontal slicing is permitted here (a subtask may be "add DB column") because
the shippable unit is the epic, not the subtask.
Gate P3: hard cap of 100 sub-issues per parent (GitHub limit) and 8 nesting levels. This
skill uses 3 issue levels, so nesting is never the binding constraint — fan-out is.
Slice scoring in brief
Six axes scored 1–5, derived from Mike Cohn's SPIDR plus scope breadth:
| Axis | Question | Weight |
|---|
| Breadth (B) | How many distinct user-visible outcomes? | 0.30 |
| Path (P) | How many distinct flows, incl. error paths? | 0.25 |
| Interface (I) | How many clients, platforms, or APIs? | 0.15 |
| Data (D) | How many data shapes, formats, migrations? | 0.15 |
| Rules (R) | How many business rules or policy variants? | 0.10 |
| Unknowns (U) | How much is genuinely unknown? | 0.05 |
Plus Coupling (C, 1–5), which does not add pressure — it damps it.
pressure = Σ(weightᵢ × (scoreᵢ − 1) / 4) → 0.0 … 1.0
k = round(band_min + pressure × (band_max − band_min))
Then the gates fire in order: ATOMIC → INDIVISIBLE → SPIKE_FIRST → COUPLING_DAMP →
OVERFLOW_PROMOTE. Full math, gate semantics, and worked examples in
references/scoring-model.md.
scripts/slice_score.py --level epic -B 4 -P 3 -I 2 -D 2 -R 3 -U 1 -C 2 --json
Output includes recommended_k, band, pressure, gates_fired[], confidence, and a
human-readable rationale the agent can paste into the issue body.
Confidence is HIGH only when every axis was scored from a real artifact (spec, ticket,
code). Axes guessed from a one-line request cap confidence at LOW, and LOW confidence means
the agent presents the number as a proposal and asks, rather than applying it.
Plan-then-apply
Every phase produces a plan file before touching GitHub. This is not ceremony — it is what
makes the operation reviewable and re-runnable.
scripts/decompose-umbrella.sh --plan plan.json --dry-run
scripts/decompose-umbrella.sh --plan plan.json
All apply scripts are idempotent by title: re-running with the same plan reuses existing
issues instead of creating duplicates. Interrupted runs can simply be re-run.
Scripts
| Script | Purpose |
|---|
preflight.sh | Capability + auth detection; writes capabilities.json |
slice_score.py | Deterministic fan-out scoring engine (the eval system) |
validate_plan.py | Schema + band + cap validation before any write |
create-milestone.sh | Idempotent milestone create (REST); returns number |
create-umbrella.sh | Umbrella issue + milestone + type + board |
decompose-umbrella.sh | Creates epics as sub-issues of an umbrella |
decompose-epic.sh | Creates subtasks as sub-issues of an epic |
link-sub-issue.sh | Three-strategy parent linker with ID-type handling |
tree-status.sh | Renders the full tree with completion rollup |
Run any script with --help. All accept --repo OWNER/REPO; all default to the current repo.
The sub-issue ID trap
The single most common failure. POST /repos/{o}/{r}/issues/{n}/sub_issues expects
sub_issue_id = the issue's REST database id (a large integer such as 3000028010).
But gh issue view --json id returns the GraphQL node id (I_kwDOOakzpM6yyU6H). Passing
the node id yields a misleading 404 The provided sub-issue does not exist.
link-sub-issue.sh handles all three paths in order:
- Native:
gh issue edit N --set-parent P (gh ≥ 2.94.0) — preferred
- REST: fetch
.id via gh api repos/{o}/{r}/issues/{n} --jq .id, then POST
- GraphQL:
addSubIssue(input: {issueId, subIssueId}) using node ids
See references/gh-command-reference.md for the full flag surface and fallbacks.
Safety constraints
- NEVER write to GitHub before the user has approved the plan for that phase.
- NEVER run a decomposition phase in parallel across parents — sequential only, so a
mid-run failure leaves a partial tree that is trivially resumable.
- NEVER delete or close existing issues as part of decomposition. Re-parenting uses
--remove-parent / --set-parent, never delete-and-recreate.
- NEVER hardcode tokens.
gh handles auth; GH_TOKEN may come from the environment only.
- NEVER fabricate a slice count when scoring inputs were not gathered — report LOW
confidence and ask instead.
- ALWAYS dry-run first when the tree exceeds 20 total issues.
- ALWAYS record the fired gates in the parent issue body, so the structure is auditable
later by a human who wasn't in the conversation.
Troubleshooting
| Symptom | Cause | Fix |
|---|
404 The provided sub-issue does not exist | node id passed where database id required | use link-sub-issue.sh, strategy 2 |
unknown flag: --parent | gh < 2.94.0 | upgrade, or the script auto-falls back |
--type rejected | org has no such issue type | preflight downgrades to labels |
| Project field edit silently no-ops | missing project scope | gh auth refresh -s project |
| Sub-issue inherits wrong milestone | children inherit parent's milestone by default | set milestone on the parent first |
| Secondary rate limit on bulk create | creating too fast | scripts back off; keep fan-out inside bands |
Extended cases in references/troubleshooting.md.
Quick reference
scripts/preflight.sh --json
scripts/slice_score.py --level umbrella -B 5 -P 4 -I 3 -D 2 -R 2 -U 2 -C 2
scripts/create-milestone.sh --title "Q3 Auth" --due 2026-09-30
scripts/create-umbrella.sh --milestone 7 --title "SSO" --body-file umbrella.md
scripts/decompose-umbrella.sh --plan plan.json --dry-run
scripts/decompose-epic.sh --plan epic-412.json
scripts/tree-status.sh --milestone 7