Skip to Content
Getting StartedConfiguration

Configuration

Pilot uses a YAML configuration file at ~/.pilot/config.yaml. All settings support environment variable expansion with ${VAR_NAME} syntax.

# Interactive setup wizard — creates config.yaml pilot setup # Validate config and dependencies pilot doctor

Authentication

Pilot delegates AI execution to Claude Code, which manages its own authentication. Pilot itself does not require an Anthropic API key to function.

ComponentAuth MethodRequired
Claude Code (execution engine)Claude subscription login or ANTHROPIC_API_KEYYes — one or the other
GitHubGITHUB_TOKEN env var or adapters.github.token in configYes, if using GitHub
GitLabGITLAB_TOKEN env var or adapters.gitlab.token in configYes, if using GitLab
TelegramTELEGRAM_BOT_TOKEN env var or configNo
LLM classifier (smart intent routing)ANTHROPIC_API_KEY env varNo — falls back to keyword matching
Epic decomposition (Haiku subtask parser)ANTHROPIC_API_KEY env varNo — falls back to regex parser
DiscordDISCORD_BOT_TOKEN env var or adapters.discord.bot_token in configYes, if using Discord
PlanePLANE_API_KEY env var or adapters.plane.api_key in configYes, if using Plane

Most users don’t need ANTHROPIC_API_KEY at all. If you’re logged into Claude Code with a Claude subscription, Pilot works out of the box. The API key is only needed for optional internal features (LLM classifier, structured subtask parsing) that have simpler fallbacks.

Environment Variables

VariableDescriptionRequired
GITHUB_TOKENGitHub personal access token (repo + workflow scopes)If using GitHub
GITLAB_TOKENGitLab personal or project access tokenIf using GitLab
TELEGRAM_BOT_TOKENTelegram bot token for chat interfaceNo
ANTHROPIC_API_KEYEnables LLM intent classifier and structured subtask parsingNo
OPENAI_API_KEYEnables Whisper voice transcription in TelegramNo
SLACK_BOT_TOKENSlack bot token for notificationsNo
DISCORD_BOT_TOKENDiscord bot token for chat interfaceNo
PLANE_API_KEYPlane.so API key for work item managementNo

Use ${VAR_NAME} in any string field to reference environment variables. Pilot expands them at load time. Paths starting with ~ are expanded to your home directory.


GitHub

Connects Pilot to your GitHub repository for issue polling and PR management.

adapters: github: enabled: true token: ${GITHUB_TOKEN} repo: "owner/repo" # owner/repo format project_path: "/path/to/local/repo" # must match repo webhook_secret: "" # for HMAC verification (webhooks mode) pilot_label: "pilot" # label that triggers Pilot polling: enabled: true # poll for issues (vs webhooks) interval: 30s label: "pilot" stale_label_cleanup: enabled: true # auto-remove stale pilot-in-progress labels interval: 30m threshold: 1h # label age before cleanup
FieldTypeDefaultDescription
enabledboolfalseEnable GitHub adapter
tokenstringPAT or GitHub App token
repostringRepository in owner/repo format
project_pathstringLocal filesystem path to the repo
webhook_secretstringHMAC secret for webhook verification
pilot_labelstring"pilot"Label that marks issues for Pilot
polling.enabledboolfalseEnable issue polling (alternative to webhooks)
polling.intervalduration30sHow often to poll for new issues
polling.labelstring"pilot"Label to filter when polling
stale_label_cleanup.enabledbooltrueAuto-remove stale pilot-in-progress labels
stale_label_cleanup.intervalduration30mCleanup check interval
stale_label_cleanup.thresholdduration1hHow old a label must be to be considered stale

Project Board

Sync task status to a GitHub Projects V2 board automatically.

adapters: github: project_board: enabled: true project_number: 3 # project number from the URL status_field: "Status" # single-select field name statuses: in_progress: "In Dev" review: "Ready for Review" done: "Done" failed: "Blocked"
FieldTypeDefaultDescription
project_board.enabledboolfalseEnable GitHub Projects V2 board sync
project_board.project_numberintProject number (from the project URL)
project_board.status_fieldstring"Status"Name of the single-select status field
project_board.statuses.in_progressstringColumn name for tasks being worked on
project_board.statuses.reviewstringColumn name for tasks in review
project_board.statuses.donestringColumn name for completed tasks
project_board.statuses.failedstringColumn name for failed/blocked tasks

Telegram

Chat interface for sending tasks, receiving notifications, and approving actions.

adapters: telegram: enabled: true bot_token: ${TELEGRAM_BOT_TOKEN} chat_id: "123456789" allowed_ids: - 123456789 # your Telegram user/chat ID project_path: "/path/to/default/repo" polling: true # enable inbound message polling plain_text_mode: true # plain text instead of Markdown transcription: backend: "auto" # "whisper-api" or "auto" openai_api_key: ${OPENAI_API_KEY} rate_limit: enabled: true messages_per_minute: 20 tasks_per_hour: 10 burst_size: 5 llm_classifier: enabled: false api_key: ${ANTHROPIC_API_KEY} # falls back to env var timeout_seconds: 2 history_size: 10 history_ttl: 30m
FieldTypeDefaultDescription
enabledboolfalseEnable Telegram adapter
bot_tokenstringTelegram bot token from @BotFather
chat_idstringDefault chat ID for outbound notifications
allowed_ids[]int64User/chat IDs authorized to send tasks
project_pathstringDefault project path for tasks sent via Telegram
pollingboolfalseEnable inbound message polling
plain_text_modebooltrueUse plain text instead of Markdown formatting
transcription.backendstring"auto"Voice transcription backend: whisper-api or auto
transcription.openai_api_keystringOpenAI API key for Whisper transcription
rate_limit.enabledbooltrueEnable per-user rate limiting
rate_limit.messages_per_minuteint20Max messages per minute per user
rate_limit.tasks_per_hourint10Max task executions per hour per user
rate_limit.burst_sizeint5Burst allowance above rate limit
llm_classifier.enabledboolfalseEnable LLM-based intent classification
llm_classifier.api_keystringAnthropic API key (falls back to ANTHROPIC_API_KEY)
llm_classifier.timeout_secondsint2Classification timeout in seconds
llm_classifier.history_sizeint10Messages to keep per chat for context
llm_classifier.history_ttlduration30mTTL for conversation history

