Relay Dispatch
Create a worktree and dispatch a task to an executor.
Usage
# Foreground (blocking — simple tasks, default executor: codex)
${CLAUDE_SKILL_DIR}/scripts/dispatch.js . -b feature-auth -p "..." --rubric-file rubric.yaml
# Same-run resume after a changes-requested review
${CLAUDE_SKILL_DIR}/scripts/dispatch.js . --run-id issue-42-20260403120000000 --prompt-file review-round-2-redispatch.md
# With explicit executor
${CLAUDE_SKILL_DIR}/scripts/dispatch.js . -e codex -b feature-auth -p "..." --rubric-file rubric.yaml
# Claude Code as executor (no Codex required)
${CLAUDE_SKILL_DIR}/scripts/dispatch.js . -e claude -b feature-auth -p "..." --rubric-file rubric.yamlFor background and parallel dispatch, see "Background & Parallel" section below.
Options
All CLI flags are registered with an explicit parsed or verbatim read mode. See references/cli-schema.md before adding or changing flags.
| Flag | Description |
|---|---|
--branch, -b | Branch name (required) |
--run-id | Resume an existing retained relay run |
--manifest | Resume an existing retained relay run by manifest path |
--prompt, -p | Task prompt (include Context + Done Criteria + self-review) |
--prompt-file | Read prompt from file (for large prompts) |
--executor, -e | Executor: codex (default), claude |
--model, -m | Model override |
--model-hints | Persist per-phase model hints as phase=model[,phase=model...] |
--sandbox | workspace-write (default) or read-only |
--copy <files> | Additional files to copy |
--timeout | Timeout in seconds (default: 1800) |
--rubric-file | REQUIRED: persist the rubric generated by relay-plan into the run dir |
--rubric-grandfathered | Retired alias; dispatch rejects it and operators must remove anchor.rubric_grandfathered manually |
--request-id | Link the run back to a relay-intake request artifact |
--leaf-id | Link the run back to a relay-intake relay-ready leaf |
--done-criteria-file | Persist a frozen Done Criteria anchor path into the run manifest |
--register | Register session in executor's app (keeps worktree) |
--no-cleanup | Compatibility alias; worktree is retained by default |
--dry-run | Show plan without executing |
--json | Structured JSON output (for background dispatch) |
Creates worktree → writes a relay run manifest → runs executor → collects result. Exits with non-zero code on failure.
Each dispatch writes a manifest to ~/.relay/runs/<repo-slug>/<run-id>.md and appends lifecycle evidence to ~/.relay/runs/<repo-slug>/<run-id>/events.jsonl. run_id is the canonical identity for re-dispatch, review, merge, close, and reporting.
model_hints is an optional top-level manifest field. Dispatch consumes model_hints.dispatch only when --model/-m is not passed:
- precedence:
--model->manifest.model_hints.dispatch-> executor default - same-run resume with
--model-hintsreplaces the stored object and recordsmodel_hints_updated --dry-runresolveseffective_dispatch_modelfor plan output but emits no events
When dispatch resumes from relay-intake, it can also store source.request_id, source.leaf_id, and anchor.done_criteria_path so review stays pinned to the frozen snapshot.
Timeout guidance
| Task type | Timeout | Rationale |
|---|---|---|
| Simple implementation | 1800 (default) | No self-review needed |
| With self-review loop | 3600 | Executor iterates 2-3 times |
| Complex / multi-file | 5400 | Deep implementation + thorough self-review |
Verify Success
After dispatch completes, confirm before proceeding to review:
# JSON output: status, runId, manifestPath, runState, cleanupPolicy
# "completed" + "review_pending" → proceed to relay-review
# "completed-with-warning" + "review_pending" → inspect uncommitted work, then review
# "failed" + "escalated" → inspect error, fix or re-dispatch
gh pr list --head <branch> --json number,url,titleSuccessful dispatches retain the worktree by default. Use the returned runId, manifest, and worktree to continue review. Resume only from changes_requested; dispatch reuses the same run and worktree.
On re-dispatch, previous Score Log + reviewer feedback are auto-prepended to the prompt. Record attempt data via captureAttempt() before transitioning to changes_requested. Storage: ~/.relay/runs/<slug>/<run-id>/previous-attempts.json.
Handling Failures
| Failure | Action |
|---|---|
| Timeout (with commits) | completed-with-warning — check worktree for uncommitted changes, proceed to review |
| Timeout (no commits) | Increase --timeout or split task into smaller pieces |
| Executor error / no commits | Read result file; revise prompt and re-dispatch |
| Branch publication / PR creation failed | Inspect the dispatch error and outer-shell GitHub auth. relay-dispatch handles publication from the orchestrator shell. |
| Branch conflicts | Resolve in worktree or create fresh worktree from updated main |
| Network/transient error | Wait 30s, retry once. If it fails again, escalate to user |
Background & Parallel
Background dispatch
Run dispatch asynchronously so the orchestrator can continue other work (planning, reviewing, user interaction) while the executor runs.
Platform examples — async dispatch: Claude Code:Bash(run_in_background=true)| Codex: shell&or platform async | Other: any non-blocking execution
${CLAUDE_SKILL_DIR}/scripts/dispatch.js . -b task-42 --prompt-file tasks/42.md --rubric-file rubric.yaml --json --timeout 3600
# Run this command in the background using your platform's async mechanism
# When executor finishes → proceed to relay-reviewParallel dispatch (independent tasks)
Launch multiple independent dispatches concurrently:
# Each dispatch runs independently in the background
${CLAUDE_SKILL_DIR}/scripts/dispatch.js . -b task-42 --prompt-file tasks/42.md --rubric-file rubric-42.yaml --json &
${CLAUDE_SKILL_DIR}/scripts/dispatch.js . -b task-43 --prompt-file tasks/43.md --rubric-file rubric-43.yaml --json &
# Each completes independently → review each PR via relay-reviewcreate-worktree.js — Standalone worktree creation
Create a worktree without dispatching, or register an existing worktree in Codex App:
# Create worktree in ~/.relay/worktrees/
${CLAUDE_SKILL_DIR}/scripts/create-worktree.js <repo> -b <branch>
# Register an existing worktree in Codex App (optional)
${CLAUDE_SKILL_DIR}/scripts/create-worktree.js <repo> --worktree-path <path> -b <branch> -t "Title" --registerWorktree Cleanup
Successful dispatches keep their worktree by default. Cleanup moves later in the lifecycle, typically after review or merge.
--no-cleanup remains accepted as a compatibility alias. --register still matters because it also opens the retained worktree in the executor app.
To prune stale retained worktrees safely from this repo:
${CLAUDE_SKILL_DIR}/scripts/cleanup-worktrees.js --repo . # clean terminal runs > 24h old
${CLAUDE_SKILL_DIR}/scripts/cleanup-worktrees.js --repo . --all # ignore age threshold
${CLAUDE_SKILL_DIR}/scripts/cleanup-worktrees.js --repo . --dry-run # show what would be removed
${CLAUDE_SKILL_DIR}/scripts/close-run.js --repo . --run-id <run-id> --reason "stale_non_terminal_run"
${CLAUDE_SKILL_DIR}/scripts/reliability-report.js --repo . --jsonExecutor completed but did not commit
recover-commit.js handles the canonical "executor finished implementation but timed out before committing" path. Replaces the ad-hoc git add -A && git commit && git push -u && gh pr create shell sequence with a single command that preflights, commits via template, pushes (no force), creates the PR (idempotent on re-run), stamps git.pr_number via the shared lock helper, and emits a recover_commit event. Manifest STATE stays review_pending — the next step is the normal review.
# Standard recovery — dispatch returned commits="" + uncommitted!=""
${CLAUDE_SKILL_DIR}/scripts/recover-commit.js --run-id <id> \
--reason "executor timeout at 1800s on 18-file refactor"
# Preview without touching anything
${CLAUDE_SKILL_DIR}/scripts/recover-commit.js --run-id <id> \
--reason "..." --dry-run
# Override PR title / body (defaults derive from branch + run-id)
${CLAUDE_SKILL_DIR}/scripts/recover-commit.js --run-id <id> \
--reason "..." --pr-title "..." --pr-body-file /tmp/pr-body.mdIf a PR already exists for the branch, the command no-ops the create step and stamps pr_number from the existing PR — safe to re-run after a partial failure. Use --dry-run first when uncertain.
Operator state recovery
recover-state.js advances a relay run's state after an external event (fix commit pushed directly, dispatch stalled, no-op re-dispatch escalated the manifest). Replaces hand-edited manual_state_override entries with structured state_recovery events and validated transitions.
# Fix pushed directly to the PR branch → return to review without re-dispatch
${CLAUDE_SKILL_DIR}/scripts/recover-state.js --repo . --run-id <id> \
--to review_pending --reason "external commit pushed; see <sha>"
# No-op re-dispatch escalated the run → bring it back for a fresh review
${CLAUDE_SKILL_DIR}/scripts/recover-state.js --repo . --run-id <id> \
--to review_pending --force --reason "no-op-dispatch-recovery"
# Hung dispatch → unstick manifest so dispatch --run-id can resume
${CLAUDE_SKILL_DIR}/scripts/recover-state.js --repo . --run-id <id> \
--to changes_requested --force --reason "dispatch hung; operator-killed"Whitelisted transitions (unlisted pairs are rejected — use the normal dispatch/review/merge flow):
| From | To | Force | Precondition |
|---|---|---|---|
changes_requested | review_pending | no | fresh commit on branch (HEAD ≠ review.last_reviewed_sha) |
escalated | review_pending | yes | — |
escalated | changes_requested | no | — |
dispatched | changes_requested | yes | — |
The script refuses transitions ALLOWED_TRANSITIONS already supports — always prefer the normal flow when it applies. Terminal states (merged, closed) are not recoverable.
Caveats
- Timeout: Use
--timeout 3600+ when self-review is included - App restart (Codex-specific): Codex App needs restart to show new worktree threads
- Exit codes: dispatch.js exits non-zero on failure — check before proceeding to review
- Parallel merges: If parallel PRs touch the same files, merge one at a time and rebase the other