---
name: Composer
slug: composer
category: AI Engineering
description: Composer orchestrates Piyaz tasks end to end, from picking ready work through research, implementation, review, and merge. Use it for backlog runs, single-task execution, or rework loops on PR feedback.
github: "https://github.com/FrkAk/piyaz/tree/main/plugins/claude-code/skills/composer"
language: TypeScript
stars: 189
forks: 18
install: "npx degit https://github.com/FrkAk/piyaz/tree/main/plugins/claude-code/skills/composer ~/.claude/skills/composer"
installs_to: ~/.claude/skills/composer
source_path: plugins/claude-code/skills/composer/SKILL.md
collection_size: 17
category_size: 3278
collection_url: "https://dirskills.com/collections/FrkAk/piyaz"
added: 2026-09-06T05:19:53.044Z
last_synced: 2026-09-06T05:19:53.044Z
canonical_url: "https://dirskills.com/skills/composer"
---

# Composer

Composer orchestrates Piyaz tasks end to end, from picking ready work through research, implementation, review, and merge. Use it for backlog runs, single-task execution, or rework loops on PR feedback.

**Install:**

```bash
npx degit https://github.com/FrkAk/piyaz/tree/main/plugins/claude-code/skills/composer ~/.claude/skills/composer
```

## README

# Composer

Composer is a Piyaz task orchestrator. Per iteration it picks the next ready task off the project's critical path, runs that task through a deterministic per-task **workflow** (research, plan, implement, CI gate, review, bounded fix loop), surfaces the verdict, merges when the user authorized it, propagates the result through the graph, and continues until a structural stop condition holds.

The orchestrator (this skill, running in the main loop) owns only the **interactive seams**: pick the task, resolve gates, run the merge gate, propagate. The token-heavy phase sequencing runs inside the workflow, off the orchestrator's context, dispatching the phase agents in fresh windows with per-phase model and effort. This is the design's main token discipline: orchestration is JavaScript, not main-loop reasoning over a transcript that grows with every phase.

Composer is glue. The heavy lifting (task selection, refinement, the Completion Protocol, propagation) lives in the `piyaz` skill (`skills/piyaz/SKILL.md`); composer reuses those flows rather than duplicating them.

## Invocation

- **`/piyaz:composer`**: backlog mode. Pick the highest-value ready task each iteration; continue until a stop condition holds.
- **`/piyaz:composer <taskRef>`**: single-task mode. Same pipeline applied to one task; exits after the iteration completes.
- **`/piyaz:composer rework <taskRef|pr-url>`**: rework mode. HOTL requested changes on GitHub instead of merging; composer rounds that feedback back through the fix loop.
- **`/piyaz:composer --pipelined`**: backlog mode with research-ahead (latency-only, costs tokens). Off by default; see *Pipelined research-ahead*.

No argument means backlog mode; `rework` plus an argument means rework mode; anything else is single-task.

## Piyaz operating context

The canonical piyaz rules load with this skill. Downstream citations (`conventions §1`, `artifacts §3`, `lifecycle §3`) refer to this loaded text.

@skills/piyaz/references/conventions.md
@skills/piyaz/references/artifacts.md
@skills/piyaz/references/lifecycle.md
@skills/piyaz/references/resilience.md

## The per-task workflow

Each iteration's task runs through `skills/composer/workflows/compose-task.js`, launched with the Workflow tool:

```
Workflow({
  scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/composer/workflows/compose-task.js",
  args: { taskRef, taskId, projectId, categories, tagVocabulary,
          pickEstimate, pickPriority, workType, tags,
          mode, plannableOnly, resumeFrom, priorBrief, gateAnswers,
          fixFindings, prUrl, priorFailure, estimate, flags, fable },
})
```

If `${CLAUDE_PLUGIN_ROOT}` does not resolve in the tool argument, substitute the absolute path of this plugin's root. The workflow runs in the background; the orchestrator is suspended until it returns, so it spends no context tokens while phases run.

The workflow dispatches the phase agents by `agentType`, each with explicit `model`/`effort`/`schema`, the implementer with `isolation:'worktree'`. It runs `research+plan → implement → ci-gate → review → [fix-loop ≤2 rotations]`, with a fixed-interval CI poll (60s, bounded) and a CI-pending re-poll path that re-reviews without burning a fix rotation, then returns one structured result. It does **not** merge, propagate, or touch edges; those are the orchestrator's seams. The phase contracts live in the agent files; do not duplicate them here.