Always set allowed_ids to restrict who can trigger Pilot via Telegram. Without it, anyone who discovers your bot can submit tasks.


Executor

Controls how Pilot runs execution backends (Claude Code, Qwen Code, or OpenCode) to execute tasks.

executor: type: "claude-code" # "claude-code", "qwen-code", or "opencode" auto_create_pr: true direct_commit: false # commit directly to branch without PR detect_ephemeral: true # detect and skip ephemeral changes skip_self_review: false # skip self-review step use_worktree: false # execute tasks in isolated git worktrees claude_code: command: "claude" # path to claude CLI extra_args: [] # additional CLI arguments planning_timeout: 2m # max time for epic planning before fallback to direct mode model_routing: enabled: false trivial: "claude-haiku" simple: "claude-sonnet-4-6" medium: "claude-sonnet-4-6" complex: "claude-opus-4-6" effort_routing: enabled: false trivial: "low" simple: "medium" medium: "high" complex: "max" timeout: default: "30m" trivial: "5m" simple: "10m" medium: "30m" complex: "60m" decompose: enabled: false min_complexity: "complex" # only decompose complex tasks max_subtasks: 5 # 2-10 range min_description_words: 50 # skip short descriptions
FieldTypeDefaultDescription
typestring"claude-code"Backend type: claude-code, qwen-code, or opencode
auto_create_prbooltrueAutomatically create PRs after execution
direct_commitboolfalseCommit directly without PR
detect_ephemeralbooltrueDetect and skip ephemeral file changes
skip_self_reviewboolfalseSkip the self-review step
use_worktreeboolfalseExecute tasks in isolated git worktrees (enables execution with uncommitted changes)
claude_code.commandstring"claude"Path to the Claude CLI binary
claude_code.extra_args[]string[]Additional CLI arguments
claude_code.planning_timeoutduration2mMaximum time for epic planning before fallback to direct execution. Affects Slack /plan and Telegram planning commands.
qwen_code.commandstring"qwen"Path to the Qwen Code CLI binary
qwen_code.use_session_resumeboolfalseReuse sessions for self-review
model_routing.enabledboolfalseRoute tasks to different models by complexity
model_routing.trivialstring"claude-haiku"Model for trivial tasks
model_routing.simplestring"claude-sonnet-4-6"Model for simple tasks
model_routing.mediumstring"claude-sonnet-4-6"Model for medium tasks
model_routing.complexstring"claude-opus-4-6"Model for complex tasks
effort_routing.enabledboolfalseRoute thinking effort by complexity
effort_routing.trivialstring"low"Effort for trivial tasks
effort_routing.simplestring"medium"Effort for simple tasks
effort_routing.mediumstring"high"Effort for medium tasks
effort_routing.complexstring"max"Effort for complex tasks
timeout.defaultduration30mDefault execution timeout
timeout.trivialduration5mTimeout for trivial tasks
timeout.simpleduration10mTimeout for simple tasks
timeout.mediumduration30mTimeout for medium tasks
timeout.complexduration60mTimeout for complex tasks
decompose.enabledboolfalseAuto-decompose complex tasks into subtasks (learn more)
decompose.min_complexitystring"complex"Minimum complexity to trigger decomposition
decompose.max_subtasksint5Maximum subtasks created (2-10)
decompose.min_description_wordsint50Minimum description words to decompose
executor: type: "claude-code" claude_code: command: "claude" extra_args: ["--verbose"]
executor: type: "opencode" opencode: server_url: "http://127.0.0.1:4096" model: "anthropic/claude-sonnet-4-6" provider: "anthropic" auto_start_server: false server_command: "opencode serve"

OpenCode backend fields

FieldTypeDefaultDescription
opencode.server_urlstring"http://127.0.0.1:4096"OpenCode server URL
opencode.modelstring"anthropic/claude-sonnet-4-6"Model identifier in provider/model format
opencode.providerstring"anthropic"LLM provider name
opencode.auto_start_serverbooltrueAuto-start OpenCode server if not running
opencode.server_commandstring"opencode serve"Command to start the server

Hooks

Claude Code hooks provide inline quality gates during task execution.

executor: hooks: enabled: false # Enable Claude Code hooks run_tests_on_stop: true # Stop gate: run tests before completion block_destructive: true # PreToolUse: block destructive Bash commands lint_on_save: false # PostToolUse: run linter after file edits

Learn more about hooks →

FieldTypeDefaultDescription
hooks.enabledboolfalseEnable Claude Code hooks
hooks.run_tests_on_stopbooltrueStop hook runs tests before completion
hooks.block_destructivebooltruePreToolUse hook blocks dangerous commands
hooks.lint_on_saveboolfalsePostToolUse hook runs linter after file changes

Stagnation Monitor

Detects when tasks are stuck in loops or making no progress.

executor: stagnation: enabled: false warn_timeout: 10m pause_timeout: 20m abort_timeout: 30m warn_at_iteration: 8 pause_at_iteration: 12 abort_at_iteration: 15 state_history_size: 5 identical_states_threshold: 3 grace_period: 30s commit_partial_work: true
FieldTypeDefaultDescription
stagnation.enabledboolfalseEnable stagnation detection
stagnation.warn_timeoutduration10mTime before warning alert
stagnation.pause_timeoutduration20mTime before pausing execution
stagnation.abort_timeoutduration30mTime before aborting task
stagnation.warn_at_iterationint8Iteration count before warning
stagnation.pause_at_iterationint12Iteration count before pausing
stagnation.abort_at_iterationint15Iteration count before aborting
stagnation.state_history_sizeint5Size of state history for loop detection
stagnation.identical_states_thresholdint3Identical states needed to detect loop
stagnation.grace_periodduration30sGrace period after intervention
stagnation.commit_partial_workbooltrueCommit partial progress before aborting

Claude Code SDK Features

Advanced Claude Code backend features for session management and structured output.

executor: claude_code: use_session_resume: false # Reuse session for self-review use_from_pr: false # Resume PR context for CI fixes use_structured_output: false # Machine-readable classifier output
FieldTypeDefaultDescription
claude_code.use_session_resumeboolfalseReuse session for self-review (~40% token savings)
claude_code.use_from_prboolfalseResume PR context for autopilot CI fixes
claude_code.use_structured_outputboolfalseEnable structured JSON output for classifiers

