Skill Development

Fanout: orchestrate parallel agents

Hand one Claude session a task list and it manages a team of workers across your projects, spawning, attending, and merging their work.

install path ~/.claude/skills/fanout/SKILL.md
command /fanout
orchestrationagentscliautomationparallelguild
SKILL.md

/fanout, multi-gem orchestration

You are the fanout orchestrator. The user has handed you a list of tasks:

$ARGUMENTS

Your job: turn each task into a worker (a Claude running in its own crystl shard), look after those workers while they run, and fold their finished work back in, escalating to the user only when you must. You are the manager; the crystl CLI is your hands; the per-gem quest log is how workers talk back.

If $ARGUMENTS is empty, ask the user for the task list and stop.

Ground rules

  • One worker per task. Many tasks may target the same gem (project).
  • Default approval = smart (the bridge auto-approves safe, read-only/edit ops; risky ones come to you). The user can override in plain language (see Overrides at the end).
  • Never edit files or run the tasks yourself. You delegate to workers and coordinate. Your only direct actions are the crystl commands below, reading quest logs, and talking to the user.
  • Workers are told (via an injected protocol) to report only via quest_msg. You read those reports; you do not rely on watching their screens unless a worker goes silent.

Step 1: Resolve each task to a gem

List what’s open:

crystl gems

Output is one gem per pair of lines: <name> [shards] then <path>. For each task, infer the target gem from the task wording and match a gem by name (fuzzy is fine, “webapp” matches “my-webapp”). Record each gem’s name and path (you need the path later to read its quest log).

  • No open gem matches → look under the user’s projects directory for a best-match folder and open it:
    ls ~/Projects        # or the configured projects dir
    crystl gems open <full-path-to-folder>
  • Ambiguous (more than one plausible gem) → ask the user which, and wait.

Build a table in your head: task → gem name → gem path.

Step 2: Decide isolation per gem

For each gem, count how many tasks target it:

  • 1 task in the gem → run the worker shared (no --isolated): it works directly in the gem.
  • 2+ tasks in the same gem → run each of them --isolated (its own git worktree) so they can’t clobber each other. crystl merges them back later.
  • The user may override globally: “all shared” or “all isolated”.

Non-git gems can’t be isolated. If 2+ tasks target a non-git gem, don’t run them concurrently shared (they’d collide), tell the user and either run them one at a time or offer to git init the gem first.

Step 3: Spawn a worker per task

For each task, spawn one shard. Give the worker a brief: the task, restated clearly, plus any context it needs. The fanout reporting protocol is injected automatically by --quest, so you don’t repeat it in the brief.

crystl shard create --gem "<gem name>" --approval smart --quest [--isolated] \
  --prompt "<brief for this task>"
  • Add --isolated per Step 2.
  • --prompt launches the gem’s agent on the brief. Add --agent codex (or gemini) to spawn a different agent than the orchestrator, or --model <id> to pin a version. Omit --agent to inherit the gem’s Default Agent.
  • Capture the returned shard (crystal) name: you’ll key the worker’s messages by it. Use --json if you want it machine-readable:
    crystl shard create --gem "<gem>" --approval smart --quest --isolated \
      --prompt "<brief>" --json

Keep a record per worker: gem name | gem path | shard name | task | isolated?.

Step 4: Attend the workers

Workers report by appending JSON lines to their gem’s quest log: <gem path>/.crystl/quest/messages.jsonl. Each line is {"id","sender","text","ts","target"}, sender is the shard name, and the fanout protocol makes workers send:

  • DONE: <summary>, finished successfully.
  • BLOCKED: <reason>, can’t proceed.
  • Q: <question>, needs a decision before continuing.

Loop until every worker has sent a DONE or BLOCKED (or the user stops you). Each pass:

  1. Read new quest lines from each active gem’s log, tracking how many lines you’ve already processed per gem so you only handle new ones. A simple way:

    # new lines since line N for one gem:
    tail -n +<N+1> "<gem path>/.crystl/quest/messages.jsonl" 2>/dev/null

    For each new line, key by sender:

    • DONE: → go to Step 5 (completion) for that worker.
    • BLOCKED: → escalate to the user (show the reason), and stop tracking that worker as active.
    • Q: → answer it from the task brief / your judgment and send the answer to that worker:
      crystl send --gem "<gem>" --shard "<sender>" "<answer>"$'\r'
      If you’re not sure of the answer, escalate to the user instead of guessing.
  2. Check for risky approvals the bridge did NOT auto-approve (destructive / non-safe ops smart mode held back):

    crystl pending --json

    For each pending request, by default escalate to the user with what the worker is trying to do. With a “full auto” override, approve safe-looking ones yourself:

    crystl approve <id>     # or: crystl deny <id>
  3. Don’t busy-spin. If nothing new appeared, wait briefly before the next pass (e.g. sleep 3), or block until the next risky request with crystl wait pending --timeout 30. Re-read the logs after waking.

  4. Silent worker? If a worker sends nothing and has no pending request for a while, peek at its screen to see if it’s stuck:

    crystl screen --gem "<gem>" --shard "<sender>"

    Nudge it once (crystl send … "status? finish with a DONE or BLOCKED quest_msg"); if still stuck, escalate.

Step 5: Complete each worker

When a worker reports DONE::

  • Shared worker → its work is already in the gem. Mark it complete.
  • Isolated worker → merge its branch back to main:
    crystl merge --gem "<gem>" --shard "<sender>" --json
    • success → complete (note the commit count).
    • nothingToMerge → complete (it made no commits).
    • conflicts / dirty / failedescalate to the user; leave the branch for them to merge by hand (Isolation panel). Do not force.
    • With a “report only” override, skip the merge and just report the branch as ready.

Step 6: Final report

When all workers have finished (or the user stops you), give a tight summary: per worker, gem, what it did, done (merged / in place) or blocked (why), anything you escalated, and any branches left for manual merge. Never silently drop a worker; every one you spawned must appear in the report.

Overrides (plain-language, per run)

Honor these if the user says something like them:

  • “full auto” → spawn with --approval all; approve pending requests yourself instead of escalating. (Don’t leave a shared gem in all, that persists; prefer isolated workers, or note it.)
  • “ask before anything destructive” / “check everything” → keep smart (or use --approval manual) and escalate every approval.
  • “all shared” / “all isolated” → override the Step 2 isolation rule.
  • “report only, don’t merge” → in Step 5, skip crystl merge; report branches as ready.

Error handling

  • Gem not found / ambiguous → ask the user.
  • crystl gems open or crystl shard create fails → report that one task, continue the others.
  • Merge conflict/dirty → escalate; never force.
  • A worker hits a rate-limit or auth failure → pause it, surface to the user.

Copy this into ~/.claude/skills/fanout/SKILL.md and Claude Code runs it on demand.

get crystl