- name
- skill-orchestrate
- description
- Autonomous state machine that drives a task through its full lifecycle (research -> plan -> implement -> complete) without user confirmation between phases. Invoke for /orchestrate command.
- allowed-tools
- Agent, Bash, Read, Edit, AskUserQuestion
# Orchestrate Skill
Fire-and-forget autonomous loop implementing the task lifecycle state machine. One engine drives
every invocation — a single task number is a batch of size one through the SAME four-move loop a
many-task batch uses; there is no separate single-task code path. Full state table, transition
diagram, and design rationale: `docs/architecture/orchestrate-state-machine.md`.
## Context References
- `.claude/scripts/orchestrate-cycle-plan.sh` — Move 1: per-cycle status refresh, eligibility,
admission, routing, and dispatch-file composition
- `.claude/scripts/orchestrate-build-dispatch.sh` — Move 1's dispatch-file writer (called
internally by `orchestrate-cycle-plan.sh` for every row)
- `.claude/scripts/orchestrate-cycle-postflight.sh` — Move 3: the shared per-task postflight body
- `.claude/docs/architecture/orchestrate-state-machine.md` — state table, loop diagram,
`mt_state_file` field reference, exit-status resolution, and `handoff-schema.md` cross-reference
---
## The Four-Move Loop
**Setup (once per invocation, before the loop begins)** — from delegation context:
`task_numbers`, `dependency_graph`, `session_id`, `lit_flag` (Move 1's `--lit` passthrough runs
`context/patterns/lit-stage4a-flow.md`'s resolver directives inside `orchestrate-build-dispatch.sh`
for every per-task dispatch), `compare_flag`,
`allow_self_modifying`, `allow_scope_collision`, `clean_flag`, `effort_flag`, `model_flag`,
`hard_mode` (`"true"` iff `effort_flag = "hard"`), `force_phases`, `continue_budget`. Register the
batch's in-flight session (best-effort, non-blocking):
```bash
bash .claude/scripts/task-lock.sh session-register "$session_id" "/orchestrate" \
"$(IFS=,; echo "${task_numbers[*]}")" 2>/dev/null || true
```
For each task in `task_numbers`, run the entry reconcile ONCE (never per-cycle — this loop below
runs it again next cycle only for a task that is still eligible then, which is a fresh call, not
a repeat):
```bash
for task_number in "${task_numbers[@]}"; do
recon_out=$(bash .claude/scripts/reconcile-task-status.sh "$task_number" "$session_id" 2>&1 || true)
[ -n "$recon_out" ] && echo "$recon_out"
done
```
### Move 1: Plan the cycle
One call. Status refresh, all-terminal check, eligibility, classification, admission (the four
defer gates and their overrides), the self-modification tie-breaker, the convergence guard, the
idle cross-batch overlap advisory, `force_phases` consumption, task-directory creation, the
lock probe, budget accounting, the inter-cycle redeploy checkpoint, and dispatch-file composition
(via `orchestrate-build-dispatch.sh`, including any `## Prior Decisions` section from a task's
`.decisions.json`) all happen inside this one script call — full contract in its own header
comment and in `orchestrate-state-machine.md`.
```bash
plan_json=$(bash .claude/scripts/orchestrate-cycle-plan.sh \
--session "$session_id" --state-file specs/state.json \
$( [ -n "${force_phases:-}" ] && echo --force-phases "$force_phases" ) \
$( [ -n "${model_flag:-}" ] && echo --model "$model_flag" ) \
$( [ "${clean_flag:-false}" = "true" ] && echo --clean ) \
$( [ "${lit_flag:-false}" = "true" ] && echo --lit ) \
$( [ "${compare_flag:-false}" = "true" ] && echo --compare ) \
$( [ "${hard_mode:-false}" = "true" ] && echo --hard ) \
$( [ "${effort_flag:-}" = "fast" ] && echo --fast ) \
$( [ "${allow_self_modifying:-false}" = "true" ] && echo --allow-self-modifying ) \
$( [ "${allow_scope_collision:-false}" = "true" ] && echo --allow-scope-collision ) \
$( [ "${continue_budget:-false}" = "true" ] && echo --continue-budget ) \
"${task_numbers[@]}")
stop_json=$(echo "$plan_json" | jq -c '.stop')
mt_state_file="specs/.orchestrator-multi-state-${session_id}.json"
```
If `stop_json` is non-null: log `.reason`/`.message`, skip to Move 4 (`all_terminal` is a success
exit; `max_cycles`/`no_eligible_stuck`/`max_infra_failures`/`convergence_guard` are partial
exits). Otherwise continue with this cycle's `plan_json.dispatch[]`/`plan_json.aux_dispatch[]`.
**Hard-mode burnout gate (`--hard` only, every cycle, before Move 2)**: the same three
self-checks single-task mode used to run (re-reading a path with no new dispatch since, a second
consecutive no-dispatch reasoning turn, reversing a decision without a fresh finding) — on any
signal, call `bash .claude/scripts/orchestrate-churn.sh --burnout-signal "$task_dir_abs"` for the
task the signal fired on. See `context/contracts/orchestrator-discipline.md` for the full
self-check text.
### Move 2: Dispatch
Issue every Agent call named by `dispatch[]` AND `aux_dispatch[]` in **one message** (concurrent
execution — multiple messages force sequential execution). Each row already carries every field
`orchestrate-build-dispatch.sh` (or its aux counterpart) resolved; the prompt is a fixed pointer.
```bash
echo "$plan_json" | jq -c '.dispatch[]' | while IFS= read -r row; do
t=$(jq -r .task <<<"$row"); phase=$(jq -r .phase <<<"$row"); agent=$(jq -r .agent <<<"$row")
model=$(jq -r '.model // empty' <<<"$row"); dispatch_file=$(jq -r .dispatch_file <<<"$row")
task_dir_abs="${SKILL_REPO_ROOT:-$(pwd)}/$(jq -r --arg t "$t" '.task_dirs[$t]' "$mt_state_file")"
dispatch_seq=$(jq -r --arg t "$t" '.dispatch_seq[$t]' "$mt_state_file")
ctx_sid="$session_id"; [ "$phase" != "implement" ] && ctx_sid="${session_id}_${t}"
# Agent tool: subagent_type: agent (model param if non-empty). Prompt: "You are dispatched by
# /orchestrate for task $t, phase $phase. Read $dispatch_file first and execute it exactly; it
# names every input, output path and contract." Context: { task_number: t,
# orchestrator_mode: true, session_id: ctx_sid, task_dir: task_dir_abs,
# handoff_path: "${task_dir_abs}/.orchestrator-handoff.json", dispatch_seq }
done
echo "$plan_json" | jq -c '.aux_dispatch[]' | while IFS= read -r row; do
t=$(jq -r .task <<<"$row"); kind=$(jq -r .kind <<<"$row"); agent=$(jq -r .agent <<<"$row")
dispatch_file=$(jq -r .dispatch_file <<<"$row")
task_dir_abs="${SKILL_REPO_ROOT:-$(pwd)}/$(jq -r --arg t "$t" '.task_dirs[$t]' "$mt_state_file")"
# Agent tool: subagent_type: agent. Prompt: "You are dispatched by /orchestrate for task $t
# (auxiliary: $kind). Read $dispatch_file first and execute it exactly." Context: { task_number:
# t, orchestrator_mode: false, session_id: session_id, task_dir: task_dir_abs } — NO
# handoff_path key at all: an aux dispatch never writes .orchestrator-handoff.json.
done
```
**MUST NOT**: an `aux_dispatch[]` row never reaches Move 3 and never contributes to
`failed_tasks` — its only effect is a written file or a revised plan a later cycle picks up.
`agent` for an aux row is FIXED by `kind` (`fork`/`fork`/`reviser-agent`/the task's own resolved
research agent), never task-type-routed — see `orchestrate-cycle-plan.sh`'s header (Decision 2).
Log every `plan_json.deferred[]`/`plan_json.blocked[]` row's `reason` verbatim — informational
only; a deferred task becomes eligible again on a later cycle.
### Move 3: Postflight
**After ALL Agent calls from Move 2 complete** (never interleaved with dispatch), run this once
per `dispatch[]` row — the single shared implementation for every task, every phase, every effort
mode:
```bash
echo "$plan_json" | jq -c '.dispatch[]' | while IFS= read -r row; do
t=$(jq -r .task <<<"$row"); phase=$(jq -r .phase <<<"$row"); agent=$(jq -r .agent <<<"$row")
force=$(jq -r .force <<<"$row")
task_dir_rel=$(jq -r --arg t "$t" '.task_dirs[$t]' "$mt_state_file")
task_type=$(jq -r --argjson n "$t" \
'.active_projects[] | select(.project_number == $n) | .task_type // "general"' specs/state.json)
plan_path_for_task=$(ls -1 "${task_dir_rel}/plans/"*.md 2>/dev/null | sort -V | tail -1)
# task_transport_error: true only when THIS task's own Agent call returned a transport/API
# error with no subagent-authored text at all (context/patterns/infra-failure-discrimination.md)
postflight_json=$(bash .claude/scripts/orchestrate-cycle-postflight.sh "$t" \
--session "$session_id" --state-file specs/state.json --phase "$phase" \
--task-dir "$task_dir_rel" --task-type "$task_type" --agent "$agent" \
--plan-path "$plan_path_for_task" --cycle-count "${cycle_count:-0}" \
--transport-error "${task_transport_error:-false}" --force-invoked "$force" \
$( [ "${hard_mode:-false}" = "true" ] && echo --hard ))
dispatch_status=$(echo "$postflight_json" | jq -r '.status')
verdict=$(echo "$postflight_json" | jq -r '.verdict')
halt=$(echo "$postflight_json" | jq -r '.halt')
infra_exempt_cycle=$(echo "$postflight_json" | jq -r '.infra_exempt_cycle')
echo "[orchestrate] Task #${t}: dispatch result: $dispatch_status (verdict=$verdict)" >&2
# ask_user verdicts accumulate for Move 4's batched relay — never asked here, never per-task.
if [ "$verdict" = "ask_user" ]; then
jq --argjson tn "$t" --argjson q "$(echo "$postflight_json" | jq -c '.user_decision')" \
'.pending_ask_user = ((.pending_ask_user // []) + [{"task": $tn, "decision": $q}])' \
"$mt_state_file" > "${mt_state_file}.tmp" && mv "${mt_state_file}.tmp" "$mt_state_file"
fi
if [ "$halt" = "true" ]; then
echo "[orchestrate] Task #${t}: OFF-SCHEMA — charged to failed_tasks inside the script's own postflight. Sibling tasks unaffected." >&2
fi
# Two cases the script does not itself resolve (loop-control, not outcome bookkeeping):
if [ "$verdict" = "defer" ] && [ "$infra_exempt_cycle" = "true" ]; then
task_infra=$(jq -r --arg t "$t" '.infra_failures[$t] // 0' "$mt_state_file" 2>/dev/null) || task_infra=0
if [ "$task_infra" -ge "${MAX_INFRA_FAILURES:-3}" ]; then
jq --argjson tn "$t" '.failed_tasks = ((.failed_tasks // []) + [$tn] | unique)' \
Auf GitHub ansehen