Automatically initialize Navigator documentation structure for projects.

executor: navigator: auto_init: true templates_path: "" # Optional custom templates path
FieldTypeDefaultDescription
navigator.auto_initbooltrueAuto-create .agent/ on first task execution
navigator.templates_pathstring""Override plugin templates location

Worktree Isolation

Enable worktree isolation to execute tasks in separate git worktrees, preventing conflicts with uncommitted changes:

executor: use_worktree: true

When enabled:

  • Each task runs in an isolated temporary worktree (/tmp/pilot-worktree-*)
  • Your original repository remains untouched during execution
  • Context config (.agent/) is copied to the worktree
  • Changes are pushed from the worktree branch to remote
  • Automatic cleanup removes worktrees after task completion
  • Orphaned worktrees are cleaned up on Pilot startup

Benefits:

  • Execute tasks with uncommitted local changes
  • No risk of merge conflicts with your work in progress
  • Parallel development: code while Pilot works
  • Clean execution environment for reliable automation

Performance impact: Minimal overhead (~100-200ms per worktree creation)

See the Worktree Isolation concept guide for detailed information on how it works, cleanup processes, and troubleshooting.


Autopilot

Autonomous PR lifecycle management — review, CI monitoring, merge, and release.

orchestrator: autopilot: enabled: false environment: "stage" # dev, stage, prod approval_source: "telegram" # telegram, slack, github-review auto_review: true # self-review before PR auto_merge: true # merge after CI passes merge_method: "squash" # squash, merge, rebase ci_wait_timeout: 30m dev_ci_timeout: 5m # shorter timeout for dev env ci_poll_interval: 30s required_checks: - test - lint auto_create_issues: true # create fix issues on CI failure issue_labels: - pilot - autopilot-fix notify_on_failure: true max_failures: 3 # max fix attempts before giving up max_merges_per_hour: 10 # rate limit merges approval_timeout: 1h merged_pr_scan_window: 30m # scan for recently merged PRs github_review: poll_interval: 30s # poll for GitHub review decisions release: enabled: false trigger: "on_merge" version_strategy: "conventional_commits" tag_prefix: "v" generate_changelog: true notify_on_release: true require_ci: true
FieldTypeDefaultDescription
enabledboolfalseEnable autopilot mode
environmentstring"stage"Environment: dev, stage, prod
approval_sourcestring"telegram"Where to get approvals: telegram, slack, github-review
auto_reviewbooltrueRun self-review before creating PR
auto_mergebooltrueAuto-merge after CI passes
merge_methodstring"squash"Git merge strategy
ci_wait_timeoutduration30mMax time to wait for CI
dev_ci_timeoutduration5mCI timeout in dev environment
ci_poll_intervalduration30sCI status polling interval
required_checks[]string["test","lint"]CI checks that must pass
auto_create_issuesbooltrueCreate fix issues when CI fails
issue_labels[]string["pilot","autopilot-fix"]Labels for auto-created fix issues
notify_on_failurebooltrueSend notification on failure
max_failuresint3Max fix attempts before stopping
max_merges_per_hourint10Rate limit on merges
approval_timeoutduration1hTime before approval request expires
merged_pr_scan_windowduration30mWindow to scan for recently merged PRs
release.enabledboolfalseAuto-create releases on merge
release.triggerstring"on_merge"When to trigger release
release.version_strategystring"conventional_commits"How to determine version bump
release.tag_prefixstring"v"Git tag prefix
release.generate_changelogbooltrueAuto-generate changelog
release.require_cibooltrueRequire CI pass before release

Quality

Quality gates run after Pilot implements changes but before creating a PR. Each gate executes a shell command in the project directory. If a required gate fails, Pilot retries the implementation with the error output as feedback — the LLM sees what broke and fixes it.

How Quality Gates Work

Implementation complete → Run gates in order: build → test → lint → coverage → ... → All required gates pass? → Create PR → Required gate fails? → retry (default): feed error to LLM, re-implement, re-run gates → fail: stop immediately, mark task as failed → warn: log warning, create PR anyway

Even without explicit configuration, Pilot auto-detects your project type and runs a minimal build gate to catch compilation errors. Set quality.enabled: true to configure the full pipeline.

Auto-Detection

When quality gates are not explicitly configured, Pilot detects your project type by checking for marker files and applies a minimal build check:

Project TypeMarker FileAuto-Detected Command
Gogo.modgo build ./...
Node.js (TypeScript)package.json + tsconfig.jsonnpm run build || npx tsc --noEmit
Node.js (JavaScript)package.jsonnpm run build --if-present
RustCargo.tomlcargo check
Pythonpyproject.toml or setup.pypython -m py_compile on changed files

To disable auto-detection, set quality.enabled: false explicitly.

Gate Types

Seven built-in gate types with sensible default timeouts:

TypeDefault TimeoutPurpose
build5mCompilation / build step
test10mTest suite execution
lint2mStatic analysis and formatting
coverage10mCode coverage with threshold enforcement
security5mSecurity scanning (e.g. gosec, npm audit)
typecheck3mType checking (e.g. tsc --noEmit, mypy)
custom5mAny arbitrary command

Project-Specific Examples

quality: enabled: true gates: - name: build type: build command: "go build ./..." required: true timeout: 5m max_retries: 2 failure_hint: "Fix compilation errors in the changed files" - name: test type: test command: "go test ./... -count=1" required: true timeout: 10m max_retries: 2 failure_hint: "Fix failing tests or update test expectations" - name: lint type: lint command: "golangci-lint run ./..." required: false timeout: 3m max_retries: 1 failure_hint: "Fix linting errors: formatting, unused imports, etc." - name: vet type: custom command: "go vet ./..." required: true timeout: 2m failure_hint: "Fix go vet issues" - name: coverage type: coverage command: "go test ./... -coverprofile=coverage.out && go tool cover -func=coverage.out" threshold: 70.0 required: false timeout: 10m
quality: enabled: true gates: - name: typecheck type: typecheck command: "npx tsc --noEmit" required: true timeout: 3m max_retries: 2 failure_hint: "Fix TypeScript type errors" - name: build type: build command: "npm run build" required: true timeout: 5m max_retries: 1 failure_hint: "Fix build errors" - name: test type: test command: "npm test -- --watchAll=false" required: true timeout: 10m max_retries: 2 failure_hint: "Fix failing tests or update snapshots" - name: lint type: lint command: "npx eslint . --max-warnings 0" required: false timeout: 2m max_retries: 1 failure_hint: "Fix ESLint warnings and errors" - name: coverage type: coverage command: "npm test -- --coverage --watchAll=false" threshold: 80.0 required: false timeout: 10m

