Turn one task into a tree of GitHub issues worked by agents. Creates (or reuses) an epic issue, then launches the recursive orchestrator → decomposer → worker pipeline. Use when asked to "decompose a task", "break this down into issues and work it", "spawn agents to build X", or run via /task-decompose.
Installs into .claude/skills of the current project.
Are you the author of Task Decompose?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/friendlyinternet-task-decompose)
---
name: task-decompose
layer: method
description: Turn one task into a tree of GitHub issues worked by agents. Creates (or reuses) an epic issue, then launches the recursive orchestrator → decomposer → worker pipeline. Use when asked to "decompose a task", "break this down into issues and work it", "spawn agents to build X", or run via /task-decompose.
allowed-tools: mcp__github__issue_read, mcp__github__issue_write, mcp__github__sub_issue_write, mcp__github__list_issues, mcp__github__search_issues, mcp__github__get_label, Read, Grep, Bash, Agent
---
# Task Decompose
The single entry point for the **recursive task-decomposition agent pipeline**. One task
in → an epic with a tree of sub-issues, each leaf worked by an agent on its own branch.
```mermaid
flowchart TD
U["/task-decompose '<task>'"] --> S[This skill: ensure epic exists]
S --> O[task-orchestrator · depth 0]
O -->|2–6 sub-issues| D[task-decomposer · depth 1+]
D -->|leaf?| Q{LEAF TEST + depth < 3}
Q -->|leaf / at cap| W[task-worker · worktree → PR Closes #N]
Q -->|too big| D2[task-decomposer · depth+1]
D2 --> Q
```
## The three agents
| Agent | Role | Recurses? | Spawns |
|-------|------|-----------|--------|
| `task-orchestrator` | reads the epic, makes 2–6 top-level sub-issues | no | a decomposer per child |
| `task-decomposer` | LEAF TEST one issue: split or build | **yes** | decomposers (split) or a worker (leaf) |
| `task-worker` | implements one leaf issue → PR | no | nothing (terminal) |
## Stop-conditions (so it can't run away)
- **MAX_DEPTH = 3** — at depth 3 the decomposer stops splitting and treats the issue as a
leaf, however big it looks.
- **MAX_CHILDREN = 6** — at most 6 children per split (orchestrator and decomposer alike).
- **LEAF TEST (all four)** — single coherent change · bounded file set · clear/testable
acceptance · doable in one focused worker run. All true ⇒ build it, don't split.
To tune the limits, edit the numbers in `.claude/agents/task-decomposer.md` (and the
orchestrator's MAX_CHILDREN).
## Integration-branch flow & safety (#348 hardening)
Learned from the #325 run, where parallel workers — branching off `main`, unable to see
each other's unmerged work — each re-created the same new package and a couple silently
chose a different design. Four rules now prevent that:
1. **Epic integration branch (#349).** The orchestrator creates `epic/<NN>-<slug>` off
`main`. Every sub-issue branches off **that** branch and targets its PR **at** it (not
`main`), so a later sub-issue sees what an earlier one built — no duplicate scaffolds.
The whole feature gets **one** human review when `epic/<NN>` is PR'd into `main` at the
end (the epic is "done" only then, not when its children merge into the epic branch).
Dependency-ordered children are **wave-gated** (foundation merges first, then dependents
on an idempotent re-run); independent ones still run in parallel. **The branch is keyed by
the epic NUMBER, not the slug (#611):** a re-dispatch resolves and **reuses** any existing
`epic/<NN>-*` (merging `main` into it), never minting a second slug — so re-runs can't leave
sibling epic branches behind.
2. **Epic-scoped package approval (#350).** A package-touching epic is approved **once**:
the `packages/` gate honours the **`CROUTON_PACKAGE_EDIT_APPROVED`** env var (inherited
by every spawned worker), so you don't unlock per-worker. **Never** commit the
`.claude/.package-edit-approved` file — `guard-package-approval.yml` fails any PR to
`main` that contains it (env can't leak; a committed file would disable the gate for all).
3. **Review dial (#351).** Risky epics (create a package / change schema / dependency
chain, or labelled `review:plan`) **pause for human plan-approval before any worker
runs** — the orchestrator posts the proposed tree, @mentions `@pmcp`, blocks. Plus the
epic→`main` PR is the integration review. Low-risk epics (`review:auto`) skip the gate.
4. **Block, don't improvise (#352, #1229).** A worker that finds a prerequisite missing (a
package/table/symbol a sibling owns, not yet merged) **stops and waits** — it never
scaffolds the missing thing itself, and never silently diverges from the epic's stated
design invariants. Missing prerequisite = blocker, not a DIY. **The same rule applies when
the *documented* path fails unexpectedly** — a tool errors (e.g. `crouton init` → *"directory
already exists"*), a generator refuses, a step behaves unlike its docs: **STOP**, comment the
exact error + what you tried + what you'd need, `@mention @pmcp`, set `status:blocked`, stop —
do **NOT** improvise an alternative or hand-roll what the tool was supposed to do. Improvising
turned #1213's one-command scaffold into **109 turns / $4.14** of off-standard work that hid the
real bug. An unexpected failure of the *intended* path is a stop signal, not a puzzle to solve.
## `.github/workflows/` boundary — embed-patch, don't block (#1076)
The Harness App token backing this pipeline (and pi's headless runs) deliberately **lacks the
`workflows` write scope** — decided as a hard, permanent boundary rather than granting it,
because a token that can edit its own CI can also alter its own guardrails. A commit touching
`.github/workflows/**` is hard-rejected by GitHub regardless of who/what is running.
If a leaf's work needs a workflow-file change (new trigger, path filter, job):
- Do **not** treat it as a blocker to stop-and-wait on — it's a known limit of your write access,
not a missing prerequisite. Commit everything else in the PR normally.
- Embed the workflow diff verbatim as a `git apply`-able fenced diff block in the PR body under a
`## Workflow patch (human applies)` heading (see PR #1075 for the exact shape).
- Post a plain top-level issue comment (never a PR *review* body) naming the pending patch and
@mentioning `NOTIFY_HANDLE`. Add `status:blocked` only if the omitted workflow change is
load-bearing for the rest of the PR; otherwise it's an FYI, not a hold.
- **This is a complete deliverable, not a partial run** — "PR opened + workflow patch embedded
for a human" is a first-class PASS, exactly like any other linked-PR run. Don't spend extra
turns hunting for a way to write the workflow file anyway.
## Triggers (manual + automatic)
- **Manual:** run `/task-decompose "<task>"` or `/task-decompose #NN` in any Claude Code
session (CLI, web, desktop, mobile app).
- **Automatic — issue opt-in:** add the **`delegate`** label to any issue and
`.github/workflows/decompose-on-issue.yml` runs `/task-decompose #NN` for you (no
command needed). Gate is the label, so only opted-in issues fire.
- **By comment (mobile-friendly):** comment **`/delegate`** (or **`/deploy`**) on an issue —
`.github/workflows/comment-dispatch.yml` applies the `delegate` label for you (via a PAT, so
the run is human-actored). Same effect as labelling, but you can do it from the GitHub mobile
app where the label UI is awkward. Owner/members only.
- **Automatic — resume after a blocker:** when an agent @mentions you and sets
`status:blocked`, just **reply on the issue** — `.github/workflows/resume-on-comment.yml`
picks it back up, removes the block, and continues.
### Auth & billing (read before changing the workflows)
- **Interactive runs (you typing `/task-decompose` in a CLI / web / app session)** run on
**whatever your session is logged into** — a **Claude Pro/Max subscription is fine**;
this is ordinary interactive Claude Code use.
- **The automated workflows are headless/unattended, so they MUST use an
`ANTHROPIC_API_KEY`** repo secret (same as `claude.yml`) — pay-per-token. **Do NOT wire
them to a subscription `CLAUDE_CODE_OAUTH_TOKEN`.** Per Anthropic's Legal & Compliance
terms, subscription OAuth is for *ordinary, individual, interactive* use of Claude Code,
**not** CI / Agent-SDK / headless automation (which bills at standard API rates). Using a
subscription token to drive these workflows would be against the terms.
- Cost is therefore a real factor on the automated path — see the model split in
`.claude/agents/CLAUDE.md` (Sonnet for the issue-only agents, Opus only for the worker)
and the `MAX_DEPTH`/`MAX_CHILDREN` caps that bound fan-out.
## How to run
`$ARGUMENTS` is either:
- **free text** — a task description → create a new epic, then orchestrate; or
- **`#NN` / a number** — an existing issue → reuse it as the epic, then orchestrate.
### Procedure
1. **Resolve the epic.**
- If `$ARGUMENTS` is (or contains) an issue number: `issue_read` it; use it as the epic.
- Else: **dedup first** — run the **`issue-dedup`** skill (search open **and**
recently-closed work by keywords + `epic` label, surface matches, decide reuse /
replace / new). If a matching epic exists, **reuse it** — don't mint a duplicate.
Otherwise create one with `issue_write` (method `create`):
- title: plain human English (no jargon);
- labels: `epic` + the component it primarily spans (`pkg:*`/`app:*`; never `root`).
For dev-tooling/`.claude` work that serves the whole monorepo, use `meta:agents`;
- body: `## 👤 For humans`, `## 🤖 For agents`, `## 🧪 How to test` per `github-tasks`,
ending with a `Dedup-checked:` attestation line — the `require-issue-dedup` hook
**blocks the create without it** (#297).
2. **Launch the orchestrator.** Spawn it via the `Agent` tool:
- `subagent_type: "task-orchestrator"`
- **`run_in_background: false` — MANDATORY. Spawn it SYNCHRONOUSLY and wait for it to finish.**
The `Agent` tool defaults to background; in the one-shot CI job (`decompose-on-issue.yml`) a
backgrounded orchestrator is **killed when the job ends**, so the sub-issue tree never persists
and the artifact-gate fails the run (the #1209/#1210 fire-and-forget). **Never** end your turn
with "orchestration is running in the background / I'll be notified" — you will **not** be
re-invoked here. Hold until the orchestrator returns (sub-issues created + epic branch pushed).
- prompt: `{ epic_issue_number: <epic number>, depth: 0 }` + a short restatement of the
task so it doesn't need an extra read.
- **If `#NN` is a CHILD issue (it has a `parent_issue_url`), not an epic** — e.g. someone
`delegate`'d / `/deploy`'d a single sub-issue from the mobile app — pass its parent epic
as `epic_issue_number` and name the child to work, OR pass the child with a note that it
is a child: the orchestrator must work it on the **parent's** `epic/<parent>-<slug>`
branch, **never** mint a new epic off `main` (where the scaffold wouldn't exist). The
pipeline hands off by **spawning** agents (`Agent` tool), never by applying the
`delegate` label from inside a run (that's bot-actored and self-rejecting).
3. **Report** the epic url and that orchestration has started. The orchestrator creates the
`epic/<NN>-<slug>` integration branch; the tree then builds itself onto it (decomposers
recurse; workers open PRs into the epic branch with `Closes #NN`); a single epic→`main`
PR lands the lot behind one review.
## Building an app? It's a POC by default — end at a preview URL
When the task is **"build an app / build X"** (a runnable app, not a package or tooling change), it is a **POC by default** — the user does **not** need to say "as a poc":
- **Default to POC.** In an *interactive* session you MAY ask once **"POC (preview) or launched app?"** — but the default is **POC**, and when running **headless you never ask** (the pipeline can't block): always POC. An app only starts in `apps/` if the user *explicitly* says it's a production launch.
- **Scaffold under `pocs/<name>`, NOT `apps/`** — the safe-to-break incubator (root `CLAUDE.md`). Label its issues `poc:<name>`. `apps/` is only for launched apps with a production counterpart.
- **Step 0 — package-fit check FIRST (#292).** *Before* slicing the workstreams below, check whether an existing `@fyit/crouton-*` package already does most of the work: read `.claude/skills/task-decompose/package-catalog.md` (one lookup — generated by `scripts/gen-package-catalog.mjs`), and if a package fits, propose **Option A** (build *on* it) vs **Option B** (from scratch) on the epic, **default to reuse**, and slice **along the chosen path**. Order: internal packages → `ecosystem-check` (external OSS) → build. This is mandatory for app/build epics — the #274 blog hand-rolled a `posts` collection + reader when a blog ≈ a `crouton-pages` page-type + `crouton-editor` body. Full procedure: the task-orchestrator's **Step 0** section.
- **Canonical workstream order for a crouton app — use the CLI, NEVER hand-mirror.** A crouton app build decomposes into this fixed shape. Do not reinvent it, and do **not** tell a worker to "mirror `pocs/blog`" or hand-copy another app — that is how versions drift:
1. **Design the data model + generation plan, then SIGN OFF.** Draft both `schemas/*.json` (the fields) **and** `crouton.config.js` (collections, target `layer`, `features`, `locales`, `dialect`). Run the **`schema-review`** skill and get human sign-off on **both** before any code is generated — the config decides the forms / list views / API surface, so it is reviewed *alongside* the schema, not after. **Post the review, the `@mention`, and `status:blocked` on this workstream's OWN issue (or its draft PR) — NEVER on the epic.** This *is* the schema/config sign-off issue, so the field tables, the config, the ping, and the human `approve` reply all live in one place; the epic only carries the final verify rollup. (If a step would touch `packages/`, the HARD GATE applies — but a normal POC build never does.)
2. **`crouton init <name> --features …`** — one deterministic pass that **scaffolds the app *and* generates** the collections / forms / API / Drizzle **schema** from the approved schema+config. The CLI pulls pinned versions from a single source (`getFrameworkPackages`), so apps never drift. (`crouton scaffold-app` is the scaffold-only fallback; `crouton add` to bolt on features later.) ⚠️ **It does NOT generate the D1 _migrations_** — `crouton config` has no migration logic, so the fresh app has a Drizzle schema but **no `server/db/migrations/sqlite/`**.
2b. **Generate + commit the initial migrations** — do **not** skip this, or the deploy's remote-migrate step fails with *"No migrations present"* (the gap that bit library-catalog, #457). Per the **`db-migrations`** skill's build-first workaround: `NITRO_PRESET=node-server nuxt build` until `.nuxt/hub/db/schema.mjs` appears → `pnpm db:generate` → **commit** `pocs/<name>/server/db/migrations/sqlite/**` (the `.sql` + `meta/`).
3. **Deploy a review-on preview** — run `pnpm poc:scaffold-deploy <name>` and open the PR so CI deploys an isolated staging Worker (with `NUXT_PUBLIC_CROUTON_REVIEW=true` + the review-bridge env) and posts the **`https://<name>.pmcp.dev`** URL (auth-working). This comes **before** UI refinement on purpose: the UI sign-off is now done **on the live preview** (#590/#488), and you can't click-to-comment on a page that isn't deployed. See the **`poc-deploy`** skill.
4. **Refine the UI — on the live preview (the sign-off gate).** Forms / UI polish, search, etc., reviewed via the live-preview loop: the reviewer clicks an element on the deployed page → a `🎯 Preview feedback` comment names the source `.vue` → the worker fixes it and **redeploys the same `<name>.pmcp.dev`** → iterate → `approve` (a comment). (Static mockup is the `--static` fallback when no deploy is possible, e.g. a package-only change.)
- **Deploy BEFORE refine — never the reverse.** Ordering refine before deploy creates a chicken-and-egg (the UI review needs a running page; the page needs a deploy) — that's the circular dependency to avoid. So a crouton-app epic is: schema → `crouton init` → **deploy preview** → **refine on the live preview**. The deploy-preview is a prerequisite of the UI gate, not the finale.
- **The endpoint is a deployed, testable preview URL — not merged code.** The preview exists from step 3 and every refinement re-rides the same URL; don't call the build "done" until that preview is live, auth-works, and the UI sign-off is approved. See the **`poc-deploy`** skill.
- **Testable with auth:** the POC's `seed` provisions a known admin; the epic's verify rollup hands back the URL **and** the login. Don't call an app build "done" until the preview URL is live.
- **Promotion is a separate, explicit step.** A POC graduates to production by being promoted **`pocs/<name>` → `apps/<name>`** (taking on `apps/` rigor, the `app:<name>` label, and a prod two-domain deploy). Never automatic — only on an explicit "launch/promote" request.
## Notifications & async Q&A (`NOTIFY_HANDLE = @pmcp`)
Headless/automation runs (a webhook- or Action-triggered session) have **no human
attached**, so agents must **never block-and-wait** on a question — `AskUserQuestion`
just times out there. Instead the pattern is **comment-and-stop**, and the comment
**@mentions the notify handle** so the owner gets a real GitHub notification (which
surfaces in the GitHub / Claude mobile app):
- **Small ambiguity** → decide with a sensible default, record the assumption in the
issue body, keep going. *No mention* (don't spam).
- **Progress / status update** ("spawning the worker for #NN", "wave 2 of 4 starting",
"preview deploying") → a plain `add_issue_comment` with **no @mention**. An @mention is
a request for the human to *act*; an FYI is not. If nothing is required of the owner,
do not ping them — post the update unmentioned (or skip it).
- **Real blocker / decision needed** → `add_issue_comment` on the issue with a tight
question + options, **@mention `NOTIFY_HANDLE`**, apply the `status:blocked` label,
then **stop** that branch. The owner replies on the issue; a resume trigger (or a
human re-running `/task-decompose #NN`) picks the thread back up.
- **Epic done** → when the last child merges, the verify-rollup comment on the epic
also @mentions `NOTIFY_HANDLE` (per `github-tasks`).
**Every actionable ping is a TOP-LEVEL comment.** A blocker question **or a sign-off request**
goes out as `add_issue_comment` (a top-level issue/PR comment) — that notifies reliably.
**Never bury the ask in a PR *review* body** (state `COMMENTED`): it's a weak surface the owner
misses (the #846 sign-off case, where pi's `lgtm/approve` ping sat inside a review). Detailed
review/analysis can live in its own artifact; the `@mention` + `status:blocked` ask stands alone
on top.
To change who gets pinged, edit `NOTIFY_HANDLE` here and in `.claude/agents/CLAUDE.md`.
## Notes
- Everything persists as real GitHub issues (epic → sub-issues → sub-sub-issues), so the
tree survives across sessions and shows progress bars on each parent.
- Workers run in **git worktree isolation** — parallel leaves never collide; they branch
off and PR into the **epic branch**, not `main` (see "Integration-branch flow").
- This plugs into the repo's ISSUE-FIRST + `github-tasks` + `/commit` + merge-policy
workflow; the agents enforce those rules themselves.
- Sub-PRs may auto-merge into the **epic branch** (a staging area). The final
**epic→`main`** PR is **not** auto-merged — that's the human review/merge gate.