Gabriel Koerich › Orch

Senior Engineer · AI Workflows · Solana / DeFi

Configuration

All runtime configuration lives in ~/.orch/config.yml (ORCH_HOME), unless overridden via the ORCH_HOME environment variable.

Config Reference

SectionKeyDescriptionDefault
top-levelproject_dirOverride project directory (auto-detected from CWD)""
enginetick_intervalMain engine tick interval in seconds10
enginesync_intervalSync tick interval in seconds (GitHub ingest, cleanup)45
enginemax_parallelMax tasks dispatched concurrently4
enginestuck_timeoutSeconds an in_progress task with a live session may run. orch raises it to the run's own timeout plus 300 seconds1800
engineno_session_stuck_timeoutTimeout (seconds) for in_progress tasks with no tmux session600
enginein_review_no_session_stuck_timeoutSeconds an in_review task may have no review session before it goes back to needs_review. orch raises it to the review timeout plus 120 seconds when it is lower1920
enginewebhook_health_check_intervalHealth check frequency for the local webhook server60
enginesilence_grace_periodSeconds of agent silence before warning300
enginesilence_cooldownSeconds a model detected as silent stays in cooldown3600
enginegraceful_shutdown_timeoutSeconds to wait for in-flight tasks on SIGTERM600
engineupgrade_check_intervalHow often to check for new orch versions and notify channels (seconds). Upgrading is operator-only — the service never runs brew itself3600
enginestartup_failure_escalation_secsSeconds of continuous startup failure before escalating to ERROR log + push notification (0 disables)3600
workflowauto_closeAuto-close GitHub issues when tasks are donetrue
workflowenable_review_agentRun a review agent after task completiontrue
workflowmax_attemptsMax attempts before marking task as blocked5
workflowmax_review_cyclesMax PR review cycles before escalating to human2
workflowmax_reroute_attemptsMax times a task may be re-routed automatically. Falls back to max_attempts when unset3
workflowtimeout_secondsTask execution timeout in seconds. Values under 1800 are raised to 18001800
workflowreview_timeout_secondsReview agent timeout, separate from the task timeout, never under 300900
workflowtimeout_by_complexitymedium and complex overrides (e.g. complex: 7200), never under timeout_seconds{}
workflowallowed_toolsTool patterns the agent may use (allowlist)[]
workflowdisallowed_toolsTool patterns blocked via --disallowedTools["Bash(rm *)","Bash(rm -*)"]
workflowpermissions.modeAgent permission mode (autonomous / supervised)autonomous
workflowpermissions.sandboxSandbox level for Codex (workspace-write / full-access)workspace-write
workflowsandboxRun task and review agents in the OS sandbox (os), or not (false). A project's .orch.yml can set false to opt out. See OS Sandboxfalse
workflowworktree_janitor_ttl_hoursJanitor TTL before pruning orphaned worktrees24
workflowworktree_janitor_dry_runLog what the janitor would prune without removing anythingfalse
workflowauto_create_followup_on_changesAuto-create follow-up task when review requests changestrue
workflowci_poll_max_wait_secsMax seconds the merge path waits for CI600
workflowci_poll_interval_secsBase CI poll interval in seconds15
workflowci_check_cooldown_secsMin seconds between CI checks of one task60
workflowmergeability_poll_secsSeconds to wait for GitHub to compute mergeability10
routermodellm, round_robin, or local (Ollama)round_robin
routeragentDefault router executorclaude
routermodelRouter model namehaiku
routertimeout_secondsRouter call timeout in seconds. Values over 45 are lowered to 4545
routermax_route_attemptsLLM failures before falling back to round-robin3
routermax_tasks_per_tickMax routing decisions per engine tick (concurrency cap)1
routerfallback_executorFallback executor when router failscodex
routerallowed_toolsDefault tool allowlist used in routing prompts[yq, jq, bash, ...]
routerweighted_round_robinUse degraded-weight-aware round robinfalse
routerpoolAgents and models the router may pick from(see config.example.yml)
dispatchhealth_check_window_hoursLookback window for counting rate-limit rows toward degraded6
dispatchdegraded_rate_limit_thresholdRate-limit rows in that window that mark an agent degraded3
dispatchmin_healthy_agentsBelow this many healthy agents, dispatch runs one task at a time2
dispatchsequential_delay_msDelay between dispatches in that sequential mode1000
webhookenabledStart the local webhook receiver (HTTP). It needs a readable webhook.secret, without one the receiver stays offfalse
webhookportWebhook listener port8080
webhooksecretHMAC secret for verifying GitHub webhook payloads""
relayurlorch-relay base URL. orch connects out to <url>/ws and wakes the sync on each GitHub App event. Unset means off""
relaytokenBearer token for the relay, a secret value (see Secret values)""
reviewmodecomment or app, see Review mode. A project's .orch.yml overrides the global valuecomment
gh.appid / private_keyGitHub App id and its key, private_key is a secret value, see GitHub App""
ghrepoDefault repo (owner/repo)"owner/repo"
ghproject_idGitHub Project v2 ID""
ghproject_status_field_idStatus field ID in Project v2""
ghproject_status_mapMapping for backlog/in_progress/review/done option IDs{}
gh.backoffbase_secondsInitial backoff duration in seconds30
gh.backoffmax_secondsMax backoff duration in seconds900
ghauth.tokenExplicit Personal Access Token""
channels.discordbot_tokenDiscord bot token""
channels.telegrambot_token / chat_idTelegram bot credentials""
channels.slackbot_token / channel_idSlack bot credentials""
notificationslevelWhich status changes reach channels: all (done, in_review, approved, blocked, failed), verbose (every change), errors_only (blocked, failed) or noneall
agents(list of strings)Agent CLIs to discover in $PATH (e.g. [claude, codex, opencode, grok])[]
model_mapsimple/medium/complex/reviewPer-complexity model name per agent{}
gitname / emailCommit identity for task and review agents<agent>[bot]
jobsbash_timeout_secondsTimeout for a bash job300