| Phase | `agentType` | Writes to Piyaz | Workflow captures |
| --- | --- | --- | --- |
| 1+2. Research+Plan (merged) | `piyaz:composer-researcher` under an orchestrator authority grant | refinement fields (`description`, `acceptanceCriteria`, `tags`, `category`, `priority`, `estimate`, `decisions`) plus `implementationPlan`; `status='planned'` on `draft → planned` only | brief, status, gatePhase, flags, confidence, refined estimate/work-type, proposed rewrites, section/step counts, open questions |
| 3. Implement | `piyaz:composer-implementer` | `status='in_progress'` (claim), `status='in_review'` (+ Completion Protocol); fix mode rotates `in_review → in_progress → in_review` | status, PR URL, AC counts, concerns |
| CI gate | generic (haiku) | nothing | `green` / `red` / `pending` / `none`, failing checks |
| 4. Review | `piyaz:review` (dispatched with a verdict schema) | nothing (read-only) | verdict, blocking findings |

## The workflow result

The workflow returns exactly one of three shapes. Branch on `result.status`, not on prose:

| `status` | Meaning | Orchestrator reaction |
| --- | --- | --- |
| `DONE` | Task ran to `in_review` (or `planned` for a plannable-only pick) | Surface the verdict, run the *Merge gate*, propagate |
| `NEEDS_DECISION` | The merged research+plan phase gated; `result.gate` carries the trigger and `result.phase` names the raising half (`research` or `plan`) | Resolve via *Gates*, then relaunch the workflow with the answer |
| `BLOCKED` | A phase could not complete; `result.phase` and `result.reason` say which and why | *Failure handling* |

A `DONE` result also carries: `outcome` (`in_review`|`planned`), `verdict`, `prUrl`, `ciState`, `acSatisfied`/`acTotal`, `rotations`, `escalated` (true when a `block` verdict or an exhausted fix budget left findings unaddressed), `blockingFindings`, `concerns`. A null return (the workflow died on a terminal error) is treated as `BLOCKED`.

## Session bootstrap

Once per session, before the first iteration:

1. **Resolve the project.** `piyaz_workspace action='projects'` and note the identifier; pass it (or a taskRef) on every call — there is no server-side selection. Single-task mode: also `piyaz_search query='<taskRef>'` to confirm the task and its current status.
2. **Read meta.** `piyaz_get view='meta'`. Keep the categories and tag vocabulary for the workflow's research args; drop the status counts.
3. **Stale-claim sweep.** `piyaz_search project='<identifier>' status=['in_progress']` for tasks already claimed. Surface possible stale claims from dead sessions in the first pick rationale.
4. **Set the merge policy.** Ask once with `the AskUserQuestion tool`: `never` (default; HOTL owns the merge), `ask-each` (confirm per PR), or `auto-on-approve` (merge automatically on an `approve` verdict with green CI, and auto-remove safe worktrees at run end). Record the choice; it holds for the whole run. When `AskUserQuestion` is unavailable (headless), default to `never`.
5. **Init the run log.** `mkdir -p .piyaz` and guard the gitignore (`grep -qxF '.piyaz/' .gitignore 2>/dev/null || printf '\n.piyaz/\n' >> .gitignore`). If `.piyaz/composer-<projectIdentifier>.md` exists and ends with `RUN_END`, archive it to `.piyaz/archive/composer-<projectIdentifier>-<date>.md` and start fresh; if it exists *without* a `RUN_END`, that is a resume signal — see *Recovering after compaction* first. When the unfinished log's `RUN_START mode=` differs from this invocation, append `RUN_END reason=superseded-by-<mode>`, archive, and start fresh. Then append `RUN_START mode=<...> mergePolicy=<...> project=<identifier>`.

Then start iterating. There is nothing to install and nothing to confirm beyond the merge policy.

## The loop

At the start of each iteration, materialize these todos and mark them off (the todo list is your compaction anchor): pick, launch workflow, handle result, surface verdict, merge gate, propagate.