For monorepos using Turborepo or Nx, prefix commands with the workspace runner: npx turbo build or npx nx run-many --target=build.

quality: enabled: true gates: - name: typecheck type: typecheck command: "mypy src/" required: false timeout: 3m failure_hint: "Fix type annotation errors" - name: test type: test command: "pytest -x --tb=short" required: true timeout: 10m max_retries: 2 failure_hint: "Fix failing tests" - name: lint type: lint command: "ruff check . && ruff format --check ." required: false timeout: 2m max_retries: 1 failure_hint: "Run 'ruff format .' to fix formatting" - name: coverage type: coverage command: "pytest --cov=src --cov-report=term-missing" threshold: 75.0 required: false timeout: 10m - name: security type: security command: "bandit -r src/ -ll" required: false timeout: 3m failure_hint: "Fix security issues flagged by bandit"
quality: enabled: true gates: - name: build type: build command: "cargo check" required: true timeout: 10m max_retries: 2 failure_hint: "Fix compilation errors" - name: test type: test command: "cargo test" required: true timeout: 15m max_retries: 2 failure_hint: "Fix failing tests" - name: lint type: lint command: "cargo clippy -- -D warnings" required: false timeout: 5m max_retries: 1 failure_hint: "Fix clippy warnings" - name: format type: custom command: "cargo fmt -- --check" required: false timeout: 1m failure_hint: "Run 'cargo fmt' to fix formatting"

Coverage Thresholds

Coverage gates parse output from common test runners to extract a percentage value. Supported output formats:

RunnerOutput PatternExample
Gocoverage: X.X% of statementscoverage: 85.3% of statements
Jest / NYCStatements : X.X% or Lines : X.X%Statements : 92.1%
pytest-covTOTAL ... X%TOTAL 450 38 92%

Set threshold on a coverage gate to enforce a minimum:

- name: coverage type: coverage command: "go test ./... -coverprofile=c.out && go tool cover -func=c.out" threshold: 80.0 # fail if coverage drops below 80% required: true # block PR creation timeout: 10m

When a coverage gate is required: false, Pilot logs a warning if coverage is below the threshold but still creates the PR. Set required: true to enforce it as a hard gate.

Failure Handling

The on_failure block defines global behavior when any required gate fails:

quality: on_failure: action: "retry" # retry | fail | warn max_retries: 3 # global retry limit across all gates notify_on: - failed # send notification on these statuses
ActionBehavior
retryFeed the error output back to the LLM, re-implement, re-run all gates. Default.
failStop immediately. Mark the task as failed. Useful for CI-only validation.
warnLog the failure as a warning. Continue to PR creation.

Per-gate retries (max_retries on individual gates) control how many times a single gate retries before reporting failure. Global retries (on_failure.max_retries) control how many full implementation cycles run. These compose: a gate with max_retries: 2 inside a pipeline with on_failure.max_retries: 3 can attempt the gate up to 2×3 = 6 times total.

Failure Hints

The failure_hint field on each gate is passed to the LLM as context when a gate fails. Write hints that help the AI fix the issue:

- name: lint type: lint command: "golangci-lint run ./..." failure_hint: "Fix linting errors. Common issues: unused imports, missing error checks, formatting."

Good hints are specific and actionable. Bad hints are vague (“fix the errors”).

Option Reference

FieldTypeDefaultDescription
enabledboolfalseEnable quality gates
gates[].namestringGate identifier
gates[].typestringGate type: build, test, lint, coverage, security, typecheck, custom
gates[].commandstringShell command to run
gates[].requiredboolfalseBlock PR if gate fails
gates[].timeoutdurationvariesMax execution time (see gate type defaults above)
gates[].thresholdfloat64Minimum threshold (coverage gates)
gates[].max_retriesint0Retry count per gate on failure
gates[].retry_delayduration0Delay between retries
gates[].failure_hintstringContext message passed to LLM on failure
on_failure.actionstring"retry"Default action: retry, fail, warn
on_failure.max_retriesintGlobal retry limit (full re-implementation cycles)
on_failure.notify_on[]stringStatuses that trigger notifications: failed, passed, skipped

Budget

Cost controls for API usage. Prevents runaway spending by enforcing daily, monthly, and per-task limits with configurable enforcement actions.

Budget enforcement is disabled by default. Enable it explicitly with budget.enabled: true. When disabled, Pilot tracks no costs and applies no limits.

How Budget Enforcement Works

┌─────────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────┐ │ New task │────▶│ Check budget │────▶│ Within limits? │────▶│ Execute │ │ arrives │ │ (enforcer) │ │ │ │ task │ └─────────────┘ └──────────────┘ └───────┬────────┘ └────┬─────┘ │ No │ ┌──────▼──────┐ ┌─────▼──────┐ │ Apply action │ │ Track cost │ │ warn/pause/ │ │ per event │ │ stop │ └────────────┘ └─────────────┘

Budget checks run before each task starts. The enforcer queries accumulated spend from the metering database, compares against configured limits, and applies the configured action if a limit is exceeded. If the usage provider returns an error, the enforcer fails open — the task is allowed to proceed (logged as a warning).

Minimal Configuration

budget: enabled: true daily_limit: 50.00 monthly_limit: 500.00

This enables budget tracking with sensible defaults for per-task limits and enforcement actions.

Full Configuration

budget: enabled: true daily_limit: 50.00 # USD per day monthly_limit: 500.00 # USD per month per_task: max_tokens: 100000 # hard cap per task execution max_duration: 30m # context timeout per task on_exceed: daily: "pause" # pause new tasks, finish current monthly: "stop" # terminate immediately per_task: "stop" # kill task that exceeds thresholds: warn_percent: 80 # alert at 80% of any limit

Option Reference