config.example.yml in the orch repo is a short starting config. This table lists every key. Run orch config <key> to read the live value for any dotted key (e.g. orch config engine.tick_interval).

Per-Project Config

Drop a .orch.yml (legacy .orchestrator.yml is still supported) in your project root to override global config (project-level keys take precedence over global).

# ~/projects/my-app/.orch.yml
gh:
  repo: "myorg/my-app"
  project_id: "PVT_..."
workflow:
  enable_review_agent: true
router:
  fallback_executor: "claude"
  • Project config is deep-merged with global config (project wins)
  • The engine reloads the file when it changes, without a restart (see Hot reload)
  • Use orch project board sync to (re-)discover project / status / estimate field IDs and persist them into the project's .orch.yml

Hot reload

The engine watches ~/.orch/config.yml and every project's .orch.yml. A change reloads the values in place, with no restart and no interruption to a running agent. Each changed key is logged with its old and new value, so orch log shows what an edit did.

engine.max_parallel resizes the dispatch pool live. Raising it hands out the extra slots at once. Lowering it never cancels a running task: orch stops handing out slots and takes the permits back as tasks finish, so the limit in force settles at the new value. The dispatch pool resized log line reports in_force and target while a shrink is still settling.

A file that does not parse is ignored: the last good values stay in force and one error is logged per breakage. Fix the file and the next save reloads normally.

A project added to projects: is picked up on the next save — its backend connects, its subscribers start and its issues are ingested on the following sync tick. A failed GitHub health check does not hold the project back: it starts anyway and the next sync tick retries, so a project added while GitHub is rate limited or unreachable still runs.

These settings still need orch service restart. Editing one logs config changed, takes effect after restart with the key, so the reload never looks like it applied. The value is not logged, because channels.* holds bot tokens.

SettingWhy
channels.*Telegram and Discord gateways hold long-lived websocket connections
webhook.*The HTTP listener binds its port at startup
relay.*The relay client reads its token and opens its websocket at startup
Removing a project from projects:Its subscriber tasks and already-dispatched agents outlive the config read

Skills

Install skills on the agent. Claude Code, Codex, and the other agents keep their own skill directories. Name the skill in the task body when the task should use it.

A skills: list or router.default_skills left in a config file has no effect.

Secret values

These keys hold secrets: channels.telegram.bot_token, channels.discord.bot_token, channels.slack.bot_token, webhook.secret, relay.token and gh.app.private_key. Each takes one of three forms, so the config file need not hold the secret:

