The composer loop
Turn one raw idea into a merged-ready spec PR in the right repo. This guide walks the composer loop (ai-conductor compose) end to end, for an operator who has an idea and wants a spec the daemon can build.
The composer loop never builds and never merges. It opens a spec PR; you merge it; the daemon picks it up from the default branch afterwards.
Prerequisites
| Requirement | Check |
|---|---|
ai-conductor on PATH | ai-conductor --help |
claude on PATH (for a Claude launch) | claude --version |
codex on PATH or CODEX_EXECUTABLE (for a Codex launch) | codex --version |
| At least one registered project | ai-conductor compose projects prints a non-empty JSON array |
gh authenticated, for intake and PR steps | gh auth status |
Register a repo with ai-conductor register <path>, or scaffold a new one with ai-conductor create <name>. See cli reference for both.
How the loop is driven
There are two surfaces and you use both:
- The front door. Bare
ai-conductor composestarts the selected interactive host with stdio inherited: Claude runs/composer; Codex runs$composer. The human stays in the loop. - The primitives.
projects,claim,worktree,land,handoff, and the recovery verbs are deterministic CLI commands. The skill calls them from in-chat reasoning; you can also call them by hand.
Every primitive prints a single JSON line on success, so each step’s output feeds the next.
For Medium and Large work, authoring runs coherence_check immediately after plan and commits .docs/coherence/<plan-stem>.md with the spec artifacts. Small work skips both the gate and the artifact, allowing the daemon to begin at BUILD after the spec PR merges.
Start a session
ai-conductor compose
You should see the selected interactive host start on its composer prompt: Claude uses /composer and Codex uses $composer. Before the session launches, the CLI polls GitHub issues into the durable inbox and prints Intake: N issue(s) queued. when N is above zero. That pre-poll is skipped when a background brain loop is already running (it owns polling) and skipped when you supply an idea on the command line.
Variants:
ai-conductor compose --idea "<your idea>"
ai-conductor compose <free text idea>
ai-conductor compose --provider codex
ai-conductor compose --provider codex --idea "<your idea>"
Both drive the first session with that idea and skip the intake pre-poll. The idea is one-shot: it applies only to the first session, and later iterations fall back to intake or chat.
--provider selects the interactive host for the session and overrides the configured provider. It can be used with or without --idea.
When the session exits, the launcher asks Process another idea in a fresh session? [Y/n] on a TTY. Answering yes starts a clean session — one idea per session, by design. On a non-TTY stdin the launcher never loops.
If you are already inside a supported host session, ai-conductor compose refuses to nest a second one. It prints guidance and exits 0: run /composer in Claude Code or $composer in Codex.
For Claude launches, the permission mode comes from CONDUCT_ENGINEER_PERMISSION_MODE and defaults to default. The value plan is rejected and coerced back to default, because a read-only session cannot run the git and gh primitives. Codex ignores this setting. See environment reference.
Step 1 — Capture the idea
Ask the inbox first:
ai-conductor compose claim
Outcomes:
| Output | Meaning | Next |
|---|---|---|
{"kind":"claim","text":"…","sourceRef":"owner/repo#N", …} | An intake idea was claimed | Carry sourceRef through steps 3–5 |
{"empty":true} | Nothing pending | Use the launch argument or the operator’s chat idea |
{"allBlocked":true,"entries":[…]} | Everything queued is blocked by an open dependency | Resolve or reprioritise the blockers |
claim exits 0 in all three cases. It holds the inbox lease for the whole walk, so a concurrent claim waits (up to 5 minutes) instead of racing. It first returns inbox envelopes left claimed by an interrupted claim — ledger entry still pending — to the inbox with their original priority, and reports released <n> stranded intake claim(s) on stderr. It then reaps claimed entries stranded longer than the stale_claim_window_hours window, returns them to pending, and can re-serve a reaped entry in the same call. The default window is 24 hours; set stale_claim_window_hours in project configuration to change it. It then acks the selected queue entry, advances the intake ledger to claimed, and persists a claim record so a later worktree --source-ref can recover the issue’s Desired-outcome bullets without you re-typing them.
claim exits 1 and claims nothing when the lease wait times out (Intake claim in progress), the intake ledger is corrupt, or stranded-claim recovery cannot read or release an envelope. Fix the reported fault, then run claim again.
Ideas that came from a launch argument or from chat have no sourceRef — omit --source-ref for those.
For what a good intake issue contains, see filing intake issues.
Step 2 — Route to a target repo
ai-conductor compose projects
Prints the registry as JSON. Pick the best-fit project, state the rationale, and confirm the target with the operator before going further. If nothing fits, scaffold a new project with ai-conductor create and continue with it.
Step 3 — Create the per-idea worktree
ai-conductor compose worktree \
--project <name> \
--idea "<idea>" \
--source-ref <owner/repo#N>
You should see {"kind":"worktree","slug":"…","branch":"spec/<slug>","worktreePath":"…","reconcile":"…"}.
worktreePathis<target>/.worktrees/engineer-<slug>, checked out on a freshspec/<slug>branch. It is your working directory for every remaining step of this idea.reconcilereports how a leftover from a prior failed run was handled:created,reused, orattached. A dirty leftover is refused — recreate it.- Failure exits 1 and makes zero changes to the target’s primary tree. Do not fall back to the primary checkout; fix the error and retry.
--source-ref is optional and normally comes from an intake-claimed idea. With --source-ref and no --body, the command first loads the Desired-outcome body from the claim record written at claim time. If no record is available, it reads the referenced GitHub issue through the configured tracker instead. A resolved empty or bulletless body still creates an outcomes file with the source reference and zero bullets. An unreadable, missing, or unparseable source leaves no outcomes file, prints a diagnostic that names --body, and still creates the worktree. --body always takes precedence.
Known limitation.
--source-refand--bodyare both parsed and honoured bycomposer worktree, but neither is declared in the commander tree, so help output omits them. If you pass--body "<text>"it wins over the claim record or issue body. Tracked in #1012.
Step 4 — Run DECIDE inside the worktree
With worktreePath as the working directory, run the real DECIDE skills in canonical order. The engineer owns the whole DECIDE phase; the daemon only builds. Every artifact is written inside the worktree, never the primary checkout.
/explore— discovery and the confirmed track.- Complexity assessment — write the tier to
.docs/complexity/<plan-stem>.md. The stem must match the plan filename or the daemon cannot resolve it. /prd— product track only./architecture-diagram— skipped at tier S./architecture-review— skipped at tier S. Every ADR must be APPROVED before landing. Each ADR added or changed by the spec must also declare at least one citable decision: use a numbered item in its## Decisionsection (preferred), a boldD<number>heading, or an ATXD<number>heading./stories— must endStatus: Accepted./conflict-check— skipped at tier S./plan./coherence-check— tiers M and L only.
Do not hand-write stub or DRAFT artifacts. See steps reference for the per-step tier-skip and enforcement table, and SDLC phases for why the order is fixed.
Step 5 — Land the spec
ai-conductor compose land \
--project <name> \
--idea "<idea>" \
--worktree <worktreePath> \
--source-ref <owner/repo#N>
land commits the already-authored .docs/ artifacts in place on the worktree’s spec/<slug> branch. It authors nothing. Its commit body summarizes the available plan summary, track and tier, story headings, and task count; handoff uses that body to prefill the spec PR description. You should see {"slug":"…","branch":"spec/<slug>","repoPath":"…"} — pass branch and the same --worktree to step 6.
Before committing, land refuses on any of:
- a missing required artifact for the recorded tier,
- any artifact carrying
Status: DRAFT, - an ADR under
.docs/decisions/whose first declared status is notAPPROVEDorSUPERSEDED, or that declares no status at all, - an added or changed approved ADR with no citable decision in its
## Decisionsection, - an empty or stub artifact,
- uncommitted changes to tracked files at any path, including tracked artifacts under
.docs/, - untracked files outside
.docs/, - an unresolved identity (no
spec_ownerconfigured and noghlogin).
--worktree is required. land never falls back to the primary checkout. On failure the worktree is kept for inspection and its path is printed.
With --source-ref, land also comments “Routed to <repo>” on the originating issue and advances the ledger to routed. That write-back is advisory: a gh failure never fails a successful land.
Known limitation.
ai-conductor compose land --helpclaims land will “open the spec PR” and that it “pushes thespec/<slug>branch, opens a PR”. The code does neither: thelanddispatch arm calls onlylandSpec, which commits in the worktree and returns. The push (git push -u origin <branch>) andgh pr createhappen inhandoff. The commander description, theai-conductor composeguide text, and the source grammar comment all agree with the code — only the per-subcommand help text disagrees. If you stop afterland, nothing has been pushed and no PR exists. Tracked in #1012.
Step 6 — Hand off: push, PR, daemon nudge
ai-conductor compose handoff \
--project <name> \
--branch <branch> \
--worktree <worktreePath> \
--source-ref <owner/repo#N>
handoff runs git push -u origin <branch> and gh pr create --label spec from inside the per-idea worktree, so the PR opens for spec/<slug> with the spec label. On success you should see one of:
| Output | Meaning |
|---|---|
{"kind":"pr-opened","url":"…"} | The spec PR exists |
{"kind":"local-commit","branch":"…","repoPath":"…","reason":"no remote configured"} | No remote; work persists on the branch |
Then it removes the per-idea worktree (the branch and commit persist), and fires ensureRunning(<target>) so the target repo’s daemon is alive to pick the spec up after you merge. That last call is fire-and-forget but never silent: on a host without tmux you get ⚠ Spec authored, but the build daemon was not started for "<name>": … on stderr while the command still exits 0.
With --source-ref, handoff comments the PR URL on the originating issue, adds a non-closing Refs <ref> to the PR body, applies the engineer:handled label, and advances the ledger to done. The originating issue’s assignees remain unchanged from claim through handoff verification and cleanup; engineer:handled marks completion without changing ownership.
On failure handoff exits 1, keeps the worktree, prints its path, and records branch evidence in the ledger so you can recover with composer resolve.
Step 7 — Deliver, then end the session
Report the PR URL and stop. In a Claude Code session, /quit and relaunch for the next idea — a fresh session per idea is the point. Durable state (registry, ledger, claim records) is file-backed, so nothing is lost across sessions.
The spec is not built until you merge the PR. The daemon reads specs from the committed default branch only; an unmerged spec/<slug> branch is invisible to it. See running the daemon.
Recovering a stranded entry
When handoff’s ledger write-back fails but the PR was opened, the entry is stuck at claimed. Stamp it by hand:
ai-conductor compose resolve <owner/repo#N> --pr-url <url> --branch <branch>
--pr-url must match ^https?:// — an invalid URL exits 1. A missing entry prints {"found":false} and exits 0. --branch is optional; omitting it preserves any branch already recorded.
To put an issue back in the pool instead:
ai-conductor compose unclaim <owner/repo#N>
unclaim returns a claimed entry to pending, preserving its original capture time so the next claim can select it. An absent or non-claimed entry is reported without changing state or failing. A claimed entry that already has a recorded PR is delivered in fact — unclaim refuses it and tells you to use resolve or forget instead. Use forget only when the issue should be removed from the ledger and made eligible for a later poll:
ai-conductor compose forget <owner/repo#N>
To recover every stale claim at once, run:
ai-conductor compose requeue --stale [--older-than <dur>]
Without --older-than, the sweep uses stale_claim_window_hours (24 hours by default); the optional duration overrides that window for this run — an unparseable duration exits 1 without touching the ledger. It requeues stranded claimed entries that have no recorded PR, removes entries only when their source issue is confirmed closed, and reports liveness-read errors without removing the entry. Claimed entries that already have a PR are reserved for resolve/forget and are never touched.
Maintenance commands
| Command | Effect |
|---|---|
ai-conductor compose poll | One synchronous sweep of the GitHub issues adapter into the durable inbox. No routing, no background process. The ledger dedups, so a double-poll enqueues nothing new. |
ai-conductor compose unclaim <sourceRef> | Returns one claimed entry to pending so it can be claimed again. Use it for a known stranded claim. |
ai-conductor compose requeue --stale [--older-than <dur>] | Bulk-recovers stale claimed entries. The default age is stale_claim_window_hours (24 hours); --older-than overrides it once. |
ai-conductor compose migrate-issue-deps | One-time prose-to-structured-link dependency migration. Dry-run by default; prints the proposal and Dry run — no links written. Re-run with --confirm to apply. |
ai-conductor compose migrate-issue-deps --confirm | Applies the migration and prints N link(s) created, M already present. |
Troubleshooting
A subcommand printed the whole guide and exited 0. For worktree, land, handoff, forget, and resolve, a missing required flag or positional prints the full guide text and exits 0, not a usage error. Check the exit code is not enough — confirm you got the JSON line you expected before moving to the next step.
composer <sub>: unknown flag '<flag>', exit 1. Each subcommand rejects any flag outside its own allow-list. --help and -h are checked before the subcommand’s own logic, so ai-conductor compose land --help always prints help and exits 0 with zero side effects.
engineer: could not launch <provider> executable <executable>. The selected host executable is unavailable. Install it or set the named override environment variable (for example, CODEX_EXECUTABLE), or run the named in-session command (/composer or $composer). The command prints the guide and exits 1.
Cannot land spec: identity unresolved. Set spec_owner in ~/.ai-conductor/config.yml or run gh auth login. See configuration reference.
For every flag, exit code, and JSON shape, see cli reference.