FieldTypeDefaultDescription
enabledboolfalseEnable budget tracking and enforcement
daily_limitfloat6450.00Maximum daily spend in USD. Resets at midnight local time
monthly_limitfloat64500.00Maximum monthly spend in USD. Resets on the 1st of each month
per_task.max_tokensint64100000Maximum tokens (input + output) a single task may consume
per_task.max_durationduration30mMaximum wall-clock time for a single task. Creates a context deadline
on_exceed.dailystring"pause"Action when daily limit is hit: warn, pause, stop
on_exceed.monthlystring"stop"Action when monthly limit is hit: warn, pause, stop
on_exceed.per_taskstring"stop"Action when per-task limit is hit: warn, pause, stop
thresholds.warn_percentfloat6480Percentage of any limit that triggers a warning alert

Per-Task Limits

Per-task limits protect against individual runaway executions — a single complex issue consuming your entire daily budget.

Token limit (per_task.max_tokens): Tracks cumulative input + output tokens during task execution. When exceeded, the task is terminated based on on_exceed.per_task.

Duration limit (per_task.max_duration): Creates a Go context with a deadline. When the deadline expires, the executor’s context is cancelled and the task stops gracefully.

# Small tasks, tight control per_task: max_tokens: 50000 # ~$0.25 per task max_duration: 15m
# Default — handles most issues per_task: max_tokens: 100000 # ~$0.50 per task max_duration: 30m
# Complex tasks, multi-file changes per_task: max_tokens: 500000 # ~$2.50 per task max_duration: 1h

Setting max_tokens: 0 or max_duration: 0 disables that specific per-task limit. The task will run without that constraint (other limits still apply).

Daily and Monthly Caps

Daily and monthly caps control aggregate spend across all tasks.

  • Daily limit resets at midnight local time. If Pilot was paused due to a daily limit, it automatically resumes at the next reset.
  • Monthly limit resets on the 1st of each month. Monthly pauses do NOT auto-resume — use pilot budget reset --confirm or wait for the new month.
budget: daily_limit: 25.00 # $25/day monthly_limit: 200.00 # $200/month — ~8 working days

The enforcer checks monthly limits first (more severe), then daily limits. This means a monthly stop action takes priority over a daily pause.

Enforcement Actions: Warn, Pause, Stop

Each limit boundary has an independently configurable action:

ActionBehaviorCurrent TaskNew TasksAuto-Resume
warnLog warning + fire alertContinuesAllowedN/A
pauseBlock new tasksFinishes normallyBlocked (queued)Yes, at next daily reset
stopTerminate immediatelyKilledBlockedNo — manual reset required

Recommended configurations by use case:

# Solo: catch runaway tasks, don't lose work on_exceed: daily: "warn" # just notify, keep working monthly: "pause" # stop new tasks at month cap per_task: "stop" # kill individual runaway tasks
# Team: strict daily, hard monthly on_exceed: daily: "pause" # pause at daily cap monthly: "stop" # hard stop at monthly cap per_task: "stop" # kill runaway tasks
# Production: hard limits everywhere on_exceed: daily: "stop" monthly: "stop" per_task: "stop"

Warning Thresholds and Alerts

The warn_percent threshold fires alerts before a limit is exceeded. Alerts are delivered through your configured alert channels (Slack, Telegram, email, PagerDuty — see Alerts).

thresholds: warn_percent: 80 # alert at 80% of daily or monthly limit

Alert types fired by the budget enforcer:

Alert TypeSeverityTrigger
daily_budget_warningwarningDaily spend reaches warn_percent
monthly_budget_warningwarningMonthly spend reaches warn_percent
daily_budget_exceededcriticalDaily spend reaches 100%
monthly_budget_exceededcriticalMonthly spend reaches 100%

Cost Tracking Model

Pilot tracks five event types that contribute to spend calculations:

Event TypeUnitRateDescription
taskper execution$1.00Flat fee per task execution
tokenper 1M tokens$3.60 input / $18.00 outputClaude API tokens (Sonnet pricing + 20% margin)
computeper minute$0.01Execution wall-clock time
storageper GB/month$0.10Memory and log storage
api_callper call$0.001External API calls (GitHub, Linear, etc.)

Token pricing includes a 20% platform margin over base Claude API rates. Opus 4.6 pricing ($5/$25 per 1M tokens) is tracked separately for tasks routed to Opus.

CLI Commands

Monitor and manage budget from the command line:

# View current spend vs limits with progress bars pilot budget status # View budget configuration and YAML template pilot budget config # Reset blocked tasks counter and resume daily-paused execution pilot budget reset --confirm

pilot budget status displays color-coded progress bars:

  • White: Under 80% of limit
  • Yellow: 80-99% of limit (warning zone)
  • Red: 100%+ (limit exceeded)

The status also shows paused/blocked indicators and per-task enforcement settings.

pilot budget reset only clears the blocked tasks counter and resumes execution paused by daily limits. Monthly limit pauses require waiting for the new month or updating monthly_limit in config.


Orchestrator

Controls task execution strategy and scheduling.

