- 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
# Orchestrate Skill
Fire-and-forget autonomous loop implementing the 10-state task lifecycle state machine.
Drives research, planning, implementation, and blocker escalation without user interaction.
## Context References
Architecture documentation (load as needed):
- `.claude/docs/architecture/orchestrate-state-machine.md` - Complete state table and transition diagram
- `.claude/docs/architecture/handoff-schema.md` - Orchestrator handoff JSON schema
Infrastructure (source as needed):
- `.claude/scripts/skill-base.sh` - Shared skill lifecycle functions
---
## Execution Flow
### Stage 0: Multi-Task Mode Detection
Parse `multi_task_mode` from the delegation context. If true, branch to multi-task stages (MT-1 through MT-5) in the **Multi-Task Mode** section below. If false or absent, fall through to Stage 1 (single-task mode).
Read from delegation context:
- `multi_task_mode` (default: false)
- `session_id`
- `focus_prompt` (default: "")
- `lit_flag` (default: "false")
If `multi_task_mode` is true: skip Stages 1-8 entirely and proceed to Stage MT-1.
---
### Stage 1: Input Validation
Read from delegation context:
- `task_number` (from `task_context.task_number`)
- `session_id`, `focus_prompt`, `lit_flag`
- `continue_budget` (default: `false`) → `continue_budget_flag`. Defect B: explicit,
operator-typed budget-continuation override for an exhausted work-cycle budget, threaded from
the command's `--continue-budget` flag. Never inferred from `session_id`, mtime, or any
automatic signal. See Stage 2 below for the override mechanism.
Resolve from `specs/state.json`:
```bash
PADDED_NUM=$(printf "%03d" "$task_number")
TASK_DATA=$(jq -r --argjson num "$task_number" \
'.active_projects[] | select(.project_number == $num)' \
specs/state.json)
```
If `TASK_DATA` is empty: exit with error "Task $task_number not found in state.json".
Extract: `PROJECT_NAME`, `TASK_TYPE` (default: "general"), `DESCRIPTION`, `TASK_DIR="specs/${PADDED_NUM}_${PROJECT_NAME}"`.
Then resolve the absolute anchor that every dispatched agent will be handed. `TASK_DIR` stays
relative (many consumers below depend on that); `TASK_DIR_ABS` is the anchor that goes into
delegation contexts, because a dispatched agent has no reliable way to know what the ambient
working directory will be when its Write tool runs.
```bash
# SKILL_REPO_ROOT is exported by skill-base.sh, which resolves it from BASH_SOURCE rather than
# from the ambient cwd. $(pwd) is a last-resort fallback for direct invocation.
TASK_DIR_ABS="${TASK_DIR_ABS:-${SKILL_REPO_ROOT:-$(pwd)}/${TASK_DIR}}"
HANDOFF_PATH_ABS="${TASK_DIR_ABS}/.orchestrator-handoff.json"
```
### Stage 1b: Resolve Task-Type Routing
Map task_type to the correct research, plan, and implementation agents via the single canonical
agent resolver, `command-route-agent.sh` — sourced from the shared manifest-routing-lib.sh
ladder (the same one `command-route-skill.sh` uses), against each manifest's `routing_agents`
declarations. No case table, no directory probe, no sed derivation: agent names are declared
data, not derived strings (see `context/guides/manifest-routing-schema.md`).
```bash
source .claude/scripts/command-route-agent.sh "research" "$TASK_TYPE" "general-research-agent" ""
RESEARCH_AGENT="$AGENT_NAME"
source .claude/scripts/command-route-agent.sh "plan" "$TASK_TYPE" "planner-agent" ""
PLANNER_AGENT="$AGENT_NAME"
source .claude/scripts/command-route-agent.sh "implement" "$TASK_TYPE" "general-implementation-agent" ""
IMPLEMENT_AGENT="$AGENT_NAME"
echo "[orchestrate] Task type: $TASK_TYPE → research=$RESEARCH_AGENT, plan=$PLANNER_AGENT, implement=$IMPLEMENT_AGENT"
```
### Stage 2: Loop Guard Initialization
Create or read the loop guard file. This tracks cycle count across conversational turns.
**Ephemeral, never committed.** `.orchestrator-loop-guard` is per-cycle runtime state with no
freshness check on read (see the resume branch below: any syntactically valid guard file at this
path is trusted, with no `session_id` or mtime comparison against the current dispatch). A
git-restored copy of a stale guard would silently resume a wrong `cycle_count`/`infra_failures`
pair — exactly the hazard this file's gitignore coverage exists to prevent. See
`context/standards/orchestrator-runtime-files.md` for the full two-class policy and rationale.
```bash
MAX_CYCLES=5
# Single shared implementation, orchestrate-loop-guard-init.sh — see that script's header for
# the full contract (MAX_INFRA_FAILURES constant, loop_guard_file/handoff_file assignment,
# mkdir -p "$TASK_DIR", and the blocker-escalation counter pair applied further below in this
# same fence). This is the same call skill-orchestrate-hard/SKILL.md's Stage 2 makes for its own
# genuinely-common portion — everything else in this stage (MAX_CYCLES's own value, the
# hard-only loop-guard-staleness detector, churn-state init) stays per-engine, either because it
# differs or because it sits inside the locked budget-continuation-override region below, which
# this script and its call site never touch.
loop_guard_init_json=$(bash .claude/scripts/orchestrate-loop-guard-init.sh "$TASK_DIR" "${HANDOFF_PATH_ABS}")
loop_guard_file=$(echo "$loop_guard_init_json" | jq -r '.loop_guard_file')
handoff_file=$(echo "$loop_guard_init_json" | jq -r '.handoff_file')
MAX_INFRA_FAILURES=$(echo "$loop_guard_init_json" | jq -r '.max_infra_failures')
# --- budget-continuation-override:begin ---
# Defect B: cycle_count is a per-task, CUMULATIVE budget that survives re-invocation BY DESIGN --
# it is deliberately NOT reset on a new session_id, because that would let an operator silently
# bypass MAX_CYCLES by simply re-invoking /orchestrate. test-session-runtime-files.sh Case 3 is
# the regression protecting this decision; this override must never disturb it. The override
# below is the sanctioned, explicit, loudly-logged escape hatch for a genuinely exhausted budget
# -- never automatic, never session_id-gated, never inferred from mtime. Verbatim-twin mechanism
# to skill-orchestrate-hard/SKILL.md's Stage 2 (base mode has no loop-guard-staleness detector to
# sit after, so this runs immediately before the pre-existing resume-read block below).
if [ -f "$loop_guard_file" ] && jq empty "$loop_guard_file" 2>/dev/null; then
peek_cycle_count=$(jq -r '.cycle_count // 0' "$loop_guard_file")
if [ "$peek_cycle_count" -ge "$MAX_CYCLES" ]; then
if [ "$continue_budget_flag" = "true" ]; then
exhaust_ts=$(date -u +%s)
exhausted_guard_dest="${TASK_DIR}/.exhausted-loop-guard-${exhaust_ts}.json"
echo "[orchestrate] BUDGET EXHAUSTED (cycle_count=${peek_cycle_count}/${MAX_CYCLES}) — --continue-budget authorized a fresh budget. Archiving exhausted guard to ${exhausted_guard_dest} for auditability, reinitializing at cycle_count=0 with cross-invocation history fields (dispatch_seq_counter, detected_defects) preserved." >&2
if cp "$loop_guard_file" "$exhausted_guard_dest" 2>/dev/null; then
# Reinit IN PLACE from the just-archived copy: reset only cycle_count, never wipe
# dispatch_seq_counter (must never repeat a value within this task) or detected_defects.
jq --arg updated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '.cycle_count = 0 | .last_updated = $updated' \
"$exhausted_guard_dest" > "${loop_guard_file}.tmp" && mv "${loop_guard_file}.tmp" "$loop_guard_file"
else
echo "[orchestrate] WARNING: could not archive exhausted guard to ${exhausted_guard_dest}; proceeding without archiving (cycle_count reset in place)." >&2
jq --arg updated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '.cycle_count = 0 | .last_updated = $updated' \
"$loop_guard_file" > "${loop_guard_file}.tmp" && mv "${loop_guard_file}.tmp" "$loop_guard_file"
fi
else
echo "[orchestrate] ERROR: work-cycle budget exhausted (cycle_count=${peek_cycle_count}/${MAX_CYCLES}). This is a budget limit, not an error condition -- the task's plan may still have incomplete phases." >&2
echo "[orchestrate] To continue this task's work, explicitly authorize a fresh budget: /orchestrate ${task_number} --continue-budget" >&2
exit 1
fi
fi
fi
# --- budget-continuation-override:end ---
if [ -f "$loop_guard_file" ] && jq empty "$loop_guard_file" 2>/dev/null; then
# Resume: read existing guard. No session_id or mtime check — see the ephemerality note above;
# this is precisely why a git-restorable guard would corrupt the cycle budget.
cycle_count=$(jq -r '.cycle_count // 0' "$loop_guard_file")
infra_failures=$(jq -r '.infra_failures // 0' "$loop_guard_file")
# System-defect observation log for this run (contract: Stage MT-1's `detected_defects`
# declaration). `// []` is the forward-compatible read for a guard file written before this
# field existed, matching the `// 0` idiom above.
detected_defects=$(jq -c '.detected_defects // []' "$loop_guard_file")
# dispatch_seq_counter: orchestrator-minted per-dispatch identity (Defect A), verbatim-twin
# field to skill-orchestrate-hard/SKILL.md's Stage 2. `// 0` forward-compatible read, matching
# cycle_count's own idiom — a guard written before this field existed resumes at 0, never
# repeating a value already minted this task since the counter only ever increments (see
# mint_dispatch_seq() below).
dispatch_seq_counter=$(jq -r '.dispatch_seq_counter // 0' "$loop_guard_file")
# Observational-only session_id tracking (NEVER a gate — see Ephemeral note above and
# context/standards/status-markers.md's rationale: SESSION_ID is regenerated per /orchestrate
# invocation, while this guard is explicitly designed to survive across conversational turns.
# The real same-task concurrency guard is task-lock.sh's acquire/heartbeat/release mutex, not
# session_id equality). A mismatch is logged, never branched on.
guard_session_id=$(jq -r '.session_id // ""' "$loop_guard_file")
if [ -n "$guard_session_id" ] && [ "$guard_session_id" != "$session_id" ]; then
echo "[orchestrate] INFO: loop guard was last written by a different session_id ('${guard_session_id}' vs current '${session_id}') — expected on conversational resume, not gated."
fi
echo "[orchestrate] Resuming — cycle $cycle_count of $MAX_CYCLES (infra failures: $infra_failures of $MAX_INFRA_FAILURES)"
else
# Fresh start: create guard atomically via init-marker. A plain
# `>` redirect has no O_EXCL semantics, so two racing writers could both take
# this branch and stomp each other's counters; init-marker's mkdir-gate +
# tmp-mv payload guarantees exactly one winner. On a lost race (exit 1),
# degrade to the same resume-read the `if`-branch above performs.
if jq -n \
--arg session_id "$session_id" \
--argjson max_cycles "$MAX_CYCLES" \
--argjson max_infra_failures "$MAX_INFRA_FAILURES" \
--arg started "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{
"session_id": $session_id,
"cycle_count": 0,
"max_cycles": $max_cycles,
"infra_failures": 0,
"max_infra_failures": $max_infra_failures,
"current_state": "reading",
"detected_defects": [],
"started": $started,
"last_updated": $started,
"dispatch_seq_counter": 0
}' | bash .claude/scripts/task-lock.sh init-marker "$loop_guard_file"; then
cycle_count=0
infra_failures=0
detected_defects='[]'
dispatch_seq_counter=0
echo "[orchestrate] Starting fresh — MAX_CYCLES=$MAX_CYCLES, MAX_INFRA_FAILURES=$MAX_INFRA_FAILURES"
else
# Lost the creation race: another writer won. Resume from their guard, reading BOTH
# counters — not just cycle_count — and the system-defect observation log alongside them.
cycle_count=$(jq -r '.cycle_count // 0' "$loop_guard_file")
infra_failures=$(jq -r '.infra_failures // 0' "$loop_guard_file")
detected_defects=$(jq -c '.detected_defects // []' "$loop_guard_file")
dispatch_seq_counter=$(jq -r '.dispatch_seq_counter // 0' "$loop_guard_file")
echo "[orchestrate] Resuming (lost init race) — cycle $cycle_count of $MAX_CYCLES (infra failures: $infra_failures of $MAX_INFRA_FAILURES)"
fi
fi
# mint_dispatch_seq(): named-shim to the single shared implementation,
# skill_orchestrate_mint_dispatch_seq (scripts/skill-base.sh) — see that function's header for the
# full contract. Kept as a locally-named function (not called directly by name) because Stage 4/5
# call sites below still say `mint_dispatch_seq`, and this file pair is where a one-sided rename
# is a known recurring defect class. Source is defensive/idempotent: this Stage 2 fence has no
# earlier explicit source line of its own to depend on.
source .claude/scripts/skill-base.sh
mint_dispatch_seq() {
skill_orchestrate_mint_dispatch_seq "$loop_guard_file"
}
# Blocker escalation counter (reset each /orchestrate invocation) — from the same shared
# orchestrate-loop-guard-init.sh call above.
blocker_escalation_count=$(echo "$loop_guard_init_json" | jq -r '.blocker_escalation_count')
MAX_BLOCKER_ESCALATIONS=$(echo "$loop_guard_init_json" | jq -r '.max_blocker_escalations')
# Drift detection constants (reset each /orchestrate invocation) — base-mode-only; hard mode has
# no Stage 5a Drift Inspection equivalent (its own H5 divergence-audit mechanism plays that role
# instead), so these stay out of the shared script.
drift_inspection_count=0
MAX_DRIFT_INSPECTIONS=1
DRIFT_COMPLETION_THRESHOLD=0.70
View on GitHub