```dot
digraph composer_iteration {
    "Pick next task" [shape=box];
    "Ready or plannable task?" [shape=diamond];
    "STOP: backlog drained" [shape=doublecircle];
    "Launch compose-task workflow" [shape=box];
    "Result status?" [shape=diamond];
    "Resolve gate with user" [shape=box];
    "Continue this task?" [shape=diamond];
    "STOP: iteration ends (single-task)" [shape=doublecircle];
    "Failure handling" [shape=box];
    "outcome = planned?" [shape=diamond];
    "Surface verdict" [shape=box];
    "Merge gate (per policy)" [shape=box];
    "Propagate" [shape=box];
    "Single-task mode?" [shape=diamond];
    "STOP: iteration complete" [shape=doublecircle];

    "Pick next task" -> "Ready or plannable task?";
    "Ready or plannable task?" -> "STOP: backlog drained" [label="no"];
    "Ready or plannable task?" -> "Launch compose-task workflow" [label="yes"];
    "Launch compose-task workflow" -> "Result status?";
    "Result status?" -> "outcome = planned?" [label="DONE"];
    "Result status?" -> "Resolve gate with user" [label="NEEDS_DECISION"];
    "Result status?" -> "Failure handling" [label="BLOCKED / null"];
    "Resolve gate with user" -> "Continue this task?";
    "Continue this task?" -> "Launch compose-task workflow" [label="yes: relaunch with answers"];
    "Continue this task?" -> "Pick next task" [label="no (backlog)"];
    "Continue this task?" -> "STOP: iteration ends (single-task)" [label="no (single-task)"];
    "outcome = planned?" -> "Single-task mode?" [label="yes (plannable-only)"];
    "outcome = planned?" -> "Surface verdict" [label="no"];
    "Surface verdict" -> "Merge gate (per policy)";
    "Merge gate (per policy)" -> "Propagate";
    "Propagate" -> "Single-task mode?";
    "Single-task mode?" -> "STOP: iteration complete" [label="yes"];
    "Single-task mode?" -> "Pick next task" [label="no"];
    "Failure handling" -> "Single-task mode?";
}
```

### Step details

1. **Pick.** Backlog: `piyaz_map view='ready'` ∩ `view='critical_path'`; rank by priority (`urgent > core > normal > backlog`), tie-break by lowest estimate. Fall back to the highest-priority `ready` task when the intersection is empty, then to `piyaz_map view='plannable'` when `ready` is empty (plannable picks route through research + plan only; mark the pick **plannable-only**). Single-task: the named task; if `done` or `cancelled`, report and stop; if already claimed, see *Failure handling* (jump to the in-flight phase, never restart). Emit a one-paragraph pick rationale (taskRef, priority, estimate, critical-path yes/no, one-sentence reason). Do not wait for approval; the user interrupts if they disagree.

2. **Gather pick facts and launch.** Build the workflow `args` from the pick and bootstrap: `taskRef`, `taskId` (the UUID, carried for cross-referencing; refs are first-class in tool calls — conventions §4), `projectId`, `categories`, `tagVocabulary`, `pickEstimate`, `pickPriority`, `workType` and `tags` (from the task row), `mode`, `plannableOnly`. Write `PICK` then `WORKFLOW task=<ref> runId=<id>` to the run log, then launch the workflow and await the result.

3. **Handle the result.** `NEEDS_DECISION` → *Gates*. `BLOCKED`/null → *Failure handling*. `DONE` with `outcome=planned` (plannable-only) → end the iteration (`TASK_END outcome=planned`); backlog returns to the pick, single-task reports and stops. `DONE` with `outcome=in_review` → step 4.

4. **Surface + merge + propagate.** Quote the final verdict block verbatim (`VERDICT` to the run log). Run the *Merge gate*. Then propagate per lifecycle §3: `piyaz_map view='neighbors' task='<taskRef>'`, `piyaz_map view='downstream' task='<taskRef>'`; update or retire edge notes the work invalidated (edge-note shape: artifacts §3). Propagation depth: full when the PR was merged or the verdict was `approve`; otherwise provisional, each note prefixed `Provisional pending HOTL on PR #<n>:`. Surface newly-unblocked tasks in the next pick rationale. Write `PROPAGATED`, then `TASK_END outcome=in_review rotations=<n>`.