orchestrator: model: "claude-sonnet-4-6" # default model for planning max_concurrent: 2 # max parallel tasks execution: mode: "sequential" # sequential | parallel | auto wait_for_merge: true # wait for PR merge before next task poll_interval: 30s pr_timeout: 1h # max wait for PR merge daily_brief: enabled: false schedule: "0 9 * * 1-5" # cron: 9 AM weekdays timezone: "America/New_York" channels: - type: slack channel: "#dev-updates" content: include_metrics: true include_errors: true max_items_per_section: 10 filters: projects: [] # empty = all projects
FieldTypeDefaultDescription
modelstring"claude-sonnet-4-6"Default model for planning
max_concurrentint2Max parallel task executions
execution.modestring"sequential"sequential — wait for PR merge before next task; parallel — dispatch all tasks concurrently; auto — parallel dispatch with scope-overlap guard; issues targeting different directories run concurrently, overlapping scopes are serialized
execution.wait_for_mergebooltrueWait for PR merge before next task
execution.poll_intervalduration30sPR status poll interval
execution.pr_timeoutduration1hMax wait for PR merge
daily_brief.enabledboolfalseEnable daily summary
daily_brief.schedulestring"0 9 * * 1-5"Cron schedule (5-field cron syntax)
daily_brief.timezonestring"America/New_York"IANA timezone for schedule
daily_brief.channels[].typestringDelivery channel: slack, telegram, email
daily_brief.channels[].channelstringChannel name or ID (e.g. #dev-updates)
daily_brief.channels[].recipients[]stringEmail recipients (for email type)
daily_brief.content.include_metricsbooltrueInclude task/cost metrics in brief
daily_brief.content.include_errorsbooltrueInclude error summaries
daily_brief.content.max_items_per_sectionint10Max items per brief section
daily_brief.filters.projects[]string[]Project name filter (empty = all projects)

Alerts

Configurable alerting for operational events, cost, and security.

alerts: enabled: true defaults: cooldown: 5m default_severity: "warning" suppress_duplicates: true channels: - name: slack-ops type: slack enabled: true severities: [warning, critical] slack: channel: "#pilot-alerts" - name: telegram-admin type: telegram enabled: true severities: [critical] telegram: chat_id: 123456789 - name: pagerduty type: pagerduty enabled: false severities: [critical] pagerduty: routing_key: "${PAGERDUTY_KEY}" rules: - name: task_stuck type: task_stuck enabled: true severity: warning channels: [slack-ops] condition: progress_unchanged_for: 10m - name: consecutive_failures type: consecutive_failures enabled: true severity: critical channels: [slack-ops, telegram-admin] cooldown: 30m condition: consecutive_failures: 3 - name: daily_spend type: daily_spend_exceeded enabled: false severity: warning channels: [slack-ops] condition: daily_spend_threshold: 50.0

Defaults

FieldTypeDefaultDescription
enabledboolfalseEnable alerting system
defaults.cooldownduration5mMinimum time between duplicate alerts
defaults.default_severitystring"warning"Severity when rule doesn’t specify one
defaults.suppress_duplicatesbooltruePrevent repeated alerts for the same event

Channel configuration

FieldTypeDefaultDescription
channels[].namestringUnique channel identifier
channels[].typestringChannel type: slack, telegram, email, webhook, pagerduty
channels[].enabledboolfalseEnable this channel
channels[].severities[]stringSeverity levels routed here: info, warning, critical
channels[].slack.channelstringSlack channel name (e.g. #pilot-alerts)
channels[].telegram.chat_idint64Telegram chat/user ID
channels[].email.to[]stringRecipient email addresses
channels[].email.subjectstringCustom subject template (optional)
channels[].webhook.urlstringWebhook endpoint URL
channels[].webhook.methodstring"POST"HTTP method: POST, PUT
channels[].webhook.headersmapCustom HTTP headers
channels[].webhook.secretstringHMAC signing secret
channels[].pagerduty.routing_keystringPagerDuty integration/routing key
channels[].pagerduty.service_idstringPagerDuty service ID

Rule configuration

FieldTypeDefaultDescription
rules[].namestringUnique rule identifier
rules[].typestringAlert type (see supported types below)
rules[].enabledboolfalseEnable this rule
rules[].severitystring"warning"Severity: info, warning, critical
rules[].channels[]stringChannel names to route this alert to
rules[].cooldownduration5mOverride default cooldown for this rule
rules[].descriptionstringHuman-readable rule description
rules[].condition.progress_unchanged_fordurationTrigger when task has no progress for this long
rules[].condition.consecutive_failuresintTrigger after N consecutive task failures
rules[].condition.daily_spend_thresholdfloat64Trigger when daily spend exceeds this USD amount
rules[].condition.budget_limitfloat64Trigger when total budget reaches this USD limit
rules[].condition.usage_spike_percentfloat64Trigger on usage spike exceeding this percentage
rules[].condition.patternstringRegex pattern to match (security rules)
rules[].condition.file_patternstringFile glob pattern (sensitive file rules)
rules[].condition.paths[]stringFile paths to monitor

Supported alert types: task_stuck, task_failed, consecutive_failures, service_unhealthy, daily_spend_exceeded, budget_depleted, usage_spike, unauthorized_access, sensitive_file_modified, unusual_pattern

Built-in rules (pre-configured in defaults):

RuleTypeDefault StateSeverityCondition
task_stucktask_stuckenabledwarningNo progress for 10m, cooldown 15m
task_failedtask_failedenabledwarningAny task failure, no cooldown
consecutive_failuresconsecutive_failuresenabledcritical3 failures in a row, cooldown 30m
daily_spenddaily_spend_exceededdisabledwarningDaily spend > $50, cooldown 1h
budget_depletedbudget_depleteddisabledcriticalBudget > $500, cooldown 4h

Approval

Multi-stage approval workflow for task execution and merging.

approval: enabled: false default_timeout: 1h default_action: "rejected" # action on timeout pre_execution: enabled: false approvers: ["@admin"] timeout: 1h default_action: "rejected" require_all: false # any one approver suffices pre_merge: enabled: false approvers: ["@lead"] timeout: 24h default_action: "rejected" require_all: false post_failure: enabled: false approvers: ["@admin"] timeout: 1h default_action: "rejected"
FieldTypeDefaultDescription
enabledboolfalseEnable approval workflow
default_timeoutduration1hDefault timeout for approval requests
default_actionstring"rejected"Action on timeout: approved, rejected
pre_execution.enabledboolfalseRequire approval before task execution
pre_merge.enabledboolfalseRequire approval before PR merge
post_failure.enabledboolfalseRequire approval to retry after failure
*.approvers[]stringUser IDs or handles who can approve
*.timeoutdurationvariesTimeout per stage
*.require_allboolfalseRequire all approvers (vs any one)

Logging

Configure log output format and rotation.

logging: level: "info" # debug, info, warn, error format: "text" # text, json output: "stdout" # stdout, stderr, or file path rotation: max_size: "100MB" max_age: "7d" max_backups: 5
FieldTypeDefaultDescription
levelstring"info"Log level: debug, info, warn, error
formatstring"text"Output format: text, json
outputstring"stdout"Destination: stdout, stderr, or file path
rotation.max_sizestringMax log file size before rotation
rotation.max_agestringMax age before deletion
rotation.max_backupsintMax rotated files to keep

Tunnel

Expose your local Pilot instance to the internet for webhooks.

tunnel: enabled: false provider: "cloudflare" # cloudflare, ngrok, manual domain: "" # custom domain (optional) port: 9090 # local port to tunnel
FieldTypeDefaultDescription
enabledboolfalseEnable tunnel
providerstring"cloudflare"Tunnel provider: cloudflare, ngrok, manual
domainstringCustom domain for tunnel
portint9090Local port to expose

Webhooks

Outbound webhooks for integrating Pilot events with external systems.

webhooks: enabled: true defaults: timeout: 30s retry: max_attempts: 3 initial_delay: 1s max_delay: 60s multiplier: 2.0 endpoints: - name: "ci-notify" url: "https://example.com/webhook" secret: "${WEBHOOK_SECRET}" # HMAC-SHA256 signing enabled: true timeout: 30s events: - task.completed - task.failed - pr.created headers: X-Custom-Header: "value" retry: max_attempts: 3
FieldTypeDefaultDescription
enabledboolfalseEnable outbound webhooks
defaults.timeoutduration30sDefault HTTP timeout for webhook calls
defaults.retry.max_attemptsint3Default max retry attempts
defaults.retry.initial_delayduration1sDelay before first retry
defaults.retry.max_delayduration60sMaximum backoff delay
defaults.retry.multiplierfloat642.0Exponential backoff multiplier

Endpoint configuration

FieldTypeDefaultDescription
endpoints[].namestringUnique endpoint identifier
endpoints[].urlstringWebhook delivery URL
endpoints[].secretstringHMAC-SHA256 signing secret
endpoints[].enabledboolfalseEnable this endpoint
endpoints[].timeoutduration30sOverride default timeout
endpoints[].events[]stringEvents to deliver (see supported list)
endpoints[].headersmapCustom HTTP headers to include
endpoints[].retry.max_attemptsint3Override default retry count
endpoints[].retry.initial_delayduration1sOverride retry initial delay
endpoints[].retry.max_delayduration60sOverride retry max delay
endpoints[].retry.multiplierfloat642.0Override backoff multiplier

Supported events: task.started, task.progress, task.completed, task.failed, task.timeout, pr.created, budget.warning


Gateway

Internal HTTP/WebSocket server configuration.

gateway: host: "127.0.0.1" port: 9090 auth: type: "claude-code" # claude-code or api-token token: "" # required if type is api-token
FieldTypeDefaultDescription
hoststring"127.0.0.1"Bind address for the HTTP/WebSocket server
portint9090Port number (1–65535)
auth.typestring"claude-code"Auth mode: claude-code (built-in) or api-token
auth.tokenstringBearer token, required when auth.type is api-token

Container deployments: Set gateway.host: "0.0.0.0" when running in Docker or Kubernetes. The default 127.0.0.1 only accepts loopback connections — health probes and ingress traffic will fail. See the Docker & Helm guide for container-specific configuration.

Container Config Mounting

When running in a container, mount config.yaml as a read-only volume:

# docker-compose.yml volumes: - ./config.yaml:/home/pilot/.pilot/config.yaml:ro
# Kubernetes ConfigMap apiVersion: v1 kind: ConfigMap metadata: name: pilot-config data: config.yaml: | version: "1.0" gateway: host: "0.0.0.0" port: 9090 adapters: github: enabled: true token: "${GITHUB_TOKEN}" repo: "your-org/your-repo"

Other Adapters

Linear

adapters: linear: enabled: false api_key: ${LINEAR_API_KEY} team_id: "TEAM_ID" pilot_label: "pilot" auto_assign: true polling: enabled: true interval: 30s # Multi-workspace support workspaces: - name: "main" api_key: ${LINEAR_API_KEY} team_id: "TEAM_ID" pilot_label: "pilot" projects: ["my-project"]
FieldTypeDefaultDescription
enabledboolfalseEnable Linear adapter
api_keystringLinear API key (legacy single-workspace)
team_idstringLinear team ID (legacy single-workspace)
pilot_labelstring"pilot"Label that marks issues for Pilot
auto_assignboolfalseAuto-assign issues to Pilot
project_ids[]stringFilter to specific Linear project IDs
polling.enabledboolfalseEnable issue polling
polling.intervalduration30sPolling interval
workspaces[].namestringWorkspace identifier
workspaces[].api_keystringAPI key for this workspace
workspaces[].team_idstringTeam ID in this workspace
workspaces[].pilot_labelstring"pilot"Label for this workspace
workspaces[].projects[]stringProject filter for this workspace
workspaces[].auto_assignboolfalseAuto-assign in this workspace

GitLab

adapters: gitlab: enabled: false token: ${GITLAB_TOKEN} base_url: "https://gitlab.com" # self-hosted URL project: "namespace/project" webhook_secret: "" pilot_label: "pilot" polling: enabled: false interval: 30s stale_label_cleanup: enabled: true interval: 30m threshold: 1h
FieldTypeDefaultDescription
enabledboolfalseEnable GitLab adapter
tokenstringGitLab personal or project access token
base_urlstring"https://gitlab.com"GitLab instance URL (for self-hosted)
projectstringProject in namespace/project format
webhook_secretstringHMAC secret for webhook verification
pilot_labelstring"pilot"Label that marks issues for Pilot
polling.enabledboolfalseEnable issue polling
polling.intervalduration30sPolling interval
polling.labelstring"pilot"Label to filter when polling
stale_label_cleanup.enabledbooltrueAuto-remove stale in-progress labels
stale_label_cleanup.intervalduration30mCleanup check interval
stale_label_cleanup.thresholdduration1hLabel age before cleanup

Slack

adapters: slack: enabled: false bot_token: ${SLACK_BOT_TOKEN} channel: "#pilot-updates" signing_secret: ""
FieldTypeDefaultDescription
enabledboolfalseEnable Slack adapter
bot_tokenstringSlack bot token (xoxb-...)
channelstringDefault notification channel
signing_secretstringSlack signing secret for webhook verification

Discord

adapters: discord: enabled: true bot_token: ${DISCORD_BOT_TOKEN} allowed_guilds: - "123456789012345678" # guild (server) IDs allowed to send tasks allowed_channels: - "987654321098765432" # channel IDs allowed to send tasks command_prefix: "!pilot" rate_limit: messages_per_second: 5 tasks_per_minute: 10
FieldTypeDefaultDescription
enabledboolfalseEnable Discord adapter
bot_tokenstringDiscord bot token
allowed_guildslistGuild (server) IDs allowed to send tasks
allowed_channelslistChannel IDs allowed to send tasks
command_prefixstringPrefix for bot commands (e.g. !pilot)
rate_limit.messages_per_secondint5Max outgoing messages per second
rate_limit.tasks_per_minuteint10Max tasks accepted per minute

Plane

adapters: plane: enabled: true base_url: "https://api.plane.so" # or self-hosted URL api_key: ${PLANE_API_KEY} workspace_slug: "my-workspace" project_ids: - "project-uuid-1" pilot_label: "pilot" webhook_secret: "" polling: enabled: true interval: 30s
FieldTypeDefaultDescription
enabledboolfalseEnable Plane adapter
base_urlstring"https://api.plane.so"Plane API base URL (self-hosted or cloud)
api_keystringPlane API key (X-API-Key header)
workspace_slugstringPlane workspace slug
project_idslistProject UUIDs to watch for work items
pilot_labelstring"pilot"Label that marks work items for Pilot
webhook_secretstringHMAC secret for webhook verification
polling.enabledboolfalseEnable work item polling
polling.intervalduration30sHow often to poll for new work items

Jira

adapters: jira: enabled: false platform: "cloud" # cloud or server base_url: "https://myorg.atlassian.net" username: "bot@example.com" api_token: ${JIRA_API_TOKEN} webhook_secret: "" pilot_label: "pilot" transitions: in_progress: "In Progress" done: "Done"
FieldTypeDefaultDescription
enabledboolfalseEnable Jira adapter
platformstring"cloud"Jira platform: cloud or server
base_urlstringJira instance URL
usernamestringJira username or email
api_tokenstringJira API token
webhook_secretstringWebhook verification secret
pilot_labelstring"pilot"Label that marks issues for Pilot
transitions.in_progressstringJira transition name for “In Progress”
transitions.donestringJira transition name for “Done”

Asana

adapters: asana: enabled: false access_token: ${ASANA_ACCESS_TOKEN} workspace_id: "1234567890" webhook_secret: "" pilot_tag: "pilot"
FieldTypeDefaultDescription
enabledboolfalseEnable Asana adapter
access_tokenstringAsana personal access token
workspace_idstringAsana workspace ID
webhook_secretstringWebhook verification secret
pilot_tagstring"pilot"Tag that marks tasks for Pilot

Azure DevOps

adapters: azure_devops: enabled: false pat: ${AZURE_DEVOPS_PAT} organization: "myorg" project: "MyProject" repository: "myrepo" base_url: "https://dev.azure.com" pilot_tag: "pilot" work_item_types: - "User Story" - "Bug" polling: enabled: true interval: 30s stale_label_cleanup: enabled: true interval: 30m threshold: 1h
FieldTypeDefaultDescription
enabledboolfalseEnable Azure DevOps adapter
patstringPersonal access token
organizationstringAzure DevOps organization name
projectstringProject name
repositorystringRepository name
base_urlstring"https://dev.azure.com"Azure DevOps instance URL
webhook_secretstringWebhook verification secret
pilot_tagstring"pilot"Tag that marks work items for Pilot
work_item_types[]stringWork item types to process (e.g. User Story, Bug)
polling.enabledboolfalseEnable work item polling
polling.intervalduration30sPolling interval
stale_label_cleanup.enabledbooltrueAuto-remove stale in-progress tags
stale_label_cleanup.intervalduration30mCleanup check interval
stale_label_cleanup.thresholdduration1hTag age before cleanup

Teams

Team-based project access control for multi-user environments. When configured, task execution is scoped to the member’s allowed projects.

team: enabled: true team_id: "engineering" member_email: "dev@example.com"
FieldTypeDefaultDescription
enabledboolfalseEnable team-based access control
team_idstringTeam ID or name to scope execution
member_emailstringEmail of the member executing tasks

Environment variable alternative:

PILOT_TEAM_ID="engineering" PILOT_MEMBER_EMAIL="dev@example.com"

Team configuration is optional. When not configured, Pilot runs without access restrictions. Enable it for multi-user deployments where you need to scope task execution to specific projects per team member.


Projects

Multi-project configuration for managing multiple repositories.

projects: - name: "backend" path: "/path/to/backend" default_branch: "main" navigator: true github: owner: "myorg" repo: "backend" - name: "frontend" path: "/path/to/frontend" default_branch: "main" default_project: "backend" memory: path: "~/.pilot/data" cross_project: true # share memory across projects

Project fields

FieldTypeDefaultDescription
projects[].namestringUnique project name (used in CLI and logs)
projects[].pathstringAbsolute filesystem path to the repository
projects[].default_branchstring"main"Default git branch for PRs
projects[].navigatorboolfalseEnable context intelligence for this project
projects[].github.ownerstringGitHub organization or user
projects[].github.repostringGitHub repository name
default_projectstringName of the project used when none is specified

Memory

FieldTypeDefaultDescription
memory.pathstring"~/.pilot/data"Storage directory for SQLite and knowledge graph
memory.cross_projectbooltrueShare learned patterns across all projects

Dashboard

FieldTypeDefaultDescription
dashboard.refresh_intervalint1000TUI refresh interval in milliseconds
dashboard.show_logsbooltrueShow execution logs panel in dashboard

Full Example

This is a production-ready configuration. Adjust values for your environment — start with environment: dev and auto_merge: false until you’re comfortable with the workflow.

version: "1.0" adapters: github: enabled: true token: ${GITHUB_TOKEN} repo: "myorg/myapp" project_path: "~/Projects/myapp" pilot_label: "pilot" polling: enabled: true interval: 30s telegram: bot_token: ${TELEGRAM_BOT_TOKEN} allowed_ids: [123456789] project_path: "~/Projects/myapp" executor: type: "claude-code" model_routing: enabled: true trivial: "claude-haiku" simple: "claude-sonnet-4-6" medium: "claude-sonnet-4-6" complex: "claude-opus-4-6" timeout: default: "30m" complex: "60m" orchestrator: max_concurrent: 2 execution: mode: "sequential" wait_for_merge: true autopilot: enabled: true environment: "dev" auto_merge: false max_failures: 3 quality: enabled: true gates: - name: build type: build command: "make build" required: true timeout: 5m - name: test type: test command: "make test" required: true timeout: 10m budget: enabled: true daily_limit: 50.00 monthly_limit: 500.00 thresholds: warn_percent: 80 logging: level: "info" format: "text"