Valueorch reads
passbox:<name>the first line of that passbox entry, once per daemon start (one Touch ID prompt)
${NAME}the environment variable NAME. orch loads ~/.private at startup, so a variable exported there reaches the service
anything elsethe value as written

A reference is the whole value: ${NAME} alone, not inside a longer string. An unset variable or a failed passbox read logs a warning naming the key, and that feature stays off.

channels:
  telegram:
    bot_token: passbox:orch/telegram-bot-token
relay:
  token: ${ORCH_RELAY_TOKEN}

GitHub Authentication

Orch resolves GitHub tokens in this order — the first match wins:

  1. GH_TOKEN environment variable
  2. GITHUB_TOKEN environment variable
  3. gh.auth.token config value
  4. gh auth token CLI

The simplest setup is just gh auth login. No extra config needed.

Git

orch's own git calls (fetches, rebases, pushes, ls-remote) all start from one helper, git_ops::git(). It reaches github.com over HTTPS with the same token, whether the remote is SSH or HTTPS, and sets commit.gpgsign=false and tag.gpgsign=false, so orch never asks for your SSH key or signing key. The settings are environment variables of that one process: no remote and no .git/config changes, and your own commits keep your signing.

Agents commit as git.name / git.email (default <agent>[bot]), without signing. Their shell pushes over HTTPS with gh's credentials (pushInsteadOf), and agents may not change remotes or url.* rules, since a worktree shares .git/config with your checkout.

Environment Variables

export GH_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"

Explicit token in config

# ~/.orch/config.yml
gh:
  auth:
    token: "ghp_xxxxxxxxxxxxxxxxxxxx"

GitHub App

Creating the App, its permissions, installing it and the relay for events are in GitHub App. This section is the config reference.

The App posts reviews only: the APPROVE or REQUEST_CHANGES review with its inline comments, and the orch/review check run. Every other call, pushes, pull requests, comments, labels and reads, uses the owner's gh token, as agents do. GitHub refuses a review from a pull request's author, so the App must never open the pull requests it reviews.

orch looks up the installation per repository (GET /repos/{repo}/installation), since your account and each organization are separate installations. On a repository the App is not installed on, reviews fall back to the ## Automated Review comment. Without gh.app.id, orch behaves as before.

# ~/.orch/config.yml
gh:
  app:
    id: "123456"
    private_key: "passbox:orch/github-app-key"   # preferred, or
    private_key_env: "ORCH_APP_KEY"               # a variable that holds the PEM, or
    private_key_path: "/path/to/app.pem"          # an absolute path

Agents run as your user and can read any file you can, so keep the key in passbox. orch reads a passbox:<name> value with passbox exec --stdin <name> -- /bin/cat and keeps the first line of the output, so store the PEM on one line with \n for each newline, for example awk '{printf "%s\\n", $0}' app.pem | passbox add orch/github-app-key. orch reads the key once after it starts, in the background, and keeps it in memory for the life of the process. A passbox read waits on Touch ID, so until it succeeds orch posts comment reviews, logs one warning and retries every 5 minutes with at most one prompt pending. Debug builds and tests never load the App.

orch signs an RS256 JWT, exchanges it for an installation token per installation, and refreshes each token 10 minutes before it expires. If the App review cannot be submitted, orch posts the decision as the ## Automated Review comment instead.

Review mode

review.mode picks how a project's pull requests are reviewed. The project's .orch.yml wins, then the global config, then the default comment.

review:
  mode: app   # or comment
ModeDecisionGate
commenta ## Automated Review commentthe orch/review commit status
appa real APPROVE or REQUEST_CHANGES review from the App, with each issue on a changed line as an inline commentthe orch/review check run from the App, in_progress while the review runs, then success or failure

In app mode the App review is the decision and no comment is posted. The ## Automated Review comment goes out only when GitHub refuses the App review, and the gate reads it when the App has no review on the head. A review and the check run belong to the head sha the agent reviewed, so a new push makes the PR pending again, except a base update orch makes in approved, which carries the approval (see Review Agent). Issues on lines outside the diff stay in the review body, since GitHub refuses an inline comment there. A project in app mode whose repository does not have the App installed, or a daemon whose App key is not read yet, reviews in comment mode.

Cluster

The cluster section runs orch on several machines. Without it orch runs on one machine. Keys, roles and setup are in Cluster.