5. **Loop.** Single-task: report the outcome and stop. Backlog: next iteration, no pause.

## Gates

A `NEEDS_DECISION` result means the merged research+plan phase needs a user decision before the task can proceed. `result.phase` names the raising half (`research` or `plan`, from the agent's `gatePhase`) and `result.gate` carries the trigger. Resolve with `the AskUserQuestion tool`, then relaunch the workflow:

- **Oversize** (`oversize-task` flag): offer to dispatch `piyaz:decompose-task` or skip the task. Composer never splits a task itself. On decompose, dispatch the decompose agent and end the iteration; the children land in the backlog.
- **Proposed rewrites** (`result.gate.proposedRewrites` non-empty): show original vs proposed per field with the rationale; offer accept / deny. On accept, apply via `piyaz_edit` and relaunch the workflow **fresh** (no `resumeFrom`) so research re-grounds on the rewritten task. On deny, end the iteration (backlog picks next; single-task stops).
- **Low confidence or external input** (confidence < 0.6, `external-input-required`, or any plan-phase open question): surface the open questions, wait for answers, then relaunch — research gate relaunches **fresh** with `gateAnswers`; a plan gate relaunches with `resumeFrom='plan'`, `priorBrief=result.brief`, and `gateAnswers`, so research is not redone (the merged phase plans from the prior brief).

**Headless gate fallback:** when `AskUserQuestion` is unavailable (errors or hangs), a `NEEDS_DECISION` resolves to skip-the-task: append a `GATE` line carrying the unasked question and the skip, write `TASK_END outcome=skipped`, end the iteration (backlog picks next; single-task stops). Never fabricate an answer; skipping is the reversible default (resilience §11).

## Merge gate

The merge gate runs after a `DONE` result with `outcome=in_review`, governed by the run's merge policy. It fires **only** when `result.verdict === 'approve'` AND `result.ciState === 'green'`; a `request-changes`, `block`, `escalated`, red, or pending result is never merged.

- **`never`** (default): do not merge. HOTL owns the merge and the `in_review → done` transition, exactly as without this feature. Propagate provisionally unless the verdict was `approve`.
- **`ask-each`**: ask `the AskUserQuestion tool` whether to merge this PR. On yes, merge as below. On no (or headless), leave it for HOTL.
- **`auto-on-approve`**: merge without asking.

To merge: `gh pr merge <url> --squash --delete-branch` (squash is the default; follow the repo's configured default method when it differs). On a clean merge, write the task `done` — this is the **one** case the orchestrator writes a status transition, authorized by the run-start merge policy.

The merge is a status flip only; it does not touch the `executionRecord`. The implementer's record already describes what shipped and is the durable record. A HOTL merge leaves it untouched, so `auto-on-approve` leaves it untouched too; that keeps the two paths identical. The PR reference resolves through `task_links`, and `method=squash` lives in the run log.

```
piyaz_edit task='<taskRef>' operations=[{op:'set', field:'status', value:'done'}]
```

Then propagate fully (the work landed) and write `MERGE task=<ref> pr=<url> method=squash` to the run log. **Drop the merged worktree before the next pick:** locate the `.claude/worktrees/wf_*` entry whose `branch` matches the PR's `headRefName` (`git worktree list --porcelain`) and run `git worktree remove <path>` + `git branch -D <branch>` (no `--force`; if git refuses on a dirty or locked tree, surface it and leave it for *Worktree cleanup at run end*). A failed merge (conflict, protected branch, merge-queue required) is not a task failure: report it, leave the task at `in_review` for HOTL, and continue.

## Model selection

The workflow self-selects each phase's model and effort from the pick facts and the research stage's refined estimate/work-type/flags. The orchestrator does not pass models; it passes the pick facts. The table the workflow applies:

| Phase | est 1–2 | est 3 | est 5 | est 8–13 / unset |
| --- | --- | --- | --- | --- |
| Research+Plan | opus | opus | opus | opus |
| Implementer | sonnet (also docs/test/chore) | sonnet if docs/test/chore, else opus | opus | opus |
| CI gate | haiku | haiku | haiku | haiku |
| Reviewer | opus | opus | opus | opus — never downgrade |

Research and plan correctness are load-bearing: a mis-refined task or a vague plan wastes far more downstream tokens than a cheaper model saves, so the merged phase never runs below opus. (CI polling is mechanical, so the cheap haiku tier holds there only.)

Guardrails force opus and higher effort on the research+plan and implement dispatches regardless of estimate when any holds: a `security`/`safety`/`compliance` tag; estimate 8, 13, or missing; a fix-mode rotation; any retry or partial-success recovery; `priority='urgent'`; or a risk-bearing research flag (`security-boundary-uncovered`, `version-drift-major`, `dep-mismatch`). These are encoded in `compose-task.js`; this table is the human-readable mirror.

Fable sits above opus and upgrades the guardrail-fired dispatches. When `args.fable` is not `'off'`, the research+plan, implement, and fix dispatches select fable instead of opus when a guardrail fires on estimate 8+, a risk tag or flag, or `priorFailure`; the final fix rotation always takes the top tier. A failed fable dispatch (no account access, terminal error) falls back to opus and disables fable for the rest of the run. Pass `fable:'off'` when the user declines the tier; the reviewer stays opus and the CI gate stays haiku either way.

## Run log

The run log is composer's crash-safe memory: an append-only event log at `.piyaz/composer-<projectIdentifier>.md`, one active file per project. The conversation can compact; the log does not. Counters derive by grep over events **after the latest `RUN_START`**: this run's iterations = `PICK` lines; failed attempts on task X = `FAIL task=X` lines.

One timestamped line per event, `key=value` pairs; multi-line payloads (blocking findings, gate questions and answers, failure summaries) follow as `> ` continuation lines. The vocabulary:

| Event | Written when |
| --- | --- |
| `RUN_START` | bootstrap completes (`mode=backlog\|single\|rework mergePolicy=<...> project=<identifier>`) |
| `PICK` | step 1 emits the pick rationale |
| `WORKFLOW` | immediately after launching the workflow (`task=<ref> runId=<wf-id>`) |
| `GATE` | a `NEEDS_DECISION` resolves — user answer or headless skip; question and answer as continuations |
| `VERDICT` | the workflow returns DONE (`verdict=<v> rotations=<n> ci=<state> escalated=<bool>`; blocking findings as continuations) |
| `MERGE` | the merge gate merges a PR (`task=<ref> pr=<url> method=squash`) |
| `ESCALATE` | a `block` or rotations-exhausted result goes to HOTL |
| `PROPAGATED` | propagation completes (`edges=<n> unblocked=<refs>`) |
| `BRIEF` | a `--pipelined` prefetch brief lands (`task=<B-ref> baselinedAt=<A-ref>`; brief verbatim as continuations) |
| `FAIL` | the workflow returns BLOCKED (failure summary as continuation) |
| `TASK_END` | the iteration ends (`outcome=in_review\|planned\|stuck\|skipped rotations=<n>`) |
| `RESUME` | recovery appends this after reading the log |
| `RUN_END` | any stop condition (`reason=<...> picked=<n> shipped=<n> merged=<n> stuck=<n> skipped=<n>`) |

Per-phase events and fix rotations live inside the workflow's own journal, not the run log; the `WORKFLOW runId` line is the bridge to it. If `.piyaz/` is not writable, fall back to any writable directory and name the chosen path in the first report; if no local write is possible, run without the log and say so — the run loses crash recovery, not correctness.

## Rework mode

Pull-based: the backend has no webhooks, and `task_links` is the only PR record. The user invokes rework when GitHub review feedback exists; composer fetches it, re-anchors it, and runs the fix loop on it.

1. **Resolve the pair.** Given a taskRef, read `task.links` filtered to `kind='pull_request'`; given a PR URL, resolve the task from the `[<taskRef>]` bracket (verify the link row agrees). Prefer the newest open PR when several exist.
2. **Reviewer-led intake.** Dispatch `piyaz:review` with `Target task: <taskRef>. PR URL: <url>. Mode: rework-intake.` The intake re-verifies the human feedback against current HEAD and returns a verdict.
3. **Branch on the intake verdict.**
   - `request-changes`: launch the workflow with `resumeFrom='fix'`, `prUrl=<url>`, and `fixFindings=<the huma
