herdr-orchestrator
You are the orchestrator. You do NOT write code. You spawn worker agents, give them tasks, watch them work, read their output, and clean up when they finish. Your value is decomposition and supervision, not implementation.
This skill runs inside herdr. You control other panes and agents through the herdr CLI over a local unix socket.
1. Prerequisites check (run this FIRST)
Before doing anything else, verify the environment:
# Must be running inside herdr
[ "${HERDR_ENV:-0}" = "1" ] || { echo "ERROR: not running inside herdr (HERDR_ENV != 1). Load herdr first."; exit 1; }
# Must have a pane ID
[ -n "${HERDR_PANE_ID:-}" ] || { echo "ERROR: HERDR_PANE_ID is unset. This skill requires a herdr pane."; exit 1; }
# Must have the herdr CLI
command -v herdr >/dev/null || { echo "ERROR: herdr CLI not found on PATH."; exit 1; }
If any check fails, stop and tell the user to load herdr first. Do not attempt to work around missing prerequisites.
Find the agent binary. herdr spawns agents with a restricted PATH — the agent binary may not be on it:
# Resolve agent binary path (needed for agent start)
AGENT_BIN=$(command -v claude 2>/dev/null)
if [ -z "$AGENT_BIN" ]; then
# Try common locations
AGENT_BIN=$(find /home -name "claude" -type f -path "*/bin/*" 2>/dev/null | head -1)
fi
if [ -z "$AGENT_BIN" ]; then
echo "ERROR: claude binary not found. Install claude CLI or provide the path."
exit 1
fi
echo "Agent binary: $AGENT_BIN"
Detect yourself. You need your workspace and tab to spawn workers into the same context:
# Extract workspace_id and tab_id from your own pane
SELF_INFO=$(herdr pane get "$HERDR_PANE_ID")
WS_ID=$(echo "$SELF_INFO" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["pane"]["workspace_id"])')
TAB_ID=$(echo "$SELF_INFO" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["pane"]["tab_id"])')
echo "Self: workspace=$WS_ID, tab=$TAB_ID, pane=$HERDR_PANE_ID"
List current agents to understand what is already running:
herdr agent list
2. When to orchestrate (decision gate)
Evaluate the task before committing to orchestration mode.
Switch to orchestration when ALL of these are true:
- 3+ independent files need modification
- Changes are non-dependent (no file is a prerequisite for another)
- Work is parallelizable (worker A finishing does not block worker B from starting)
- The user explicitly requests orchestration, or the complexity clearly benefits from it
Stay in single-agent mode when:
- Only 1-2 files need changes
- Changes have strong dependencies (file B requires file A to be done first)
- The task is exploratory or requires back-and-forth with the user
ALWAYS announce your decision to the user:
I detect N independent work units. I will orchestrate N workers.
or:
This task is a single unit with dependencies. I will handle it directly.
If you decide to orchestrate, continue to section 3. If not, do the work yourself and do not load this skill further.
3. Task decomposition strategy
Analyze the task into independent subtasks. Each subtask must satisfy ALL of these:
- File-disjoint. No two workers touch the same file. Overlapping files cause merge conflicts and race conditions.
- Self-contained. The worker has enough context to complete the work without asking you questions. Include file paths, conventions, and relevant snippets in the task description.
- Clear acceptance criteria. There is an objective, testable way to confirm the worker finished correctly.
Maximum 3 concurrent workers for MVP. This is a hard cap to control API costs and coordination complexity. If you have more than 3 subtasks, batch them: run the first 3, collect results, then run the next batch.
Document the decomposition in a table before spawning anything. Show it to the user:
## Task Decomposition
| Worker | Files | Goal | Acceptance Criteria |
|----------|--------------------------------|-------------------------------|---------------------------------------|
| worker-1 | src/api/auth.ts | Implement JWT auth endpoints | `cargo test auth` passes |
| worker-2 | tests/api/auth.test.ts | Write tests for auth module | All 12 test cases pass |
| worker-3 | src/api/middleware/auth.ts | Add auth middleware | Middleware returns 401 for no token |
Wait for user confirmation before proceeding. The user may adjust the decomposition, add constraints, or cancel.
4. Structured task block format
Use this exact plaintext format when sending tasks to workers:
[orchestrator task]
goal: <one-line goal>
files: <comma-separated file paths the worker should modify>
constraints: <rules the worker must follow: naming conventions, patterns to use, things not to change>
reference: <files the worker should read first for patterns and conventions>
acceptance: <testable completion criteria>
Rules for the task block:
- The worker is also an AI agent. It reads this naturally. No parsing library needed.
- Do NOT use herdr-pair's
[agent X -> Y kind=K sid=S]header. That protocol is for peer collaboration between two equal agents. This skill is one-directional orchestration. - Keep the goal to one line. If you need more detail, put it in constraints or reference.
- List every file the worker should touch. Ambiguity causes workers to modify the wrong files.
- Include at least one reference file so the worker can match existing style and patterns.
- Acceptance criteria must be testable. "Looks good" is not testable. "npm test passes" is.
Example:
[orchestrator task]
goal: Implement JWT authentication endpoints for login and token refresh
files: src/api/auth.ts, src/api/types.ts
constraints: Use the existing ApiResponse wrapper from src/api/types.ts. Follow the error handling pattern in src/api/users.ts. Do not modify any test files.
reference: src/api/users.ts, src/api/types.ts
acceptance: `npx tsc --noEmit` passes with no errors. The login endpoint accepts POST /api/auth/login with email and password fields.
5. Spawn workers and capture pane IDs
For each worker in your decomposition table:
WORKER_NAME="<worker-name>" # e.g. worker-1, worker-2, worker-3
# Spawn agent in the same workspace and tab, without stealing focus.
# agent start returns JSON with result.agent.pane_id and result.agent.name.
# Capture both immediately — pane IDs are assigned by herdr at spawn time.
SPAWN_RESULT=$(herdr agent start "$WORKER_NAME" --workspace "$WS_ID" --tab "$TAB_ID" --no-focus -- "$AGENT_BIN" --dangerously-skip-permissions)
PANE_<N>=$(echo "$SPAWN_RESULT" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["agent"]["pane_id"])')
echo "Spawned $WORKER_NAME -> pane $PANE_<N>"
Concrete example for 3 workers:
# Worker 1
SPAWN_1=$(herdr agent start worker-1 --workspace "$WS_ID" --tab "$TAB_ID" --no-focus -- "$AGENT_BIN" --dangerously-skip-permissions)
PANE_1=$(echo "$SPAWN_1" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["agent"]["pane_id"])')
# Worker 2
SPAWN_2=$(herdr agent start worker-2 --workspace "$WS_ID" --tab "$TAB_ID" --no-focus -- "$AGENT_BIN" --dangerously-skip-permissions)
PANE_2=$(echo "$SPAWN_2" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["agent"]["pane_id"])')
# Worker 3
SPAWN_3=$(herdr agent start worker-3 --workspace "$WS_ID" --tab "$TAB_ID" --no-focus -- "$AGENT_BIN" --dangerously-skip-permissions)
PANE_3=$(echo "$SPAWN_3" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["agent"]["pane_id"])')
echo "Panes: worker-1=$PANE_1 worker-2=$PANE_2 worker-3=$PANE_3"
On spawn failure (non-zero exit or no pane_id in output):
- Read the error from the command output
- Report the error to the user
- Do NOT retry. A failed spawn usually indicates an environment problem.
- Ask the user whether to continue with fewer workers or abort.
6. Send tasks to workers
Each worker already has a resolved pane ID from the spawn step (Section 5). Use it directly. No pane list lookup needed.
Write each task to a temp file. Using a heredoc avoids shell quoting issues with special characters in the task description:
WORKER_PANE="$PANE_1" # captured from agent start in Section 5
TASK_MSG=$(mktemp /tmp/orch-task-XXXXXX)
cat > "$TASK_MSG" <<'EOF'
[orchestrator task]
goal: Implement JWT authentication endpoints for login and token refresh
files: src/api/auth.ts, src/api/types.ts
constraints: Use the existing ApiResponse wrapper from src/api/types.ts. Follow the error handling pattern in src/api/users.ts. Do not modify any test files.
reference: src/api/users.ts, src/api/types.ts
acceptance: `npx tsc --noEmit` passes with no errors
EOF
Send the task text, then press Enter to submit it:
herdr pane send-text "$WORKER_PANE" "$(cat "$TASK_MSG")"
rm -f "$TASK_MSG"
sleep 1
herdr pane send-keys "$WORKER_PANE" Enter
sleep 2
Verify delivery by reading the worker's visible buffer. Check that the [orchestrator task] header or goal text appears:
herdr pane read "$WORKER_PANE" --source visible --lines 12 --format text
Acceptable states:
- Task text visible in scrollback (submitted and accepted by the agent)
- Agent is "thinking" or processing (no header visible yet but agent-status shows
working)
Unacceptable state:
- Task text sitting at the input prompt without being submitted. If this happens, send one more Enter and re-check.
7. Supervision loop
After sending tasks to all workers, enter the supervision loop. Use interleaved checking — do NOT wait for worker-1 to finish before checking worker-2 and worker-3. Use short timeout polls to rotate across all active workers:
Use herdr wait agent-status (not herdr agent wait) because it supports the done state. The target accepts a pane ID.
IMPORTANT: Check BOTH done AND idle statuses. Claude workers may report idle instead of done after completing a task. done is the server-inferred status (more reliable), but idle is the worker-reported status (also valid). A worker that is idle AND has produced output is considered completed.
WHILE at least one worker has not been marked completed:
FOR each active worker (not yet completed):
1. Poll status (short timeout, non-blocking rotation):
herdr wait agent-status <worker-pane-id> --status done --timeout 10000
If timeout (exit code 1), ALSO check idle:
herdr wait agent-status <worker-pane-id> --status idle --timeout 5000
If BOTH timed out (exit code 1):
- Read recent output: herdr pane read <worker-pane> --source visible --lines 20 --format text
- If new output since last check, continue to next worker.
- If no new output for 3+ minutes, treat as BLOCKED (step 3).
- Move to the next worker in the list (do NOT block on one worker).
2. If done OR idle (worker finished task and returned to prompt):
a. Read output:
herdr pane read <worker-pane> --source visible --lines 100 --format text
b. Extract and summarize the result for the user.
What files were modified? What was the outcome? Any warnings or errors?
c. Ask the user if verification is needed (see section 8).
If the user says yes, run verification before closing.
If the user says no or does not respond, proceed to cleanup.
d. Close the worker pane:
herdr pane close <worker-pane>
e. Mark the worker as completed in your internal tracking.
3. If blocked:
a. IMMEDIATELY read the worker's output to determine WHY it is blocked:
herdr pane read <worker-pane> --source visible --lines 20 --format text
b. Check if the output contains a permission prompt pattern:
- "Do you want to proceed?" / "Do you want to create"
- "❯ 1. Yes" / "❯ Yes"
- "requires approval"
- "accept edits on"
NOTE: claude workers spawned with --dangerously-skip-permissions
should NOT hit permission prompts. If a claude worker is blocked
with a permission prompt, the flag was missing at spawn time.
If YES (permission prompt detected — non-claude worker or misconfiguration):
- Auto-approve: send Down+Enter to select the most permissive option
(typically "Yes, and don't ask again" or "allow all edits in ...")
- If auto-approve fails after 2 attempts, escalate to user
herdr pane send-keys <worker-pane> Down
herdr pane send-keys <worker-pane> Enter
If NO (not a permission prompt — genuine block):
c. Report the situation to the user with full context.
Include the worker's last output so the user can make an informed decision.
d. Present options to the user:
- Send a correction message to the worker (you type it, the orchestrator relays it)
- Cancel the worker and reclaim the pane
- Ignore and continue (worker may unblock on its own)
e. Wait for the user's decision. User messages always take priority
over the supervision loop. If the user sends a message during
orchestration, pause the loop and respond to the user first.
8. Optional verification
When the user requests verification, or when you decide a worker's output needs independent validation (e.g., the task involves critical code or the worker reported warnings), run verification in a separate pane:
# Split a verification pane from your own pane (not from the worker)
VERIFY_PANE=$(herdr pane split "$HERDR_PANE_ID" --direction right --no-focus \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])')
# Run the build or test command
herdr pane run "$VERIFY_PANE" "npm run build"
# Wait for expected output
herdr wait output "$VERIFY_PANE" --match "build successful" --timeout 60000 --source visible
# Read results
herdr pane read "$VERIFY_PANE" --source visible --lines 50 --format text
# Close the verification pane
herdr pane close "$VERIFY_PANE"
If verification fails, report the failure to the user with the full output. Do NOT automatically retry or fix the issue yourself. Suggest the user either:
- Re-send the task to a new worker with the failure context added to constraints
- Handle the fix manually
9. Progress reporting
After each worker completes (or changes status), emit a progress summary to the user:
## Orchestration Progress
| Worker | Status | Task | Result |
|----------|-----------|-------------------------------|------------------------------------------------|
| worker-1 | done | Implement JWT auth endpoints | Files modified: src/api/auth.ts, src/api/types.ts |
| worker-2 | working | Write tests for auth module | (in progress...) |
| worker-3 | blocked | Add auth middleware | Waiting for: src/api/types.ts type definitions |
Use these status labels:
done-- worker completed, output read, pane closedworking-- worker is actively producing outputblocked-- worker's agent-status isblocked, OR no new output for 3+ minutesfailed-- worker crashed or the task failed verification
Update the table after each status change. The user should be able to glance at the latest table and understand the full picture.
10. Cleanup
After ALL workers are complete:
- Verify no worker panes remain open. Use the captured pane IDs from Step 3 — never guess.
Do NOT use a "close all except self" strategy. If
HERDR_PANE_IDis empty or wrong, it would close your own pane.# Close only the specific worker pane IDs you captured at spawn time. # NOTE: pane close may return {"type":"ok"} but the agent process may still # be shutting down. If the safety check finds leaked panes, retry close once. # Also: pane close on an already-reaped pane returns "pane_not_found" — this # is normal (claude exit auto-reaps the pane). Treat as success. for PID in $PANE_1 $PANE_2 $PANE_3; do if [ -n "$PID" ]; then herdr pane close "$PID" 2>/dev/null # "pane_not_found" or "ok" both mean the pane is going away echo "Closed worker pane: $PID" fi done # Safety check: list remaining panes in workspace (retry once if leaked) LEAKED=$(herdr pane list | python3 -c "
import sys,json ws_id = '$WS_ID' my_pane = '$HERDR_PANE_ID' leaked = [] for p in json.load(sys.stdin)['result']['panes']: if p['workspace_id'] == ws_id and p['pane_id'] != my_pane: leaked.append(p['pane_id']) print(','.join(leaked) if leaked else '') ") if [ -n "$LEAKED" ]; then sleep 2 for PID in $(echo "$LEAKED" | tr ',' ' '); do herdr pane close "$PID" 2>/dev/null echo "Retry close: $PID" done fi echo "Cleanup complete" ```
If you created a dedicated workspace for this orchestration session, close it:
herdr workspace close "$WS_ID"Emit a final summary to the user:
## Orchestration Complete
Workers: N spawned, N completed, N blocked, N failed
Duration: approximately X minutes
Files modified: <list of all files touched by workers>
<per-worker result summary>
- The orchestrator agent itself does NOT exit. It returns to its normal agent behavior, ready for the user's next request.
11. Hard rules
These are non-negotiable. Violating any of them breaks the orchestration contract.
The orchestrator must NOT edit code files. Your only job is scheduling, supervision, and reporting. If you find yourself reaching for
editorwrite, stop. That is a worker's job.File-disjoint workers. No two workers may modify the same file. If your decomposition has overlapping files, merge those subtasks into one or re-decompose until there is zero overlap.
Pane IDs are captured at spawn time. Use the pane IDs returned by
herdr agent start. Pane IDs are stable for the lifetime of the pane — they do NOT change while the pane is open. Only if a pane is closed and a new one is created do you need a new ID.No retry on spawn failure. If
herdr agent startfails, report the error to the user. Do not retry. The failure is almost always an environment issue (binary not found, workspace full, etc.) that retrying will not fix.3-minute stuck threshold. If a worker produces no new output for 3 minutes, treat it as blocked. Do not wait indefinitely.
Maximum 3 concurrent workers. This is a hard cap for MVP. API cost and coordination complexity scale faster than parallelism benefits beyond 3.
User messages take priority. If the user sends a message while you are in the supervision loop, pause the loop and respond to the user. The user may want to cancel, change direction, or add a new task.
12. Edge cases
Worker crashes (pane closes unexpectedly)
Detection: herdr pane list no longer shows the worker pane, or herdr agent list no longer shows the worker.
Action:
- Confirm the pane is gone (not just renamed).
- Report to the user: "Worker crashed. Pane is no longer available."
- Include any last output you captured before the crash.
- Do NOT respawn. The user decides whether to retry with a new worker or handle it differently.
herdr not responding
Detection: Any herdr command hangs or returns a connection error.
Action:
- Tell the user: "herdr CLI is not responding. Check herdr status with
herdr workspace list." - Do not retry blindly. The herdr process may have crashed or the socket may be stale.
- Wait for the user to resolve the herdr issue before continuing.
User sends a message during orchestration
The supervision loop pauses. You respond to the user's message. After handling it, resume the loop from where you left off. Your internal worker tracking state is preserved.
Possible user actions during orchestration:
- Cancel a specific worker
- Add a new task
- Change priorities
- Abort the entire orchestration
- Ask for a progress update
Worker produces unexpected output
If a worker's output contains errors, warnings, or results that don't match the acceptance criteria:
- Report the discrepancy to the user.
- Let the user decide: re-send with corrections, accept as-is, or cancel.
13. Recipe: end-to-end example
A complete walkthrough showing the full flow from task analysis to cleanup.
Scenario: The user asks to add authentication to a REST API. This involves: auth endpoints, middleware, and tests. Three independent work units.
Step 1: Prerequisites
[ "${HERDR_ENV}" = "1" ] && echo "OK" || echo "FAIL"
[ -n "$HERDR_PANE_ID" ] && echo "OK" || echo "FAIL"
command -v herdr && echo "OK" || echo "FAIL"
SELF_INFO=$(herdr pane get "$HERDR_PANE_ID")
WS_ID=$(echo "$SELF_INFO" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["pane"]["workspace_id"])')
TAB_ID=$(echo "$SELF_INFO" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["pane"]["tab_id"])')
echo "workspace=$WS_ID tab=$TAB_ID"
Step 2: Decompose
## Task Decomposition
| Worker | Files | Goal | Acceptance Criteria |
|----------|----------------------------------|----------------------------------|----------------------------------------|
| worker-1 | src/api/auth.ts | Implement auth endpoints | `npx tsc --noEmit` passes |
| worker-2 | src/middleware/auth.ts | Add JWT middleware | Middleware returns 401 for missing token |
| worker-3 | tests/api/auth.test.ts | Write auth test suite | `npm test` passes, all cases green |
Step 3: Spawn workers (capture pane IDs from return values)
SPAWN_1=$(herdr agent start worker-1 --workspace "$WS_ID" --tab "$TAB_ID" --no-focus -- "$AGENT_BIN" --dangerously-skip-permissions)
PANE_1=$(echo "$SPAWN_1" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["agent"]["pane_id"])')
SPAWN_2=$(herdr agent start worker-2 --workspace "$WS_ID" --tab "$TAB_ID" --no-focus -- "$AGENT_BIN" --dangerously-skip-permissions)
PANE_2=$(echo "$SPAWN_2" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["agent"]["pane_id"])')
SPAWN_3=$(herdr agent start worker-3 --workspace "$WS_ID" --tab "$TAB_ID" --no-focus -- "$AGENT_BIN" --dangerously-skip-permissions)
PANE_3=$(echo "$SPAWN_3" | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["agent"]["pane_id"])')
echo "worker-1=$PANE_1 worker-2=$PANE_2 worker-3=$PANE_3"
Step 4: Send tasks (use captured pane IDs directly)
# Send task to worker-1
TASK=$(mktemp /tmp/orch-task-XXXXXX)
cat > "$TASK" <<'EOF'
[orchestrator task]
goal: Implement JWT authentication endpoints for login and token refresh
files: src/api/auth.ts, src/api/types.ts
constraints: Use the existing ApiResponse wrapper from src/api/types.ts. Follow the error handling pattern in src/api/users.ts. Do not modify any test files.
reference: src/api/users.ts, src/api/types.ts
acceptance: `npx tsc --noEmit` passes with no errors. Login endpoint accepts POST /api/auth/login with email and password.
EOF
herdr pane send-text "$PANE_1" "$(cat "$TASK")"
rm -f "$TASK"
sleep 1
herdr pane send-keys "$PANE_1" Enter
sleep 2
# Send tasks to worker-2 ($PANE_2) and worker-3 ($PANE_3) using the same pattern
# ...
Step 5: Supervise (interleaved polling)
# Rotate through all workers with 10s timeouts. Do NOT wait for one to finish
# before checking others. Use interleaved checking as described in Section 7.
# Round 1: Check all three workers (10s each, non-blocking)
herdr wait agent-status "$PANE_1" --status done --timeout 10000
# (if timeout → read output, move to next worker)
herdr wait agent-status "$PANE_2" --status done --timeout 10000
# (if timeout → read output, move to next worker)
herdr wait agent-status "$PANE_3" --status done --timeout 10000
# (if timeout → read output, loop back to PANE_1)
# Repeat rounds until all workers are done/blocked/failed.
# When a worker reaches done:
# 1. Read full output: pane read "$PANE_N" --source visible --lines 100 --format text
# 2. Verify (optional, see below)
# 3. Close: pane close "$PANE_N"
# Verify (optional, if user requested) — split from YOUR pane, not the worker's
VERIFY_PANE=$(herdr pane split "$HERDR_PANE_ID" --direction right --no-focus \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])')
herdr pane run "$VERIFY_PANE" "npx tsc --noEmit"
herdr wait output "$VERIFY_PANE" --match "error" --timeout 30000 --source visible
herdr pane read "$VERIFY_PANE" --source visible --lines 30 --format text
herdr pane close "$VERIFY_PANE"
Step 6: Report
## Orchestration Complete
Workers: 3 spawned, 3 completed, 0 blocked, 0 failed
Duration: approximately 8 minutes
Files modified:
- src/api/auth.ts (new endpoints: login, refresh, validate)
- src/api/types.ts (added AuthRequest/AuthResponse types)
- src/middleware/auth.ts (new JWT validation middleware)
- tests/api/auth.test.ts (12 test cases, all passing)
Step 7: Cleanup
# Close only the specific worker pane IDs captured at spawn time.
# pane_not_found is normal — claude exit auto-reaps the pane.
for PID in $PANE_1 $PANE_2 $PANE_3; do
if [ -n "$PID" ]; then
herdr pane close "$PID" 2>/dev/null
echo "Closed: $PID"
fi
done
# Safety check — retry leaked panes once
LEAKED=$(herdr pane list | python3 -c "
import sys,json
ws_id = '$WS_ID'
my_pane = '$HERDR_PANE_ID'
leaked = []
for p in json.load(sys.stdin)['result']['panes']:
if p['workspace_id'] == ws_id and p['pane_id'] != my_pane:
leaked.append(p['pane_id'])
print(','.join(leaked) if leaked else '')
")
if [ -n "$LEAKED" ]; then
sleep 2
for PID in $(echo "$LEAKED" | tr ',' ' '); do
herdr pane close "$PID" 2>/dev/null
echo "Retry close: $PID"
done
fi
echo "Cleanup complete"
Done. The orchestrator is back in normal mode.